欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 源码解析 - Scroller

程序员文章站 2024-01-28 15:57:52
...

分析版本 : Android API 26

介绍

Android开发中,如果我们希望使一个View滑动的话,除了使用属性动画外。
我们还可以使用系统提供给我们的两个类Scroller和OverScroller用来实现弹性滑动。下面分析一下Scroller的使用方法以及实现方式。

View中的scrollBy()和scrollTo()方法介绍

/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
 */
public void scrollTo(int x, int y) {
    //判断当前的偏移量是否等于之前的偏移量
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        //回调onScrollChanged 方法
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }

scrollTo()是指将前视图内容横向偏移x距离(x > 0 向左移动,否则反之),纵向偏移y距离(y > 0 向上移动,否则反之)。移动的是View 的内容, 不是View 本身。

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy()方法里直接调用了scrollTo()方法,表示在当前偏移量的基础上继续偏移(x,y)

自定义可以滚动的 ScrollTextView

/**
 * Created by Owen Chan
 * On 2018-01-12.
 */

public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {

    private Scroller mScroller;

    private int mLeft = 0;
    private int mTop = 0;

    public ScrollTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    public void startScrollerScroll() {
        mScroller.startScroll(mLeft, mTop, 0, -160, 10000);
        invalidate();
    }

    public void startScrollerFling() {
        mScroller.fling(mLeft, mTop, 0, -5000, mLeft, mLeft, 200, 12000);
    }
}

通过调用startScrollerScroll()与startScrollerFling() 方法我们发现View 中的内容位置发生了变化,首先我们看看流程图 怎么触发View 重新绘制的

Android 源码解析 - Scroller

Scroller类只负责计算,它并不负责操作View的滚动,调用了ScrollTextView 的startScrollerScroll()方法后调用了invalidate()方法。invalidate()方法导致View重新绘制,因此会调用View的draw()方法,在View的draw()方法中又会去调用computeScroll()方法,computeScroll()方法在View中是一个空实现,在ScrollTextView 中我们实现了computeScroll()方法。在上面的computeScroll()方法中,我们调用了mScroller.computeScrollOffset()方法来计算当前滑动的偏移量。如果还在滑动过程中就会返回true。所以我们就能在if中通过Scroller拿到当前的滑动坐标从而做任何我们想做的处理。从而形成了滑动动画。
下面我们解释一下Scroller的两个方法的具体作用:

/**
 * Start scrolling by providing a starting point, the distance to travel,
 * and the duration of the scroll.
 * 
 * @param startX Starting horizontal scroll offset in pixels. Positive
 *        numbers will scroll the content to the left.
 * @param startY Starting vertical scroll offset in pixels. Positive numbers
 *        will scroll the content up.
 * @param dx Horizontal distance to travel. Positive numbers will scroll the
 *        content to the left.
 * @param dy Vertical distance to travel. Positive numbers will scroll the
 *        content up.
 * @param duration Duration of the scroll in milliseconds.
 */
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

mStartX:起始滑动点的X坐标
startY:起始滑动点Y的坐标
dx:滑动的水平偏移量, dx > 0 向左滑动, 否则 向右滑动
dy:滑动的垂直偏移量,dy>0 向上滑动,否则向下滑动
duration:滑动的执行时间


/**
 * Start scrolling based on a fling gesture. The distance travelled will
 * depend on the initial velocity of the fling.
 * 
 * @param startX Starting point of the scroll (X)
 * @param startY Starting point of the scroll (Y)
 * @param velocityX Initial velocity of the fling (X) measured in pixels per
 *        second.
 * @param velocityY Initial velocity of the fling (Y) measured in pixels per
 *        second
 * @param minX Minimum X value. The scroller will not scroll past this
 *        point.
 * @param maxX Maximum X value. The scroller will not scroll past this
 *        point.
 * @param minY Minimum Y value. The scroller will not scroll past this
 *        point.
 * @param maxY Maximum Y value. The scroller will not scroll past this
 *        point.
 */
public void fling(int startX, int startY, int velocityX, int velocityY,
        int minX, int maxX, int minY, int maxY) {
    // Continue a scroll or fling in progress
    if (mFlywheel && !mFinished) {
        float oldVel = getCurrVelocity();

        float dx = (float) (mFinalX - mStartX);
        float dy = (float) (mFinalY - mStartY);
        float hyp = (float) Math.hypot(dx, dy);

        float ndx = dx / hyp;
        float ndy = dy / hyp;

        float oldVelocityX = ndx * oldVel;
        float oldVelocityY = ndy * oldVel;
        if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                Math.signum(velocityY) == Math.signum(oldVelocityY)) {
            velocityX += oldVelocityX;
            velocityY += oldVelocityY;
        }
    }

startX 起始滑动点的X坐标

startY 起始滑动点的Y坐标

velocityX X方向上的加速度

velocityY Y方向上的加速度

minX X方向上滑动的最小值,不会滑动超过这个点

maxX X方向上滑动的最大值,不会滑动超过这个点

minY Y方向上滑动的最小值,不会滑动超过这个点

maxY Y方向上滑动的最大值,不会滑动超过这个点

Scroller源码解析

1、 构造方法

/**
 * Create a Scroller with the specified interpolator. If the interpolator is
 * null, the default (viscous) interpolator will be used. Specify whether or
 * not to support progressive "flywheel" behavior in flinging.
 */
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    mFinished = true;
    if (interpolator == null) {
        mInterpolator = new ViscousFluidInterpolator();
    } else {
        mInterpolator = interpolator;
    }
    mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    mFlywheel = flywheel;

    mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

注释写的很清楚,可以传入自定义的interpolator和是否支持飞轮flywheel的功能,当然这两个并不是必须的。如果不传入interpolator会默认创建一个ViscousFluidInterpolator,从字面意义上看是一个粘性流体插值器。对于flywheel是指是否支持在滑动过程中,如果有新的fling()方法调用是否累加加速度。

2、startScroll()方法的实现

/**
 * Start scrolling by providing a starting point, the distance to travel,
 * and the duration of the scroll.
 * 
 * @param startX Starting horizontal scroll offset in pixels. Positive
 *        numbers will scroll the content to the left.
 * @param startY Starting vertical scroll offset in pixels. Positive numbers
 *        will scroll the content up.
 * @param dx Horizontal distance to travel. Positive numbers will scroll the
 *        content to the left.
 * @param dy Vertical distance to travel. Positive numbers will scroll the
 *        content up.
 * @param duration Duration of the scroll in milliseconds.
 */
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

注释里有对变量进行解释

####3.computeScrollOffset() 方法中 SCROLL_MODE 的实现

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */ 
public boolean computeScrollOffset() {
    //判断滚动是否结束
    if (mFinished) {
        return false;
    }
    //滚动消耗的时间
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            //计算当前偏移量的比例
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        case FLING_MODE:
          ......

            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

到这整个的过程就结束了 。