Android 带你从源码的角度解析Scroller的滚动实现原理

转帖请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17483273),请尊重他人的辛勤劳动成果,谢谢!java

今天给你们讲解的是Scroller类的滚动实现原理,可能不少朋友不太了解该类是用来干吗的,可是研究Launcher的朋友应该对他很熟悉,Scroller类是滚动的一个封装类,能够实现View的平滑滚动效果,什么是实现View的平滑滚动效果呢,举个简单的例子,一个View从在咱们指定的时间内从一个位置滚动到另一个位置,咱们利用Scroller类能够实现匀速滚动,能够先加速后减速,能够先减速后加速等等效果,而不是瞬间的移动的效果,因此Scroller能够帮咱们实现不少滑动的效果。android

在介绍Scroller类以前,咱们先去了解View的scrollBy() 和scrollTo()方法的区别,在区分这两个方法的以前,咱们要先理解View 里面的两个成员变量mScrollX, mScrollY,X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,相对的不是屏幕的原点,而是View的左边缘,举个通俗易懂的例子,一列火车从吉安到深圳,途中通过赣州,那么原点就是赣州,偏移量就是 负的吉安到赣州的距离,你们从getScrollX()方法中的注释中就能看出答案来canvas

[java] view plaincopy在CODE上查看代码片派生到个人代码片app

  1. /** ide

  2.     * Return the scrolled left position of this view. This is the left edge of post

  3.     * the displayed part of your view. You do not need to draw any pixels 动画

  4.     * farther left, since those are outside of the frame of your view on ui

  5.     * screen. this

  6.     * spa

  7.     * @return The left edge of the displayed part of your view, in pixels. 

  8.     */  

  9.    public final int getScrollX() {  

  10.        return mScrollX;  

  11.    }  

