Listview smooth scroll until item is at top - android

I am working on an android application, I have a list view with 10 items. I need to do the following. When the user clicks a button, I want the list to smooth scroll to item at position 5, so this item is displayed on the top of the list.
I have found 2 methods that can be used for this, but both methods are not working exactly how I need:
listView.setSelection(5) this will scroll to the row and put it on top of the list But without animation
list.smoothScrollToPosition(5) this will scroll the listview untill the row is visible but it will not put it on top (it is at the bottom of the page) and if the row is allready visible it will not scroll as it considers it is visible.
So is there a way to have the same behavior as the setSelection method but with smoothscrolling?
Thank you

I believe smoothScrollToPositionFromTop() does what you want.
There's also one that will take the desired animation duration in milliseconds as an argument.

So is there a way to have the same behavior as the setSelection method
but with smoothscrolling?
You could post a delayed Runnable and create your own smooth scroll effect using ListView.setSelection. Here's an example:
private ListView mListView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mListView.post(new PositionScroller(this));
}
private static final class PositionScroller implements Runnable {
private static final int SMOOTH_SCROLL_DURATION = 25;
private int mSelectedPosition;
private final WeakReference<YourParentActivity> mParent;
private PositionScroller(YourParentActivity parent) {
mParent = new WeakReference<YourParentActivity>(parent);
}
#Override
public void run() {
final ListView list = mParent.get().mListView;
if (mSelectedPosition <= 5) {
if (list.postDelayed(this, SMOOTH_SCROLL_DURATION)) {
list.setSelection(mSelectedPosition++);
}
}
}
}

Related

disable the scroll effect of the action list

I've got a GuidedStepSupportFragment fragment like this.
public class SampleStepFragment extends GuidedStepSupportFragment {
#NonNull
#Override
public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = "Title";
String breadcrumb = "Breadcrumb";
String description = "Description";
Drawable icon = getActivity().getDrawable(R.drawable.ic_videocam_black_24dp);
return new GuidanceStylist.Guidance(title, description, breadcrumb, icon);
}
#Override
public void onCreateActions(#NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
addAction(actions, ACTION_CONTINUE, "Action1");
addAction(actions, ACTION_BACK, "Action2");
}
}
Problem: When I scroll the action list, it shows like this;
But I want to something like this;
How can I disable this effect on my action list?
Thanks
I managed it and it wasn't easy to figure out.
There's no supported way of doing it, since the APIs that actually make this possible are package private or hidden from public use on purpose. (You can do it yourself, but you just end up copying classes from the leanback libraries.)
The solution:
#Override
public void onCreateActions(#NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
addAction(actions, GuidedAction.ACTION_ID_CONTINUE, "Action1");
addAction(actions, GuidedAction.ACTION_ID_CANCEL, "Action2");
// Run code delayed on mainThread (any other/better method can/should be used)
// It's delayed because if focus scroll is disabled, the list will stick to the top of the layout
new Handler(Looper.getMainLooper()).postDelayed(this::disableFocusScroll, 500);
}
private void disableFocusScroll() {
RecyclerView.LayoutManager layoutManager = SampleStepFragment.this.getGuidedActionsStylist().getActionsGridView().getLayoutManager();
try {
Method method = layoutManager.getClass().getMethod("setFocusScrollStrategy", int.class);
method.invoke(layoutManager, 1 /* FOCUS_SCROLL_ITEM */);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
Log.e(TAG, "disableFocusScroll: ", e);
}
}
Full example
The explanation:
A GuidedStepSupportFragment requests a GuidedActionsStylist which is responsible for rendering the list items on the right side. source
The GuidedActionsStylist stylist inflates the layout lb_guidedactions.xml which contains a VerticalGridView source
The VerticalGridView extends BaseGridView and creates a GridLayoutManager as its layout manager. This GridLayoutManager is sadly package private and final... (android why..?). It has the method setFocusScrollStrategy which is used to determine how scrolling behaves.source
See the different focus scroll strategies:
/**
* Always keep focused item at a aligned position. Developer can use
* WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
* In this mode, the last focused position will be remembered and restored when focus
* is back to the view.
* #hide
*/
#RestrictTo(LIBRARY_GROUP)
public final static int FOCUS_SCROLL_ALIGNED = 0;
/**
* Scroll to make the focused item inside client area.
* #hide
*/
#RestrictTo(LIBRARY_GROUP)
public final static int FOCUS_SCROLL_ITEM = 1;
/**
* Scroll a page of items when focusing to item outside the client area.
* The page size matches the client area size of RecyclerView.
* #hide
*/
#RestrictTo(LIBRARY_GROUP)
public final static int FOCUS_SCROLL_PAGE = 2;
So since the API is hidden, we just use reflection to expose the setFocusScrollStrategy method and set it to FOCUS_SCROLL_ITEM.
We can't do this immediately though, since without the default scroll setting, the list items will pop to the top of the layout and won't stay centered. So I added a delay of 500ms which is horrible... If you manage to find out when it's best to trigger this, let me know.
It turns out there is a much more elegant and simpler solution. It is enough to add to the body of onViewCreated in the class that inherits guideStepSupportFragment windowAlignment option (there are other alignment options):
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
val gridView = guidedActionsStylist.actionsGridView
gridView.windowAlignment = VerticalGridView.WINDOW_ALIGN_BOTH_EDGE
// ...
super.onViewCreated(view, savedInstanceState)
}
Here is a simple way to disable VerticalGridView scrolling
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
gridView.setScrollEnabled(false);
super.onViewCreated(view, savedInstanceState);
}

