18

I am building a chat-like Android application, similar to Hangouts. For this purpose I am using a vertical ListView with stackFromBottom=true and transcriptMode="normal".

The list is sorted from older messages (top) to younger messages (bottom). In normal state, the ListView is scrolled to the bottom, showing the youngest message.

The ListView uses a reverse endless-scroll adapter to load more (older) messages once the user scrolls to the top of the list. Once older messages are loaded, they are added to the top of the list.

The desired behavior is that the ListView maintains its scroll-position to the bottom of the list, when older messages are added to the top of it. That is, when older messages are added to the top, the scroll-view should show the same messages that were shown before adding the older messages.

Unfortunately, this does not work. Instead of maintaining the scroll-position to the bottom, the ListView maintains the scroll-position to the top of the list. That is, after adding older messages to the list, the list shows the top-most (i.e oldest) messages.

Is there an easy way to tell the ListView to maintain its scroll-position to the bottom, instead of the top, when adding new items to the top of it?

Update

For demonstration purposes, I have minimized the use-case and code as much as possible. The following example creates a simple string array list. Clicking on an item will add another item at the top of the list. Long-Clicking on an item will add another item at the bottom of the list.

Everything works fine when adding items at the bottom of the list (long-click). When adding items at the top of the list (simple click), then there are 3 cases:

  • If the list was scrolled to the bottom, then everything works fine. After adding the item to the top, the list is still scrolled to the bottom.
  • If the list is not scrolled to the bottom (nor to the top), then the list will maintain its scroll-y position (from top). This makes the list appear to "jump" one item up. I would like the list not to "jump up" in this case, i.e I would like it to maintain its scroll-y position from the bottom.
  • If the list is scrolled to the top, then the same happens as in case 2. The preferred behavior is the same as case 2.

(The last 2 cases are in fact the same. I separated them because the problem can be better demonstrated in case 2.)

Code

public class MyListActivity extends Activity {
  int topIndex = 1;

  int bottomIndex = 20;

  protected final void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.my_list_activity);

    final ListView list = (ListView) findViewById(R.id.list);
    list.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
    list.setStackFromBottom(true);

    final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
    for (int i = topIndex; i <= bottomIndex; i++) {
      adapter.add(Integer.toString(i));
    }
    list.setAdapter(adapter);

    list.setOnItemClickListener(new OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
        adapter.insert(Integer.toString(--topIndex), 0);
      }
    });

    list.setOnItemLongClickListener(new OnItemLongClickListener() {
      @Override
      public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
        adapter.add(Integer.toString(++bottomIndex));
        return true;
      }
    });
  }
}
Fatih Coşkun
  • 243
  • 2
  • 8
  • I believe you mean android:stackFromBottom=true – Submersed Feb 26 '14 at 21:17
  • Yes, I do. Corrected, thanks. – Fatih Coşkun Feb 26 '14 at 23:48
  • Have you figured out a solution for this? Struggling with this as well. – Leo Jul 02 '14 at 21:43
  • From a cursory look in the source, this seems to be a deliberate design choice in `AbsListView` (as opposed to the base `AdapterView`). It synchronizes the scroll state based on the _position_ of the first/last item instead of the _id_. However, if it has an item selected when the dataset change is reported, then it does synchronize the scroll position of the selected item itself based on the id (regardless of whether the `ListAdapter` has stable ids or not). – corsair992 Jul 09 '14 at 00:01
  • Hm, so then maybe I need to set an item as selected before adding more items? – Leo Jul 10 '14 at 19:24
  • @FatihCoşkun have you figured out any solution? – Leo Jan 13 '15 at 00:06

4 Answers4

12

I have figured this out. Wherever it is that you are getting new data I call the following before changing data in the adapter:

int firstVisibleItem = list.getFirstVisiblePosition();
int oldCount = adapter.getCount();
View view = list.getChildAt(0);
int pos = (view == null ? 0 :  view.getBottom());

Then I call:

adapter.setData(data);

list.setSelectionFromTop(firstVisibleItem + adapter.getCount() - oldCount + 1, pos);
Leo
  • 4,652
  • 6
  • 32
  • 42
3