如今咱们知道了向右滑动 mScrollX就为负数,向左滑动mScrollX为正数,接下来咱们先来看看 scrollTo()方法的源码

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.    * Set the scrolled position of your view. This will cause a call to 

  3.    * {@link #onScrollChanged(int, int, int, int)} and the view will be 

  4.    * invalidated. 

  5.    * @param x the x position to scroll to 

  6.    * @param y the y position to scroll to 

  7.    */  

  8.   public void scrollTo(int x, int y) {  

  9.       if (mScrollX != x || mScrollY != y) {  

  10.           int oldX = mScrollX;  

  11.           int oldY = mScrollY;  

  12.           mScrollX = x;  

  13.           mScrollY = y;  

  14.           onScrollChanged(mScrollX, mScrollY, oldX, oldY);  

  15.           if (!awakenScrollBars()) {  

  16.               invalidate();  

  17.           }  

  18.       }  

  19.   }  

从该方法中咱们能够看出,先判断传进来的(x, y)值是否和View的X, Y偏移量相等,若是不相等,就调用onScrollChanged()方法来通知界面发生改变,而后重绘界面,因此这样子就实现了移动效果啦, 如今咱们知道了scrollTo()方法是滚动到(x, y)这个偏移量的点,他是相对于View的开始位置来滚动的。在看看scrollBy()这个方法的代码

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.     * Move the scrolled position of your view. This will cause a call to 

  3.     * {@link #onScrollChanged(int, int, int, int)} and the view will be 

  4.     * invalidated. 

  5.     * @param x the amount of pixels to scroll by horizontally 

  6.     * @param y the amount of pixels to scroll by vertically 

  7.     */  

  8.    public void scrollBy(int x, int y) {  

  9.        scrollTo(mScrollX + x, mScrollY + y);  

  10.    }  

原来他里面调用了scrollTo()方法,那就好办了,他就是相对于View上一个位置根据(x, y)来进行滚动,可能你们脑海中对这两个方法还有点模糊,不要紧,仍是举个通俗的例子帮你们理解下,假如一个View,调用两次scrollTo(-10, 0),第一次向右滚动10,第二次就不滚动了,由于mScrollX和x相等了,当咱们调用两次scrollBy(-10, 0),第一次向右滚动10,第二次再向右滚动10,他是相对View的上一个位置来滚动的。

对于scrollTo()和scrollBy()方法还有一点须要注意,这点也很重要,假如你给一个LinearLayout调用scrollTo()方法,并非LinearLayout滚动,而是LinearLayout里面的内容进行滚动,好比你想对一个按钮进行滚动,直接用Button调用scrollTo()必定达不到你的需求,你们能够试一试,若是真要对某个按钮进行scrollTo()滚动的话,咱们能够在Button外面包裹一层Layout,而后对Layout调用scrollTo()方法。


了解了scrollTo()和scrollBy()方法以后咱们就了解下Scroller类了,先看其构造方法

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.  * Create a Scroller with the default duration and interpolator. 

  3.  */  

  4. public Scroller(Context context) {  

  5.     this(context, null);  

  6. }  

  7.   

  8. /** 

  9.  * Create a Scroller with the specified interpolator. If the interpolator is 

  10.  * null, the default (viscous) interpolator will be used. 

  11.  */  

  12. public Scroller(Context context, Interpolator interpolator) {  

  13.     mFinished = true;  

  14.     mInterpolator = interpolator;  

  15.     float ppi = context.getResources().getDisplayMetrics().density * 160.0f;  

  16.     mDeceleration = SensorManager.GRAVITY_EARTH   // g (m/s^2)  

  17.                   * 39.37f                        // inch/meter  

  18.                   * ppi                           // pixels per inch  

  19.                   * ViewConfiguration.getScrollFriction();  

  20. }  

只有两个构造方法,第一个只有一个Context参数,第二个构造方法中指定了Interpolator,什么Interpolator呢?中文意思插补器,了解Android动画的朋友都应该熟悉
Interpolator,他指定了动画的变化率,好比说匀速变化,先加速后减速,正弦变化等等,不一样的Interpolator能够作出不一样的效果出来,第一个使用默认的Interpolator(viscous) 


接下来咱们就要在Scroller类里面找滚动的方法,咱们从名字上面能够看出startScroll()应该是个滚动的方法,咱们来看看其源码吧

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. public void startScroll(int startX, int startY, int dx, int dy, int duration) {  

  2.     mMode = SCROLL_MODE;  

  3.     mFinished = false;  

  4.     mDuration = duration;  

  5.     mStartTime = AnimationUtils.currentAnimationTimeMillis();  

  6.     mStartX = startX;  

  7.     mStartY = startY;  

  8.     mFinalX = startX + dx;  

  9.     mFinalY = startY + dy;  

  10.     mDeltaX = dx;  

  11.     mDeltaY = dy;  

  12.     mDurationReciprocal = 1.0f / (float) mDuration;  

  13.     // This controls the viscous fluid effect (how much of it)  

  14.     mViscousFluidScale = 8.0f;  

  15.     // must be set to 1.0 (used in viscousFluid())  

  16.     mViscousFluidNormalize = 1.0f;  

  17.     mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);  

  18. }  

在这个方法中咱们只看到了对一些滚动的基本设置动做,好比设置滚动模式,开始时间,持续时间等等,并无任何对View的滚动操做,也许你正纳闷,不是滚动的方法干吗还叫作startScroll(),稍安勿躁,既然叫开始滚动,那就是对滚动的滚动以前的基本设置咯。

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.  * Call this when you want to know the new location.  If it returns true, 

  3.  * the animation is not yet finished.  loc will be altered to provide the 

  4.  * new location. 

  5.  */   

  6. public boolean computeScrollOffset() {  

  7.     if (mFinished) {  

  8.         return false;  

  9.     }  

  10.   

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

  12.   

  13.     if (timePassed < mDuration) {  

  14.         switch (mMode) {  

  15.         case SCROLL_MODE:  

  16.             float x = (float)timePassed * mDurationReciprocal;  

  17.   

  18.             if (mInterpolator == null)  

  19.                 x = viscousFluid(x);   

  20.             else  

  21.                 x = mInterpolator.getInterpolation(x);  

  22.   

  23.             mCurrX = mStartX + Math.round(x * mDeltaX);  

  24.             mCurrY = mStartY + Math.round(x * mDeltaY);  

  25.             break;  

  26.         case FLING_MODE:  

  27.             float timePassedSeconds = timePassed / 1000.0f;  

  28.             float distance = (mVelocity * timePassedSeconds)  

  29.                     - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);  

  30.               

  31.             mCurrX = mStartX + Math.round(distance * mCoeffX);  

  32.             // Pin to mMinX <= mCurrX <= mMaxX  

  33.             mCurrX = Math.min(mCurrX, mMaxX);  

  34.             mCurrX = Math.max(mCurrX, mMinX);  

  35.               

  36.             mCurrY = mStartY + Math.round(distance * mCoeffY);  

  37.             // Pin to mMinY <= mCurrY <= mMaxY  

  38.             mCurrY = Math.min(mCurrY, mMaxY);  

  39.             mCurrY = Math.max(mCurrY, mMinY);  

  40.               

  41.             break;  

  42.         }  

  43.     }  

  44.     else {  

  45.         mCurrX = mFinalX;  

  46.         mCurrY = mFinalY;  

  47.         mFinished = true;  

  48.     }  

  49.     return true;  

  50. }  

