Scroller源码分析

本文分析版本: Android API 22

1.简介

Android开发中,如果我们希望使一个View滑动的话,除了使用属性动画外。我们还可以使用系统提供给我们的两个类ScrollerOverScroller用来实现弹性滑动。在我以前的一篇ViewDragHelper源码分析中我们有讲到过Scroller的作用。那么我们今天就来仔细分析一下Scroller的使用方法以及实现方式。

2.使用方法

在看Scroller的使用方法之前我们需要先了解一下View中的scrollBy()scrollTo()方法,scrollTo()方法的实现如下:


    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距离,纵向偏移y距离。注意这里是View的内容的偏移,而不是View本身。而scrollBy()方法如下:

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

scrollBy()方法里直接调用了scrollTo()方法,表示在当前偏移量的基础上继续偏移(x,y)。现在我们来看看Scroller的用法。SkyScrollerDemo是我写的一个ScrollerOverScroller的使用demo。下面的用法都是来自于这个demo里,大家可以clone下来配合本文一起阅读。本文我们主要研究Scroller。对于OverScroller我在demo里也写了相关的使用方法,在本文的最后我们再做讨论。

Scroller一般需要配合重写computeScroll()一起使用,代码如下:

public class ScrollTextView extends TextView {
    private Context mContext;
    private Scroller mScroller;

    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        init();
    }

    private void init() {
        mScroller = new Scroller(mContext);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            offsetLeftAndRight(mScroller.getCurrX() - mLeft);
            offsetTopAndBottom(mScroller.getCurrY() - mTop);
            invalidate();
        }
    }
    //以mLeft,mTop为初始点,在DEFAULT_DURATION的时间内,在Y轴上滑动-400的偏移量
    public void startScrollerScroll() {
        mScroller.startScroll(mLeft, mTop, 0, -400, DEFAULT_DURATION);
        invalidate();
    }
    //以mLeft,mTop为初始点,并以Y方向上-5000的加速度滑动,最小Y坐标为200,最大Y坐标为1200
    public void startScrollerFling() {
        mScroller.fling(mLeft, mTop, 0, -5000, mLeft, mLeft, 200, 1200);
        invalidate();
    }
}

在上面的代码里,当我们调用startScrollerScroll()startScrollerFling()方法时我们就发现View滑动了。如果以前没了解过Scroller的同学可能会不理解。这里大致分析一下调用流程,首先我们要知道Scroller其实只负责计算,它并不负责滑动View,当我们调用了ScrollerstartScrollerScroll()方法时,我们紧接着调用了invalidate()方法。invalidate()方法会使View重新绘制。因此会调用Viewdraw()方法,在Viewdraw()方法中又会去调用computeScroll()方法,computeScroll()方法在View中是一个空实现,所以需要我们自己实现computeScroll()方法。在上面的computeScroll()方法中,我们调用了mScroller.computeScrollOffset()方法来计算当前滑动的偏移量。如果还在滑动过程中就会返回true。所以我们就能在if中通过Scroller拿到当前的滑动坐标从而做任何我们想做的处理。在demo里我们根据滑动的偏移量来改变了View的坐标偏移量。从而形成了滑动动画。下面我们解释一下Scroller的两个方法的具体作用:

1.startScroll(int startX, int startY, int dx, int dy, int duration):

通过起始点、偏移的距离和滑动的时间来开始滑动。

  • startX 起始滑动点的X坐标
  • startY 起始滑动点的Y坐标
  • dx 滑动的水平偏移量。>0 则表示往左滑动。
  • dy 滑动的垂直偏移量。>0 则表示往上滑动。
  • duration 滑动执行的时间

2.fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) :