First get the firstVisiblePosition (or the Last one since its upside down. Yo got to try and check it yourself) and then get the top of the View which is added to the List and then using setSe;ectionFromTop() method, you can scroll to the desired location.

Code:

int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0); //or upside down (list.size - 1)
int top = (v == null) ? 0 : v.getTop(); //returns the top of the view its Y co-ordinates. [Doc][1]
mList.setSelectionFromTop(index, top);

Source.

Community
  • 1
  • 1
Atul O Holic
  • 6,692
  • 4
  • 39
  • 74
  • I have also been fiddling around with some custom code. Your code shows the general idea, but in my case the code looks much more complicated (since items are added to the top of the list). I found the code quite complicated and boilerplate and hoped that there might be a simpler way. Well, this is Android... so I should have known better. – Fatih Coşkun Feb 26 '14 at 20:47
  • 1
    This answer is correct in it's approach, but the provided code does not accomplish anything, as it synchronizes the scroll state based on the _top-based_ position of the visible item instead of the id or bottom-based position, which is what the `ListView` itself already does. – corsair992 Jul 09 '14 at 02:20
0

Edit to account for additional information:

I think I've found your issue. I replicated your described use cases by using a standalone list as the only view in XML:

<ListView
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
</ListView>

I fixed the problem to follow your desired use cases by nesting the layout in a parent layout:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </ListView>

</FrameLayout>
Submersed
  • 8,810
  • 2
  • 30
  • 38
  • I am setting the adapter to the list once when my activity is initialized. After that, new data is added to the existing adapter followed by notifyDataChanged(). As I said, the list already does maintain scroll-position. However it does so from the top. I need it to maintain the scroll-position from the bottom. – Fatih Coşkun Feb 26 '14 at 20:44
  • Not 100% clear on the difference here. Since in your question you stated - "That is, when older messages are added to the top, the scroll-view should show the same messages that were shown before adding the older messages"; if it maintains scroll position, it maintains the current scroll position - period. If the scroll position doesn't change, there isn't really a difference between maintaining it from a "top position" vs a "bottom position". – Submersed Feb 26 '14 at 21:24
  • Note that the following is derived from my observations. When adding new items to the ListView, it maintains the scroll-y position. This means that after adding items, it will be scrolled down the exact same amount as it was previously. This works great when adding items to the bottom of the list. That is, the same items are visible before and after adding new items (to the bottom). However, since I am adding items to the top of the list the maintained scroll-y leads different items to be shown after adding items (to the top). – Fatih Coşkun Feb 26 '14 at 21:52
  • I've tested the two use cases, i.e. addToTop being adapter.insert(item, 0); and addToBottom being adapter.add(item);, and that's not the case in my test project. Also, I inflated the ListView with android:stackFromBottom="true" and android:transcriptMode="normal", I get your intended functionality - so without seeing the source, it's hard to say what the problem is. – Submersed Feb 26 '14 at 22:31
  • I will try to copy the important parts of my code tomorrow. One difference to your code is that I am not using an ArrayAdapter. I am using a custom subclass of BaseAdapter. Hence I don't have the methods adapter.insert(item,0) or adapter.add(item). I will look up the implementation of those methods, but I don't think that they do some magic here. – Fatih Coşkun Feb 26 '14 at 23:52
  • I have updated the question with a minimized code, which demonstrates the problem. – Fatih Coşkun Feb 27 '14 at 10:00
  • I tested your solution and it does not work for me. When testing, please be careful to not be scrolled to the bottom when adding new items to the top. Because in that case, it will work. – Fatih Coşkun Feb 27 '14 at 15:59
  • I tested all 3 use cases as described, and it works for me on my Nexus 7 and Nexus 4. – Submersed Feb 27 '14 at 16:10
  • That's strange, I am also testing on a Nexus 4. In case 2, after adding an item to the top (simple-click), your view shows the same items as before the click? – Fatih Coşkun Feb 28 '14 at 08:15
  • I have minSdkVersion="8" targetSdkVersion="19", what is your setup? – Fatih Coşkun Feb 28 '14 at 09:34
-2

after you insert the record in your adapter you can try to use

   yourListView.setSelection(yourAdapter.getCount() - 1);
Duran Jayson
  • 327
  • 3
  • 6