57

I'm trying to smoothly scroll to last element of a list after adding an element to the arrayadapter associated with the listview. The problem is that it just scrolls to a random position

arrayadapter.add(item);
//DOES NOT WORK CORRECTLY:
listview.smoothScrollToPosition(arrayadapter.getCount()-1);

//WORKS JUST FINE:
listview.setSelection(arrayadapter.getCount()-1);
user1515520
  • 624
  • 1
  • 6
  • 10

9 Answers9

65

You probably want to tell the ListView to post the scroll when the UI thread can handle it (which is why yours it not scrolling properly). SmoothScroll needs to do a lot of work, as opposed to just go to a position ignoring velocity/time/etc. (required for an "animation").

Therefore you should do something like:

    getListView().post(new Runnable() {
        @Override
        public void run() {
            getListView().smoothScrollToPosition(pos);
        }
    });
Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • 13
    +1 for the idea of `post`. However, I stumbled upon cases where `smoothScrollToPosition` still does not work, leaving a small offset space between the top of the widget and the item in the position to scroll to. In these cases, `setSelection` (combined with `post` of course) works flawlessly. – Giulio Piancastelli Oct 09 '13 at 13:39
  • Yeah, me too. ListViews are way inferior to UITableView and such. It's sad, because Lists are a core concept in mobile where the screen size is compromised so we have to scroll stuff. In any case, just so you know, scrolling works, you just have to understand the rules, the layout must have been laid, you must do it in the UI thread, you must post it so the list can queue the operation, etc. A lot of magic must happen for the scroll to occur. I'm sure @RomanianGuy can throw more light into this. Still the iSO implementation works flawlessly for the most part. – Martin Marconcini Oct 09 '13 at 18:59
  • 5
    The name of the guy is @RomainGuy BTW. – Giulio Piancastelli Oct 10 '13 at 07:37
  • Ah @GiulioPiancastelli thanks for that :) A typo, but can't edit it now. – Martin Marconcini Oct 10 '13 at 18:40
  • 1
    Here's a more complete workaround: https://stackoverflow.com/questions/14479078/smoothscrolltopositionfromtop-is-not-always-working-like-it-should/20997828#20997828 – Lars Blumberg Aug 13 '14 at 12:48
  • This is the correct answer :) Makes sense to make the view itself post it in the queue. – The Nomad Jun 05 '17 at 22:06
15

(Copied from my answer: smoothScrollToPositionFromTop() is not always working like it should)

This is a known bug. See https://code.google.com/p/android/issues/detail?id=36062

However, I implemented this workaround that deals with all edge cases that might occur:

First call smothScrollToPositionFromTop(position) and then, when scrolling has finished, call setSelection(position). The latter call corrects the incomplete scrolling by jumping directly to the desired position. Doing so the user still has the impression that it is being animation-scrolled to this position.

I implemented this workaround within two helper methods:

smoothScrollToPosition()

public static void smoothScrollToPosition(final AbsListView view, final int position) {
    View child = getChildAtPosition(view, position);
    // There's no need to scroll if child is already at top or view is already scrolled to its end
    if ((child != null) && ((child.getTop() == 0) || ((child.getTop() > 0) && !view.canScrollVertically(1)))) {
        return;
    }

    view.setOnScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(final AbsListView view, final int scrollState) {
            if (scrollState == SCROLL_STATE_IDLE) {
                view.setOnScrollListener(null);

                // Fix for scrolling bug
                new Handler().post(new Runnable() {
                    @Override
                    public void run() {
                        view.setSelection(position);
                    }
                });
            }
        }

        @Override
        public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
                                 final int totalItemCount) { }
    });

    // Perform scrolling to position
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            view.smoothScrollToPositionFromTop(position, 0);
        }
    });
}

getChildAtPosition()

public static View getChildAtPosition(final AdapterView view, final int position) {
    final int index = position - view.getFirstVisiblePosition();
    if ((index >= 0) && (index < view.getChildCount())) {
        return view.getChildAt(index);
    } else {
        return null;
    }
}
Lars Blumberg
  • 19,326
  • 11
  • 90
  • 127
7

You should use setSelection() method.