Android Listview.getChildAt() points to two items

I have an activity that has a single listview in it with enough items to extend of the page.
I want to set a certain listView item at position i to a different drawable.
To go this I use the line of code..
listView.getChildAt(selector).setBackgroundResource(R.drawable.main_button_shape_pressed);
There is a very confusing problem going in. This line of code is setting two listView items to the specified drawable.
When i = 0 item 0 and item 11 are set to that drawable. It turns out that when I call this line of code with i both item i and item i+11 are set to that drawable. This is rather baffling. Then to mix EVERYTHING when I start the activity in landscape, it is a different second listview that gets set to that drawable. And in certain scenarios when I change from portrait to landscape, the current highlight listview item on screen will change to a different one.
WTF is going on with the listview class? Are the indexes to it children constantly pointing to different things?
Here is my entire activity.
public class SelectorActivity extends Activity {
private ListView listView;
private int selector;
private boolean set;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.selector_layout);
set=false;
Bundle extras = getIntent().getExtras();
if(extras!=null)
{
selector=extras.getInt("selector");
}
listView=(ListView)findViewById(R.id.selector_layout);
//set the string array for the listview
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.sounds_array, android.R.layout.simple_list_item_1);
adapter.setDropDownViewResource(android.R.layout.simple_list_item_1);
listView.setBackgroundResource(R.drawable.listview_background);
listView.setAdapter(adapter);
highlightSelected();
}
//this method will highlight a selected listview once that listview is drawn
private void highlightSelected()
{
if(!set)
{
new Thread(
new Runnable()
{
#Override
public void run() {
// TODO Auto-generated method stub
boolean trigger=true;
while(trigger)
{
if(listView.getChildAt(selector)!=null)
{
set=true;
trigger=false;
listView.getChildAt(selector).setBackgroundResource(R.drawable.main_button_shape_pressed);
}
}
}
}
).start();
}
}
}
ListViews recycle their children. While drawing itself, the ListView will create a new view for every visible child. When you scroll, it will then re-use the last view that became non-visible (scrolled off the screen) as the next view in the list. That is why it's a different view index in landscape and that is why it would probably be a different view index on a device with a different screen size.
The solution should be to reset the view background in the Adapter's getView() method.
Additionally, touching views on anything other than the UI (main) thread is a bad practice. Check the selected item index in the getView() method and set the background right there. You'll also need to handle the case where the selected index changes (unless it never changes after this activity is created) by iterating over visible views in the listview and setting their backgrounds to the appropriate values.
// Must be final to use inside the ArrayAdapter
final int selector = extras == null ? -1 : extras.getInt("selector");
ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(
this,
R.array.sounds_array,
android.R.layout.simple_list_item_1) {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View newView = super.getView(position, convertView, parent);
// set the background according to whether this is the selected item
if (position == selector) {
// this is the selected item
newView.setBackgroundResource(R.drawable.main_button_shape_pressed);
} else {
// default background for simple_list_item_1 is nothing
newView.setBackground(null);
}
return newView;
}
};

Android: ListView.getChildAt() throws null

Sometimes, when I call goToLast() it throws me a null exception in vista=lista.getChildAt(), it happens when the list is full, I dont know why I have this code:
private void goToLast() {
lista.post(new Runnable() {
#Override
public void run() {
lista.setSelection(mensajes.getCount() - 1);
View vista = lista.getChildAt(mensajes.getCount() - 1);
TextView txtMensaje = (TextView)vista.findViewById(R.id.txtMensajeLista);
txtMensaje.setBackgroundColor(Color.RED);
}
});
}
You should log lista.getChildCount(), see how many children the ListView has. ListView recycles views, which means it only hold limit number of views.
So if you want to get the last view, you should do something like this
private void goToLast() {
lista.post(new Runnable() {
#Override
public void run() {
lista.setSelection(mensajes.getCount() - 1);
// Figure out the last position of the view on the list.
int lastViewIndex = lista.getChildCount() - 1;
View vista = lista.getChildAt(lastViewIndex);
TextView txtMensaje = (TextView)vista.findViewById(R.id.txtMensajeLista);
txtMensaje.setBackgroundColor(Color.RED);
}
});
}
ListView.getChildAt() position is different to the position in your adapter. ListView recycles its views, so if you have more items in your adapter, than can fit on the screen it will not create views for all of those, but only the ones that are visible. If you want to update an item in the ListView you need to update it in the adapter and call notifyDataSetChanged()