基于一个快速滑动手势下的滑动。滑动的距离与这个手势最初的加速度有关。

  • startX 起始滑动点的X坐标
  • startY 起始滑动点的Y坐标
  • velocityX X方向上的加速度
  • velocityY Y方向上的加速度
  • minX X方向上滑动的最小值,不会滑动超过这个点
  • maxX X方向上滑动的最大值,不会滑动超过这个点
  • minY Y方向上滑动的最小值,不会滑动超过这个点
  • maxY Y方向上滑动的最大值,不会滑动超过这个点

3.源码分析

我们依然通过调用流程来分析Scroller的实现:

1.构造方法

public Scroller(Context context) {
    this(context, null);
}

public Scroller(Context context, Interpolator interpolator) {
    this(context, interpolator,
            context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}

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
}

最终都会调用最后一个构造方法。必须传入Context对象。可以传入自定义的interpolator和是否支持飞轮flywheel的功能,当然这两个并不是必须的。如果不传入interpolator会默认创建一个ViscousFluidInterpolator,从字面意义上看是一个粘性流体插值器。对于flywheel是指是否支持在滑动过程中,如果有新的fling()方法调用是否累加加速度。如果不传默认在2.3以上都会支持。剩下就是初始化了一些用于计算的参数。这样就完成了Scroller的初始化了。下面我们来看看startScroll()方法的实现:

2.startScroll()方法的实现

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
  // mMode 分两种方式 1.滑动:SCROLL_MODE 2. 加速度滑动:FLING_MODE
  mMode = SCROLL_MODE;
  // 是否滑动结束 这里是开始所以设置为false
  mFinished = false;
  // 滑动的时间
  mDuration = duration;
  // 开始的时间
  mStartTime = AnimationUtils.currentAnimationTimeMillis();
  // 开始滑动点的X坐标
  mStartX = startX;
  // 开始滑动点的Y坐标
  mStartY = startY;
  // 最终滑动到位置的X坐标
  mFinalX = startX + dx;
  // 最终滑动到位置的Y坐标
  mFinalY = startY + dy;
  // X方向上滑动的偏移量
  mDeltaX = dx;
  // Y方向上滑动的偏移量
  mDeltaY = dy;
  // 持续时间的倒数 最终用来计算得到插值器返回的值
  mDurationReciprocal = 1.0f / (float) mDuration;
}

很简单只是一些变量的赋值。根据我们前面使用方法里的分析,最终会调用computeScrollOffset()方法:

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

// 当你需要知道新的位置的时候调用这个方法,如果动画还未结束则返回true
public boolean computeScrollOffset() {
    //如果已经结束 则直接返回false
    if (mFinished) {
        return false;
    }
    //得到以及度过的时间
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    //如果还在动画时间内
    if (timePassed < mDuration) {
        switch (mMode) {
            case SCROLL_MODE:
                // 根据timePassed * mDurationReciprocal,从mInterpolator中取出当前需要偏移量的比例
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                // 赋值给 mCurrX,mCurrY
                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;
}

首先的到当前时间与滑动开始时间的时间差,如果还在滑动时间内则通过插值器获得当前的进度并乘以总偏移量并赋值给mCurrXmCurrY。如果已经结束则直接将mFinalXmFinalY赋值并将mFinished设置�为true。所以这样我们就能通过getCurrX()getCurrY()来得到对应的mCurrXmCurrY来做相应的处理了。整个Scroll的过程就是这样了。

4.fling()方法的实现

    public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY) {
        // 如果前一次滑动还未结束,又调用了新的fling()方法时,
        // 则累加相同方向上加速度
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = FloatMath.sqrt(dx * dx + dy * 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;
            }
        }

        //设置为FLING_MODE
        mMode = FLING_MODE;
        mFinished = false;
        //根据勾股定理获得总加速度
        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);

        mVelocity = velocity;
        // 通过加速度得到滑动持续时间
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));

        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);

        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }

依然是为计算需要的各种变量赋值。因为引入了加速度的概念所以变得相对复杂,首先先判断了如果一次滑动未结束又触发另一次滑动时,是否需要累加加速度。然后是设置mModeFLING_MODE。然后根据velocityXvelocityY算出总的加速度velocity,紧接着算出这个加速度下可以滑动的距离mDistance。最后再通过xy方向上的加速度比值以及我们设定的最大值和最小值来给mFinalXmFinalY赋值。赋值结束后,通过调用invalidate(),最终依然会调用computeScrollOffset()方法:

5.computeScrollOffset() 方法中 FLING_MODE 的实现


    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
                case SCROLL_MODE:
                    ...
                    break;
                case FLING_MODE:
                    // 当前已滑动的时间与总滑动时间的比值
                    final float t = (float) timePassed / mDuration;
                    final int index = (int) (NB_SAMPLES * t);
                    // 距离系数
                    float distanceCoef = 1.f;
                    // 加速度系数
                    float velocityCoef = 0.f;
                    if (index < NB_SAMPLES) {
                        final float t_inf = (float) index / NB_SAMPLES;
                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
                        final float d_inf = SPLINE_POSITION[index];
                        final float d_sup = SPLINE_POSITION[index + 1];
                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                    }

                    // 计算出当前的加速度
                    mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                    // 计算出当前的mCurrX 与mCurrY
                    mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                    // Pin to mMinX <= mCurrX <= mMaxX
                    mCurrX = Math.min(mCurrX, mMaxX);
                    mCurrX = Math.max(mCurrX, mMinX);

                    mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                    // Pin to mMinY <= mCurrY <= mMaxY
                    mCurrY = Math.min(mCurrY, mMaxY);
                    mCurrY = Math.max(mCurrY, mMinY);

                    // 如果到达了终点 则结束
                    if (mCurrX == mFinalX && mCurrY == mFinalY) {
                        mFinished = true;
                    }

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

由于fling()方法中将mMode赋值为FLING_MODE。所以我们直接来看FLING_MODE中的代码。可以看出根据当前滑动时间与总滑动时间的比例。再根据一个SPLINE_POSITION数组计算出了距离系数distanceCoef与加速度系数velocityCoef。再根据这两个系数计算出当前加速度与当前的mCurrXmCurrY。关于SPLINE_POSITION的初始化是在下面的静态代码块里赋值的:

    static {
        float x_min = 0.0f;
        float y_min = 0.0f;
        for (int i = 0; i < NB_SAMPLES; i++) {
            final float alpha = (float) i / NB_SAMPLES;

            float x_max = 1.0f;
            float x, tx, coef;
            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                if (Math.abs(tx - alpha) < 1E-5) break;
                if (tx > alpha) x_max = x;
                else x_min = x;
            }
            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

            float y_max = 1.0f;
            float y, dy;
            while (true) {
                y = y_min + (y_max - y_min) / 2.0f;
                coef = 3.0f * y * (1.0f - y);
                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                if (Math.abs(dy - alpha) < 1E-5) break;
                if (dy > alpha) y_max = y;
                else y_min = y;
            }
            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
        }
        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
    }

我并没有看懂这段代码的实际意义。网上也没有找到比较清晰的解释。通过debug得知SPLINE_POSITION是一个长度为101并且从0-1递增数组。猜想这应该是一个函数模型并且最终用于计算出滑动过程中的加速度与位置。如果有同学能详细解释这段代码的作用,欢迎在这篇文章留言。至此Scroller的两个主要方法的实现我们就分析完了。

4.OverScroller解析

OverScroller是对Scroller的拓展,它在Scroller的基础上拓展出了更多的方法。OverScrollerfling方法支持滑动到终点之后并超出一段距离并返回,类似于弹性效果。另外一个springBack()方法是指将指定的点平滑滚动到指定的终点上。这个终点由设置的参数决定。原理我们就不再探究了,大家可以自行研究这两个类的差别。最后具体的使用方法在文章最上面的demo里都有提供。可以clone下来帮助理解。

我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: http://weibo.com/u/2030683111
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容