TaoZang
  • 1,690
  • 2
  • 15
  • 15
  • 2
    It's not necessarily the same, you might not want the item to be selected, or you can simply be looking for the smooth effect – mdelolmo Nov 06 '12 at 11:27
  • 1
    It actually helped me out, instead of nothing happening it still gets scrolled to the very end of the list. – Slickelito Dec 05 '12 at 14:57
  • 2
    `setSelection` indeed works where `smoothScrollToPosition` does not, but it would be helpful if you can enlight us on why it is so. – Giulio Piancastelli Oct 09 '13 at 13:41
  • 2
    @GiulioPiancastelli smoothScrollToPosition doesn't work because it's supposed to scroll by a small amount towards the desired position. That means that it won't work for long lists or even for items that are far by more than 100dp or so. The documentation isn't clear about it but that's how it actually works. You can probably find out more by checking the actual implementation code if you're interested. – Valerio Santinelli Oct 16 '13 at 11:16
5

Use LayoutManager to smooth scroll

layoutManager.smoothScrollToPosition(recyclerView, new RecyclerView.State(), position);
Siddhesh Shirodkar
  • 891
  • 13
  • 16
4
final Handler handler = new Handler();
//100ms wait to scroll to item after applying changes
handler.postDelayed(new Runnable() {
@Override
public void run() {
   listView.smoothScrollToPosition(selectedPosition);
}}, 100);
VSB
  • 9,825
  • 16
  • 72
  • 145
  • He waits 100 millis to let recyclerview finish its calculations then do the smooth scroll. you can also write `recyclerView.postDelayed({ recyclerView.smoothScrollToPosition(0) }, 300)` – Amr Nov 05 '21 at 09:45
3

The set selection method mentioned by Lars works, but the animation was too jumpy for our purposes as it skips whatever was left. Another solution is to recall the method repeatedly until the first visible position is your index. This is best done quickly and with a limit as it will fight the user scrolling the view otherwise.

private  void DeterminedScrollTo(Android.Widget.ListView listView, int index, int attempts = 0) {
    if (listView.FirstVisiblePosition != index && attempts < 10) {
        attempts++;
        listView.SmoothScrollToPositionFromTop (index, 1, 100);
        listView.PostDelayed (() => {
            DeterminedScrollTo (listView, index, attempts);
        }, 100);
    }
}

Solution is in C# via. Xamarin but should translate easily to Java.

Daniel Roberts
  • 534
  • 5
  • 8
1

Do you call arrayadapter.notifyDataSetChanged() after you called arrayadapter.add()? Also to be sure, smoothScrollToPosition and setSelection are methods available in ListView not arrayadapter as you have mentioned above.

In any case see if this helps: smoothScrollToPosition after notifyDataSetChanged not working in android

Community
  • 1
  • 1
Code Poet
  • 11,227
  • 19
  • 64
  • 97
  • yea.. I made a mistake here in the forum, it's of course listview.smoothScrollToPosition(pos) and listview.setSelection(pos). and notifyDataSetChanged gets called automatically when calling arrayadapter.add().., otherwise setSelection wouldn't work – user1515520 Jul 11 '12 at 12:34
  • 1
    it's just weird that setSelection(pos) works perfectly, but smoothScrollToPosition(pos) doesn't, isn't it? – user1515520 Jul 11 '12 at 12:41
1

Solution in kotlin:

fun AbsListView.scrollToIndex(index: Int, duration: Int = 150) {
    smoothScrollToPositionFromTop(index, 0, duration)
    postDelayed({
        setSelection(index)
        post { smoothScrollToPositionFromTop(index, 0, duration) }
    }, duration.toLong())
}

PS: Looks like its quite messed up on Android SDK side so this is kind of best you can get, if you don't want to calculate your view offset manually. Maybe best easy way is to set duration to 0 for long list to avoid any visible jump.

I had some issues when calling just setSelection in some positions in GridView so this really seems to me as solution instead of using that.

Renetik
  • 5,887
  • 1
  • 47
  • 66
0

use this in android java, it work for me:

private void DeterminedScrollTo(int index, int attempts) {
        if (listView.getFirstVisiblePosition() != index && attempts < 10) {
            attempts++;
            if (listView.canScrollVertically(pos))
                listView.smoothScrollToPositionFromTop(index, 1, 200);
            int finalAttempts = attempts;
            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                @Override
                public void run() {
                    DeterminedScrollTo(index, finalAttempts);
                }
            }, 200);
        }
    }