Swipe / (Fling) with dynamic views

What i want to:
I want to add a swipe or what i learned it's named on android fling, to my app.
I have a dynamic number of views, the number is the amount of dates from an ICS file which i parse, i want to make a swipe effekt on all of these views together.
But if i have let's say 12 of these each having a ListView with 8 items (max) it would use a lot of memory i guess. So i would like that only the 2 before current selected view and the 2 after to be initialized.
What i have tried:
In my search i stumpled around this stackoverflow question which mentions HorizontalPager. But i dont know to make it work with a number of ListView's and load them dynamically.
I tried a ViewGroup and then add and remove a ListView but it's not working, it display's the ViewGroup but not the ListView
public class HourView extends ViewGroup
{
private ListView listView;
/**
* #param context
*/
public HourView(Context context)
{
super(context);
init(false);
}
/**
*
* #param context
* #param day the current day
*/
public HourView(Context context, String day,
boolean shouldBeVisible)
{
super(context);
this.day = day;
init(shouldBeVisible);
}
private void init(boolean shouldBeVisible)
{
if (shouldBeVisible)
{
listView = new ListView(getContext());
if (day == null)
{
day = Event.dtFormatDay.format(new Date());
}
new GetEvents(day).execute();
addView(listView);
}
else
{
removeAllViews();
listView = null;
}
}
}
The GetEvents() is a AsyncTask (a class inside the viewgroup class) that gets some events from a local database, the code is the onPostExecute is as follows
protected void onPostExecute(String errMsg)
{
if (errMsg != null)
{
listView.setAdapter(new SkemaHoursRowAdapter(getContext(), eventItems));
listView.requestLayout();
addView(listView, layoutParams);
}
}
eventItems is an array i parse to my custom rowadapter (which i know works). The view group is displayed but the listView is not.
Any suggestions??
I ended up not using a viewgroup instead i made a loader-listadapter which i display if the correct list has not been updated yet.
So instead of extending ViewGroup it extends ListView and it's adapter get set at at first to the load-adapter and when loaded it sets it to the correct listview.

How to apply android layout animation only to children above a certain index?

I have a ListView containing a series of notes.
Currently I use a layout animation to slide all the notes in from the side when the list first loads; this works perfectly.
However, I'm trying to figure out how to apply a layout animation only to list items below a certain point. Say I delete an item on the list: I'd like all items below it to shift up into the deleted note's old spot.
I've tried finding a way to customize the animation delays or interpolators by child index but haven't found anything appropriate for this location. Is there a way to do this using a custom layout animation (such as extending LayoutAnimationController) or would I have to do this low level and animate each view individually?
Any suggestions?
Create your animation and call it in your list OnItemClickListener. After that you might use the adapter's notifyDataSetChanged to refresh the list content.
In this example, I created a method called removeListItem with receives the row you want to remove and the position of that row in you list content array.
public class MainActivity extends ListActivity implements OnItemClickListener{
ArrayList<String> values;
ArrayAdapter<String> adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
values = generateMockData(50);
adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_list_item_1, values);
setContentView(R.layout.activity_main);
getListView().setAdapter(adapter);
getListView().setOnItemClickListener(this);
}
private ArrayList<String> generateMockData(int number) {
ArrayList<String> result = new ArrayList<String>();
for(int i = 0; i < number; i++)
result.add(""+i+" "+ (int)Math.random() * 13);
return result;
}
private void removeListItem(View rowView, final int positon) {
Animation anim = AnimationUtils.loadAnimation(this,
android.R.anim.slide_out_right);
anim.setDuration(500);
rowView.startAnimation(anim);
new Handler().postDelayed(new Runnable() {
public void run() {
values.remove(positon);//remove the current content from the array
adapter.notifyDataSetChanged();//refresh you list
}
}, anim.getDuration());
}
public void onItemClick(AdapterView<?> arg0, View row, int position, long arg3) {
if(position == YOUR_INDEX) //apply your conditions here!
removeListItem(row,position);
}
I had a very similar problem and was able to find a solution with a simple View subclass that allows you to use a layout animation controller to animate only the views that you specify. Please see this link:
Can LayoutAnimationController animate only specified Views
In your layout xml, try adding:
<ListView
android:id="#android:id/list"
...
android:animateLayoutChanges="true"/>
This will automatically animate insertions and deletions from your listview.
If you have a custom animation, named anim_translate_left, use instead:
<ListView
android:id="#android:id/list"
...
android:animateLayoutChanges="#anim/anim_translate_left"/>
Source: Google API's

Categories

Resources