咱们在startScroll()方法的时候获取了当前的动画毫秒赋值给了mStartTime,在computeScrollOffset()中再一次调用AnimationUtils.currentAnimationTimeMillis()来获取动画
毫秒减去mStartTime就是持续时间了,而后进去if判断,若是动画持续时间小于咱们设置的滚动持续时间mDuration,进去switch的SCROLL_MODE,而后根据Interpolator来计算出在该时间段里面移动的距离,赋值给mCurrX, mCurrY, 因此该方法的做用是,计算在0到mDuration时间段内滚动的偏移量,而且判断滚动是否结束,true表明还没结束,false则表示滚动介绍了,Scroller类的其余的方法我就不提了,大都是一些get(), set()方法。

看了这么多,到底要怎么才能触发滚动,你内心确定有不少疑惑,在说滚动以前我要先提另一个方法computeScroll(),该方法是滑动的控制方法,在绘制View时,会在draw()过程调用该方法。咱们先看看computeScroll()的源码

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.     * Called by a parent to request that a child update its values for mScrollX 

  3.     * and mScrollY if necessary. This will typically be done if the child is 

  4.     * animating a scroll using a {@link android.widget.Scroller Scroller} 

  5.     * object. 

  6.     */  

  7.    public void computeScroll() {  

  8.    }  

