I'm building an interface similar to the Google Hangouts chat interface. New messages are added to the bottom of the list. Scrolling up to the top of the list will trigger a load of previous message history. When the history comes in from the network, those messages are added to the top of the list and should not trigger any kind of scroll from the position the user had stopped when the load was triggered. In other words, a "loading indicator" is shown at the top of the list:
Which is then replaced in-situ with any loaded history.
I have all of this working... except one thing that I've had to resort to reflection to accomplish. There are plenty of questions and answers involving merely saving and restoring a scroll position when adding items to the adapter attached to a ListView. My problem is that when I do something like the following (simplified but should be self-explanatory):
public void addNewItems(List<Item> items) { final int positionToSave = listView.getFirstVisiblePosition(); adapter.addAll(items); listView.post(new Runnable() { @Override public void run() { listView.setSelection(positionToSave); } }); }
Then what the user will see is a quick flash to the top of the ListView, then a quick flash back to the right location. The problem is fairly obvious and discovered by many people: setSelection()
is unhappy until after notifyDataSetChanged()
and a redraw of ListView
. So we have to post()
to the view to give it a chance to draw. But that looks terrible.
I've "fixed" it by using reflection. I hate it. At its core, what I want to accomplish is reset the first position of the ListView
without going through the rigamarole of the draw cycle until after I've set the position. To do that, there's a helpful field of ListView: mFirstPosition
. By gawd, that's exactly what I need to adjust! Unfortunately, it's package-private. Also unfortunately, there doesn't appear to be any way to set it programmatically or influence it in any way that doesn't involve an invalidate cycle... yielding the ugly behavior.
So, reflection with a fallback on failure:
try { Field field = AdapterView.class.getDeclaredField("mFirstPosition"); field.setAccessible(true); field.setInt(listView, positionToSave); } catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme> e.printStackTrace(); listView.post(new Runnable() { @Override public void run() { listView.setSelection(positionToSave); } }); } }
Does it work? Yes. Is it hideous? Yes. Will it work in the future? Who knows? Is there a better way? That's my question.
How do I accomplish this without reflection?
An answer might be "write your own ListView
that can handle this." I'll merely ask whether you've seen the code for ListView
.
EDIT: Working solution with no reflection based on Luksprog's comment/answer.
Luksprog recommended an OnPreDrawListener()
. Fascinating! I've messed with ViewTreeObservers before, but never one of these. After some messing around, the following type of thing appears to work quite perfectly.
public void addNewItems(List<Item> items) { final int positionToSave = listView.getFirstVisiblePosition(); adapter.addAll(items); listView.post(new Runnable() { @Override public void run() { listView.setSelection(positionToSave); } }); listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { @Override public boolean onPreDraw() { if(listView.getFirstVisiblePosition() == positionToSave) { listView.getViewTreeObserver().removeOnPreDrawListener(this); return true; } else { return false; } } }); }
Very cool.
Just call stopScroll(myListView); when you need to stop scroll. Show activity on this post. // Stop scrolling smoothScrollBy(0, 0);
ListView itself is scrollable.
Android ListView is a view which groups several items and display them in vertical scrollable list. The list items are automatically inserted to the list using an Adapter that pulls content from a source such as an array or database.
As I said in my comment, a OnPreDrawlistener
could be another option to solve the problem. The idea of using the listener is to skip showing the ListView
between the two states(after adding the data and after setting the selection to the right position). In the OnPreDrawListener
(set with listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);
) you'll check the current visible position of the ListView
and test it against the position which the ListView
should show. If those don't match then make the listener's method return false
to skip the frame and set the selection on the ListView
to the right position. Setting the proper selection will trigger the draw listener again, this time the positions will match, in which case you'd unregister the OnPreDrawlistener
and return true
.
I was breaking up my head until I found a solution similar to this. Before adding a set of items you have to save top distance of the firstVisible item and after adding the items do setSelectionFromTop().
Here is the code:
// save index and top position int index = mList.getFirstVisiblePosition(); View v = mList.getChildAt(0); int top = (v == null) ? 0 : v.getTop(); // for (Item item : items){ mListAdapter.add(item); } // restore index and top position mList.setSelectionFromTop(index, top);
It works without any jump for me with a list of about 500 items :)
I took this code from this SO post: Retaining position in ListView after calling notifyDataSetChanged
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With