12

I'm a newbie in Android development, and I would just like to know a little bit about the Scroller widget (android.widget.Scroller). How does it animate the view? Can the Animation object, if it exists, be accessed? If so, how? I've read the source code, but could find no clues, or maybe I'm too new?

I just wanted to do some operations after a Scroller finishes scrolling, something like

m_scroller.getAnimation().setAnimationListener(...);
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
Kenn Cal
  • 3,659
  • 2
  • 17
  • 18

4 Answers4

40

The Scroller widget doesn't actually do much of the work at all for you. It doesn't fire any callbacks, it doesn't animate anything, it just responds to various method calls.

So what good is it? Well, it does all of the calculation for e.g. a fling for you, which is handy. So what you'd generally do is create a Runnable that repeatedly asks the Scroller, "What should my scroll position be now? Are we done flinging yet?" Then you repost that runnable on a Handler (usually on the View) until the fling is done.

Here's an example from a Fragment I'm working on right now:

private class Flinger implements Runnable {
    private final Scroller scroller;

    private int lastX = 0;

    Flinger() {
        scroller = new Scroller(getActivity());
    }

    void start(int initialVelocity) {
        int initialX = scrollingView.getScrollX();
        int maxX = Integer.MAX_VALUE; // or some appropriate max value in your code
        scroller.fling(initialX, 0, initialVelocity, 0, 0, maxX, 0, 10);
        Log.i(TAG, "starting fling at " + initialX + ", velocity is " + initialVelocity + "");

        lastX = initialX;
        getView().post(this);
    }

    public void run() {
        if (scroller.isFinished()) {
            Log.i(TAG, "scroller is finished, done with fling");
            return;
        }

        boolean more = scroller.computeScrollOffset();
        int x = scroller.getCurrX();
        int diff = lastX - x;
        if (diff != 0) {
            scrollingView.scrollBy(diff, 0);
            lastX = x;
        }

        if (more) {
            getView().post(this);
        }
    }

    boolean isFlinging() {
        return !scroller.isFinished();
    }

    void forceFinished() {
        if (!scroller.isFinished()) {
            scroller.forceFinished(true);
        }
    }
}

The details of using Scroller.startScroll should be similar.

Bill Phillips
  • 7,687
  • 1
  • 25
  • 13
  • I see. So you implement a sort of listener thread for the Scroller. Nice. Thank you! :) – Kenn Cal Jun 07 '11 at 00:20
  • 1
    Shouldn't your `isFlinging()` method return `!scroller.isFinished()`? Either that or it should be renamed `isFinished()`. Great example though :) – ashughes Nov 19 '11 at 01:20
  • Yes it should. Fixed. Thanks! – Bill Phillips Feb 07 '12 at 03:50
  • Hopefully someone can explain this to me. I don't see where it will repeat to keep moving the view x amount every t amount of time – Lpc_dark Mar 17 '13 at 23:30
  • The Scroller object does not do anything to repeat. In this code, the "repeat" happens where you see "if (more) { getView().post(this); }". (Check out the documentation for View.post(Runnable) for more info. Docs here are helpful, too: http://developer.android.com/reference/android/os/Handler.html) – Bill Phillips Mar 18 '13 at 10:30
  • which view does the getView() refers to? – Deepak Senapati Oct 08 '13 at 08:29
  • It doesn't matter. View.post(Runnable) is a convenience shortcut for posting things to the UI thread. – Bill Phillips Oct 08 '13 at 22:38
  • 2
    Almost two years after I asked this question, I figure you could also make a subclass that takes a delegate and periodically calls certain methods on that delegate, i.e. onScrollFinished(), or onOverscroll(), etc. This would be better IMO than polling the scroller all the time. @BillPhillips you really helped me on this one though. :) – Kenn Cal Dec 09 '13 at 06:35
4

like Bill Phillips said, Scroller is just an Android SDK class helping with calculating scrolling positions. I have a full working example here:

public class SimpleScrollableView extends TextView {
    private Scroller mScrollEventChecker;

    private int mLastFlingY;
    private float mLastY;
    private float mDeltaY;

    public SimpleScrollableView(Context context) {
        this(context, null, 0);
    }