没错,他是一个空的方法,须要子类去重写该方法来实现逻辑,到底该方法在哪里被触发呢。咱们继续看看View的绘制方法draw()

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. public void draw(Canvas canvas) {  

  2.        final int privateFlags = mPrivateFlags;  

  3.        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&  

  4.                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  

  5.        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;  

  6.   

  7.        /* 

  8.         * Draw traversal performs several drawing steps which must be executed 

  9.         * in the appropriate order: 

  10.         * 

  11.         *      1. Draw the background 

  12.         *      2. If necessary, save the canvas' layers to prepare for fading 

  13.         *      3. Draw view's content 

  14.         *      4. Draw children 

  15.         *      5. If necessary, draw the fading edges and restore layers 

  16.         *      6. Draw decorations (scrollbars for instance) 

  17.         */  

  18.   

  19.        // Step 1, draw the background, if needed  

  20.        int saveCount;  

  21.   

  22.        if (!dirtyOpaque) {  

  23.            final Drawable background = mBackground;  

  24.            if (background != null) {  

  25.                final int scrollX = mScrollX;  

  26.                final int scrollY = mScrollY;  

  27.   

  28.                if (mBackgroundSizeChanged) {  

  29.                    background.setBounds(00,  mRight - mLeft, mBottom - mTop);  

  30.                    mBackgroundSizeChanged = false;  

  31.                }  

  32.   

  33.                if ((scrollX | scrollY) == 0) {  

  34.                    background.draw(canvas);  

  35.                } else {  

  36.                    canvas.translate(scrollX, scrollY);  

  37.                    background.draw(canvas);  

  38.                    canvas.translate(-scrollX, -scrollY);  

  39.                }  

  40.            }  

  41.        }  

  42.   

  43.        // skip step 2 & 5 if possible (common case)  

  44.        final int viewFlags = mViewFlags;  

  45.        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  

  46.        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  

  47.        if (!verticalEdges && !horizontalEdges) {  

  48.            // Step 3, draw the content  

  49.            if (!dirtyOpaque) onDraw(canvas);  

  50.   

  51.            // Step 4, draw the children  

  52.            dispatchDraw(canvas);  

  53.   

  54.            // Step 6, draw decorations (scrollbars)  

  55.            onDrawScrollBars(canvas);  

  56.   

  57.            // we're done...  

  58.            return;  

  59.        }  

  60.   

  61.        ......  

  62.        ......  

  63.        ......  

咱们只截取了draw()的部分代码,这上面11-16行为咱们写出了绘制一个View的几个步骤,咱们看看第四步绘制孩子的时候会触发dispatchDraw()这个方法,来看看源码是什么内容

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. /** 

  2.     * Called by draw to draw the child views. This may be overridden 

  3.     * by derived classes to gain control just before its children are drawn 

  4.     * (but after its own view has been drawn). 

  5.     * @param canvas the canvas on which to draw the view 

  6.     */  

  7.    protected void dispatchDraw(Canvas canvas) {  

  8.   

  9.    }  

好吧,又是定义的一个空方法,给子类来重写的方法,因此咱们找到View的子类ViewGroup来看看该方法的具体实现逻辑

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1. @Override  

  2. protected void dispatchDraw(Canvas canvas) {  

  3.     final int count = mChildrenCount;  

  4.     final View[] children = mChildren;  

  5.     int flags = mGroupFlags;  

  6.   

  7.     if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {  

  8.         final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;  

  9.   

  10.         final boolean buildCache = !isHardwareAccelerated();  

  11.         for (int i = 0; i < count; i++) {  

  12.             final View child = children[i];  

  13.             if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {  

  14.                 final LayoutParams params = child.getLayoutParams();  

  15.                 attachLayoutAnimationParameters(child, params, i, count);  

  16.                 bindLayoutAnimation(child);  

  17.                 if (cache) {  

  18.                     child.setDrawingCacheEnabled(true);  

  19.                     if (buildCache) {                          

  20.                         child.buildDrawingCache(true);  

  21.                     }  

  22.                 }  

  23.             }  

  24.         }  

  25.   

  26.         final LayoutAnimationController controller = mLayoutAnimationController;  

  27.         if (controller.willOverlap()) {  

  28.             mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;  

  29.         }  

  30.   

  31.         controller.start();  

  32.   

  33.         mGroupFlags &= ~FLAG_RUN_ANIMATION;  

  34.         mGroupFlags &= ~FLAG_ANIMATION_DONE;  

  35.   

  36.         if (cache) {  

  37.             mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;  

  38.         }  

  39.   

  40.         if (mAnimationListener != null) {  

  41.             mAnimationListener.onAnimationStart(controller.getAnimation());  

  42.         }  

  43.     }  

  44.   

  45.     int saveCount = 0;  

  46.     final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;  

  47.     if (clipToPadding) {  

  48.         saveCount = canvas.save();  

  49.         canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,  

  50.                 mScrollX + mRight - mLeft - mPaddingRight,  

  51.                 mScrollY + mBottom - mTop - mPaddingBottom);  

  52.   

  53.     }  

  54.   

  55.     // We will draw our child's animation, let's reset the flag  

  56.     mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;  

  57.     mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;  

  58.   

  59.     boolean more = false;  

  60.     final long drawingTime = getDrawingTime();  

  61.   

  62.     if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {  

  63.         for (int i = 0; i < count; i++) {  

  64.             final View child = children[i];  

  65.             if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  

  66.                 more |= drawChild(canvas, child, drawingTime);  

  67.             }  

  68.         }  

  69.     } else {  

  70.         for (int i = 0; i < count; i++) {  

  71.             final View child = children[getChildDrawingOrder(count, i)];  

  72.             if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  

  73.                 more |= drawChild(canvas, child, drawingTime);  

  74.             }  

  75.         }  

  76.     }  

  77.   

  78.     // Draw any disappearing views that have animations  

  79.     if (mDisappearingChildren != null) {  

  80.         final ArrayList<View> disappearingChildren = mDisappearingChildren;  

  81.         final int disappearingCount = disappearingChildren.size() - 1;  

  82.         // Go backwards -- we may delete as animations finish  

  83.         for (int i = disappearingCount; i >= 0; i--) {  

  84.             final View child = disappearingChildren.get(i);  

  85.             more |= drawChild(canvas, child, drawingTime);  

  86.         }  

  87.     }  

  88.   

  89.     if (debugDraw()) {  

  90.         onDebugDraw(canvas);  

  91.     }  

  92.   

  93.     if (clipToPadding) {  

  94.         canvas.restoreToCount(saveCount);  

  95.     }  

  96.   

  97.     // mGroupFlags might have been updated by drawChild()  

  98.     flags = mGroupFlags;  

  99.   

  100.     if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {  

  101.         invalidate(true);  

  102.     }  

  103.   

  104.     if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&  

  105.             mLayoutAnimationController.isDone() && !more) {  

  106.         // We want to erase the drawing cache and notify the listener after the  

  107.         // next frame is drawn because one extra invalidate() is caused by  

  108.         // drawChild() after the animation is over  

  109.         mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;  

  110.         final Runnable end = new Runnable() {  

  111.            public void run() {  

  112.                notifyAnimationListener();  

  113.            }  

  114.         };  

  115.         post(end);  

  116.     }  

  117. }  

