Scroller源码分析

时光荏苒, 很久很久以前学校时拍的。左一班长去年结婚了, 左二去了腾讯混得不错, 左三好像在上海切玻璃...haha
 Android API 21
  1. 作用
  2. 成员
  3. 方法
  4. 使用注意
  5. 替补选手 OverScroller
1. 作用
  • Scroller是一个滚动的计算工具类,他根据客户端传入的滚动初始坐标x,y数值,以及最终的x,y数值,时间或者速度等,利用时间变化插值器来计算不同时刻的滚动坐标的,从而实现一个动态,平滑的滚动效果。比如我们的viewPager手动设置Position页面的的平滑滚动效果,又比如我们的ScrollView的滑翔滚动都是利用它来计算和实现的。
2. 重要成员
  1. mStartX, mStartY,mFinalX, mFinalY:起始和滚动终点的x, y坐标。
  2. mCurrX, mCurrY:某时刻的当前坐标
  3. mDuration, mDistance:时间和距离
  4. SCROLL_MODE, FLING_MODE:前者是普通的滚动方式,后者是松手后会滑翔一段距离的滚动方式
  5. mFinished: true结束滚动啦, false还在滚动
3. 关键方法

​ 一波代码正向你袭来额 ~

  1. 构造Scroller(): 如果客户端 (就是调用端哦 ) 没有给Scroller设定插值器,就给他构建一个插值器,然后初始化一些物理效果的减速配置类似于摩擦力,滑翔操作就是靠这个摩擦力来停下来的哦。时间插值器,简单理解就是随着时间比例变化,我们的对应坐标数值比例变化。如果是匀速那么二者是一样的,如果是加速那么就先变化快,后变化慢,这样子啦。类似于如果匀速变化时间变化比例达到了0.5, 坐标变化也就是0.5.如果是加速那么时间0.5, 坐标变化可能就是0.3, 0.4这样子。

    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());
         //fling模式下,是否累加前面还未结束的速度
            mFlywheel = flywheel;
    
            mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
        }
    
  2. 普通滚动: startScroll: 这里主要初始客户端配置的滚动信息,可以看出,这里start并没有真正地启动滚动哦。

    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        //普通滚动模式
            mMode = SCROLL_MODE;
            mFinished = false;
            mDuration = duration;
            mStartTime = AnimationUtils.currentAnimationTimeMillis();
        //设定启动,和结束的滚动x, y坐标
            mStartX = startX;
            mStartY = startY;
            mFinalX = startX + dx;
            mFinalY = startY + dy;
            mDeltaX = dx;
            mDeltaY = dy;
        //总时间作为底数,为了后面计算时间变化因子
            mDurationReciprocal = 1.0f / (float) mDuration;
        }
    
    
    
  3. 滑翔滚动fling: 这里的主要意图是根据客户端设置的起点x, y坐标以及滑翔的初始速度以及设置的摩擦力减速来计算将要滑动的最终目标坐标和所需时间。因此这里的客户端一开始是不需要知道滑动的目的地,距离是多少的哦,系统会计算的;

     public void fling(int startX, int startY, int velocityX, int velocityY,
                int minX, int maxX, int minY, int maxY) {
            // 如果支持滑动累积,并且还没有结束滑动
            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;
                }
            }
    
         //设置滑翔模式,
            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;
         
         //计算x, y方向上速度的分量
            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;
     //根据x方向的分量和总距离,来计算x向最终的滚动位置
            mFinalX = startX + (int) Math.round(totalDistance * coeffX);
            // Pin to mMinX <= mFinalX <= mMaxX
            mFinalX = Math.min(mFinalX, mMaxX);
            mFinalX = Math.max(mFinalX, mMinX);
         //根据y方向的分量和总距离,来计算y向最终的滚动位置
            mFinalY = startY + (int) Math.round(totalDistance * coeffY);
            // Pin to mMinY <= mFinalY <= mMaxY
            mFinalY = Math.min(mFinalY, mMaxY);
            mFinalY = Math.max(mFinalY, mMinY);
        }
    
    
  4. computeScrollOffset: 我们的startScroll和fling都只是设地滚动的初始值,或者根据设定的初始值来计算最后的滚动位置,但是中间特定时刻的滚动位置,他们二者是没有去做的,那这些功能肯定是要有人来做的,要不然视图在刷新ui的时候怎么知道该在哪个位置绘制新的内容呢, 这就是computeScrollOffset的意图啦。

     public boolean computeScrollOffset() {
         //如果滚动结束,该方法返回false.
            if (mFinished) {
                return false;
            }
    
            int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
         //如果滚动过的时间还没到我们总的滚动耗时
            if (timePassed < mDuration) {
                switch (mMode) {
                case SCROLL_MODE://普通滚动
                        //根据时间插值器和当前消耗的时间比例来计算数值消费的比例因子;
                        //如匀速计算,500ms/1000s的消耗的时间因子是0.5,那么数值消费的因子也是0.5,
                        //如果是加速计算,那么时间因子0.5对应的数值因子会大于0.5,可能是0.6,0.7等
                        //当前滚动距离 = 总距离 × 数值消费因子
                    final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                        //根据滚动距离和起始位置计算当前的x, y位置。
                    mCurrX = mStartX + Math.round(x * mDeltaX);
                    mCurrY = mStartY + Math.round(x * mDeltaY);
                    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;
                    //计算当前的x滚动位置
                    mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                    // Pin to mMinX <= mCurrX <= mMaxX
                    mCurrX = Math.min(mCurrX, mMaxX);
                    mCurrX = Math.max(mCurrX, mMinX);
                    //计算当前的y滚动位置
                    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;
            }
         //没有滚动完,返回true;
            return true;
        }
    
  5. abortAnimation: 终止滑动过程,如果在滑动的过程中突然想要中止滑动就利用他啦,比如ScrollView滑动的时候,手指突然触摸一下屏幕,就停啦,原理也很简单。mFinished = true;

      public void abortAnimation() {
          //设定位置
            mCurrX = mFinalX;
            mCurrY = mFinalY;
          // 关闭滚动
            mFinished = true;
        }
    

    Scroller基本就这些内容啦~

4. 使用注意
 Scrollers trackscroll offsets for you over time, but they don't automatically apply those
 positions to your view. It's your responsibility to get and apply new coordinates .....
     
  • 代码里有一段这样的话,意思就是说scroller只负责计算,但是他可不管把当前计算的值给到view上,不会去绘制view新的位置新的内容,这是你自己的事,别找我。好吧.....所以我们使用的时候,要自己来应用啦,怎么应用?
  1. 使用mScroller.startScroll(0, 0, xxxx, 0);
  2. 重写view的computeScroll方法, 在里面调用mScroller.computeScrollOffset()计算滚动位置,然后通过scroller.getCurentX, getCurretnY来获取位置,并应用这些位置到view上。重写computeScroll,是因为view在每60ms刷新屏幕的时候会来调我们的这个方法有没有滚动内容计算呢。
5. OverScroller: Scroller的替补选手

他其实和Scoller的意图是一样的,都是计算不同时刻的滚动坐标的。只不过他内部的是实现策略剥离到一个单独的类SplineOverScroller中去了,大致实现思路是相同的。不过他加强了Scroller的功能,支持了边界回弹效果,是通过对外接口springBack实现的。意思是如果我当前位置没有在设定的边界内,会继续以这个边界为目标数值,继续滚动以回到这个范围。一般会在手势跟踪中的手势抬起的时候,判断当前停靠的位置满不满足范围限制,如果不满足就要再设定一下滚动目标位置,再滚动下。

  1. springBack

    //返回false,表示当前滚动已经在目标区域了,可以停止渲染了; 返回true,表示当前的滚动位置还不在目标区域中,他还需要继续设置滚动位置, 计算滚动,并且你需要手动去触发一次重绘以开始新的滚动内容绘制,所以在调用该方法之后如果返回true,就要postInvalidateOnAnimation()一下啦。不信你看看ScrollView源码
    public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
            mMode = FLING_MODE;
    
            // Make sure both methods are called.
         //会将目标数值minx, maxY作为目标值设置
            final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
            final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
            return spingbackX || spingbackY;
        }
    
    
    
    1. SplineOverScroller.springback, startSpringback: 其实就是根据新的目标位置来计算新一轮的滚动,就是我们的回弹滚动啦。
     boolean springback(int start, int min, int max) {
                mFinished = true;
    
                 //重置前面的滚动计算的内容
                mStart = mFinal = start;
                mVelocity = 0;
    
                mStartTime = AnimationUtils.currentAnimationTimeMillis();
                mDuration = 0;
    
                if (start < min) {
                    startSpringback(start, min, 0);
                } else if (start > max) {
                    startSpringback(start, max, 0);
                }
             //mFinished=true,结束了就表示不用再搞啥计算了;mFinished=false,表示还没结束,你要给
                 //计算咯,和前面正好对应。
                return !mFinished;
            }
    
    ---
        
    private void startSpringback(int start, int end, int velocity) {
                // mStartTime has been set
             //没结束呢!
                mFinished = false;
                mState = CUBIC;
             //初始化start, final的x, y位置啦。
                mStart = start;
                mFinal = end;
                final int delta = start - end;
         
                mDeceleration = getDeceleration(delta);
                // TODO take velocity into account
                mVelocity = -delta; // only sign is used
         //计算新的滚动距离,和滚动时间。
                mOver = Math.abs(delta);
                mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
            }
    
    

好啦,结束啦, 道声晚安吧......

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