Modifying ListView's smoothScrollToPosition transition

645 Views Asked by At

The default behavior for a ListView when calling smoothScrollToPosition on it, it to move with linear speed to the specified position.

Digging into ListView's and AbsListView's code, I can see that this behavior takes place because AbsListView uses a PositionScroller object (implementing AbsPositionScroller) that in turn uses a FlingRunnable object on which the method startScroll gets called with linear = true (which ends up having its OverScroller object use a LinearInterpolator).

I want to modify this behavior, and have it use for example the Scroller.ViscousFluidInterpolator class that the OverScroller class would use by default, but I'm not finding a way to do it.

I see that AbsListView defines a AbsPosScroller interface (that himself implements with a PositionScroller class), that I could try to implement with my own class to have it end up using the ViscousFluidInterpolator, but for some reason this interface is private to the package android.widget...

Am I missing something, or does it look like this has been written in a way that prevents it to have a behavior like that one be customized? Why would they bother writing up a AbsPosScroller interface in first place?

Any leads on how could I get the behavior I want without having to write my entire ListView class from scratch?

1

There are 1 best solutions below

0
On BEST ANSWER

While I still don't know why would they write these components in a way that their behavior can't be customized easily when it would've been pretty easy to do it, I came up with an alternative implementation of smoothScrollToPosition (awesomeScrollToPosition in the code below) that does what I needed.

This solution makes use of an OverScroller object (that internally uses the ViscousInterpolator unless a different one is specified) to provide the effect I was looking for, for scrolling to elements within the visible page (the solution to achieve scrolling across pages is more convoluted, but this works for the problem I needed to solve).

I basically implemented a Runnable class private to my own ListView subclass (MyListView) that deals with the scrolling animation, re-posting itself to the UI thread for as long as the animation needs to run, using scrollingListBy in every frame (this method is only available since KitKat [19] though).

public class MyListView extends ListView {

    private MyScroller mScroller;

    /* MyListView constructors here */

    public void awesomeScrollToPosition(int position, int duration) {

        if (getChildCount() == 0) {
            // Can't scroll without children (visible list items)
            return;
        }

        if (mScroller == null) {
            mScroller = new MyScroller();
        }

        if (mScroller.isRunning()) {
            mScroller.stop();
        }

        int firstPos = getFirstVisiblePosition();
        int lastPos = getLastVisiblePosition();

        if (!(firstPos <= position && position <= lastPos)) {
            // Can't scroll to an item outside of the visible range this easily
            return;
        }

        int targetPosition = position - firstPos;
        int targetTop = getChildAt(targetPosition).getTop();

        mScroller.start(targetTop, duration);
    }

    private class MyScroller implements Runnable {

        OverScroller mScroller;

        boolean mRunning;
        int mLastY;

        MyScroller() {
            mScroller = new OverScroller(getContext());
            mRunning = false;
        }

        void start(int y, int duration) {

            // start scrolling
            mLastY = 0;
            mScroller.startScroll(0, 0, 0, y, duration);

            mRunning = true;
            postOnAnimation(this);
        }

        boolean isRunning() {
           return mRunning;
        }

        @Override
        public void run() {

            boolean more = mScroller.computeScrollOffset();
            final int currentY = mScroller.getCurrY();

            // actual scrolling
            scrollListBy(currentY - mLastY);

            if (more) {
                mLastY = currentY;

                // schedule next run
                postOnAnimation(this);
            } else {
                stop();
            }
        }

        public void stop() {

            mRunning = false;
            removeCallbacks(this);
        }
    }
}