Android 源码解析 - Scroller

分析版本 : 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 本身。

备注:读到这里,是否有读者和我有一样的困惑,不是应该x>0,向右移动;y>0向下移动。因为在我看来(如下图所示),x方向向右为正,y方向向下为正。后来转念一想,应该这样理解才对。假设移动前的顶点坐标为(x1, y1),移动后的坐标为(x2,y2)。且x=x1-x2 > 0,即x1要大于x2,则x2只能向左移动。y方向的分析同理。得证!

/**
 * 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 重新绘制的

这里写图片描述

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;
}

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

原文转载自:https://blog.csdn.net/owenchan1987/article/details/79048432

猜你喜欢

转载自blog.csdn.net/n_fly/article/details/113771172
今日推荐