这个方法代码有点多,可是咱们仍是挑重点看吧,从65-79行能够看出 在dispatchDraw()里面会对ViewGroup里面的子View调用drawChild()来进行绘制,接下来咱们来看看drawChild()方法的代码

[java] view plaincopy在CODE上查看代码片派生到个人代码片

  1.  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  

  2.     ......  

  3.     ......  

  4.   

  5.     if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&  

  6.                 (child.mPrivateFlags & DRAW_ANIMATION) == 0) {  

  7.             return more;  

  8.         }  

  9.   

  10.         child.computeScroll();  

  11.   

  12.         final int sx = child.mScrollX;  

  13.         final int sy = child.mScrollY;  

  14.   

  15.         boolean scalingRequired = false;  

  16.         Bitmap cache = null;  

  17.   

  18.     ......  

  19.     ......  

  20.   

  21. }  

只截取了部分代码,看到child.computeScroll()你大概明白什么了吧,转了老半天终于找到了computeScroll()方法被触发,就是ViewGroup在分发绘制本身的孩子的时候,会对其子View调用computeScroll()方法


整理下思路,来看看View滚动的实现原理,咱们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,而后迫使View进行绘制,咱们调用View的invalidate()或postInvalidate()就能够从新绘制View,绘制View的时候会触发computeScroll()方法,咱们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,若是滚动没有结束咱们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会从新绘制View,可是咱们仍是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,从新绘制View又触发computeScroll(),因此就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果
也许有人会问,干吗还要调用来调用去最后在调用scrollTo()方法,还不如直接调用scrollTo()方法来实现滚动,其实直接调用是能够,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,因此Android提供了Scroller类实现平滑滚动的效果。为了方面你们理解,我画了一个简单的调用示意图


好了,讲到这里就已经讲完了Scroller类的滚动实现原理啦,不知道你们理解了没有,Scroller能实现不少滚动的效果,因为考虑到这篇文章的篇幅有点长,因此这篇文章就不带领你们来使用Scroller类了,我将在下一篇文章将会带来Scroller类的使用,但愿你们到时候关注下,有疑问的同窗在下面留言,我会为你们解答的!

相关文章
相关标签/搜索