    public SimpleScrollableView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SimpleScrollableView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mScrollEventChecker != null && !mScrollEventChecker.isFinished()) {
            return super.onTouchEvent(event);
        }

        final int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = event.getY();
                return true;

            case MotionEvent.ACTION_MOVE:
                int movingDelta = (int) (event.getY() - mLastY);
                mDeltaY += movingDelta;
                offsetTopAndBottom(movingDelta);
                invalidate();
                return true;

            case MotionEvent.ACTION_UP:
                mScrollEventChecker = new Scroller(getContext());
                mScrollEventChecker.startScroll(0, 0, 0, (int) -mDeltaY, 1000);
                post(new Runnable() {
                    @Override
                    public void run() {
                        if (mScrollEventChecker.computeScrollOffset()) {
                            int curY = mScrollEventChecker.getCurrY();
                            int delta = curY - mLastFlingY;
                            offsetTopAndBottom(delta); // this is the method make this view move
                            invalidate();
                            mLastFlingY = curY;
                            post(this);
                        } else {
                            mLastFlingY = 0;
                            mDeltaY = 0;
                        }
                    }
                });
                return super.onTouchEvent(event);
        }

        return super.onTouchEvent(event);
    }
}

The demo custom view above will scroll back to original position after the user release the view. When user release the view, then startScroll() method is invoked and we can know what the distance value should be for every single message post.

Full working example: Github repository

shanwu
  • 1,493
  • 6
  • 35
  • 45
2

We can extend the Scroller class then intercept corresponding animation start methods to mark that was started, after computeScrollOffset() return false which means animation finished's value, we inform by a Listener to caller :

public class ScrollerImpl extends Scroller {
    ...Constructor...

    private boolean mIsStarted;
    private OnFinishListener mOnFinishListener;

    @Override
    public boolean computeScrollOffset() {
        boolean result = super.computeScrollOffset();
        if (!result && mIsStarted) {
            try { // Don't let any exception impact the scroll animation.
                mOnFinishListener.onFinish();
            } catch (Exception e) {}
            mIsStarted = false;
        }
        return result;
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy) {
        super.startScroll(startX, startY, dx, dy);
        mIsStarted = true;
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        super.startScroll(startX, startY, dx, dy, duration);
        mIsStarted = true;
    }

    @Override
    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
        super.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
        mIsStarted = true;
    }

    public void setOnFinishListener(OnFinishListener onFinishListener) {
        mOnFinishListener = onFinishListener;
    }

    public static interface OnFinishListener {
        void onFinish();
    }
}
VinceStyling
  • 3,707
  • 3
  • 29
  • 44
  • The previous implementation uses a thread to poll the Scroller about scroll position. In your implementation where you extend Scroller, how do you poll the ScrollerImpl from your View to get the changes in the scroll position? – alexismorin Oct 26 '14 at 05:40
  • forgive me, i'm not understand your question very clearly, does you want to know how to use the `ScrollerImpl`? – VinceStyling Oct 26 '14 at 06:22
  • @carignan.boy You don't poll in this case. What you do is to set the ScrollerImpl OnFinishListener and then wait for the onFinish() method to be called. It's a lot better to do this instead of polling; this is exactly what I meant in the comment I wrote on the accepted answer. – Kenn Cal Oct 08 '15 at 09:08
2

Great answer above. Scroller#startScroll(...) indeed works the same way.

For example, the source for a custom scrolling TextView at: http://bear-polka.blogspot.com/2009/01/scrolltextview-scrolling-textview-for.html

Sets a Scroller on a TextView using TextView#setScroller(Scroller).

The source for the SDK's TextView at: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/widget/TextView.java#TextView.0mScroller

Shows that TextView#setScroller(Scroller) sets a class field which is used in situations like bringPointIntoView(int) where Scroller#scrollTo(int, int, int, int) is called.

bringPointIntoView() adjusts mScrollX and mScrollY (with some SDK fragmentation code), then calls invalidate(). The point of all this is that mScrollX and mScrollY are used in methods like onPreDraw(...) to affect the position of the drawn contents of the view.

Tenacious
  • 594
  • 4
  • 6