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

1
2
3
4
5
6
7
8
9
10
11
/**
     * Return the scrolled left position of this view. This is the left edge of
     * the displayed part of your view. You do not need to draw any pixels
     * farther left, since those are outside of the frame of your view on
     * screen.
     *
     * @return The left edge of the displayed part of your view, in pixels.
     */ 
    public  final  int  getScrollX() { 
        return  mScrollX; 
   

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
    * 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; 
           onScrollChanged(mScrollX, mScrollY, oldX, oldY); 
           if  (!awakenScrollBars()) { 
               invalidate(); 
          
      
   }

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

1
2
3
4
5
6
7
8
9
10
/**
     * 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); 
    }

  

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

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
  * Create a Scroller with the default duration and interpolator.
  */ 
public  Scroller(Context context) { 
     this (context,  null ); 
   
/**
  * Create a Scroller with the specified interpolator. If the interpolator is
  * null, the default (viscous) interpolator will be used.
  */ 
public  Scroller(Context context, Interpolator interpolator) { 
     mFinished =  true
     mInterpolator = interpolator; 
     float  ppi = context.getResources().getDisplayMetrics().density *  160 .0f; 
     mDeceleration = SensorManager.GRAVITY_EARTH    // g (m/s^2) 
                   39 .37f                         // inch/meter 
                   * ppi                            // pixels per inch 
                   * ViewConfiguration.getScrollFriction(); 

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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; 
     // This controls the viscous fluid effect (how much of it) 
     mViscousFluidScale =  8 .0f; 
     // must be set to 1.0 (used in viscousFluid()) 
     mViscousFluidNormalize =  1 .0f; 
     mViscousFluidNormalize =  1 .0f / viscousFluid( 1 .0f); 
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
  * Call this when you want to know the new location.  If it returns true,
  * the animation is not yet finished.  loc will be altered to provide the
  * new location.
  */  
public  boolean  computeScrollOffset() { 
     if  (mFinished) { 
         return  false
    
   
     int  timePassed = ( int )(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 
   
     if  (timePassed < mDuration) { 
         switch  (mMode) { 
         case  SCROLL_MODE: 
             float  x = ( float )timePassed * mDurationReciprocal; 
   
             if  (mInterpolator ==  null
                 x = viscousFluid(x);  
             else 
                 x = mInterpolator.getInterpolation(x); 
   
             mCurrX = mStartX + Math.round(x * mDeltaX); 
             mCurrY = mStartY + Math.round(x * mDeltaY); 
             break
         case  FLING_MODE: 
             float  timePassedSeconds = timePassed /  1000 .0f; 
             float  distance = (mVelocity * timePassedSeconds) 
                     - (mDeceleration * timePassedSeconds * timePassedSeconds /  2 .0f); 
               
             mCurrX = mStartX + Math.round(distance * mCoeffX); 
             // Pin to mMinX <= mCurrX <= mMaxX 
             mCurrX = Math.min(mCurrX, mMaxX); 
             mCurrX = Math.max(mCurrX, mMinX); 
               
             mCurrY = mStartY + Math.round(distance * mCoeffY); 
             // Pin to mMinY <= mCurrY <= mMaxY 
             mCurrY = Math.min(mCurrY, mMaxY); 
             mCurrY = Math.max(mCurrY, mMinY); 
               
             break
        
    
     else 
         mCurrX = mFinalX; 
         mCurrY = mFinalY; 
         mFinished =  true
    
     return  true

  咱们在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()的源码

1
2
3
4
5
6
7
8
/**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */ 
    public  void  computeScroll() { 
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public  void  draw(Canvas canvas) { 
        final  int  privateFlags = mPrivateFlags; 
        final  boolean  dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 
                (mAttachInfo ==  null  || !mAttachInfo.mIgnoreDirtyState); 
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; 
   
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */ 
   
        // Step 1, draw the background, if needed 
        int  saveCount; 
   
        if  (!dirtyOpaque) { 
            final  Drawable background = mBackground; 
            if  (background !=  null ) { 
                final  int  scrollX = mScrollX; 
                final  int  scrollY = mScrollY; 
   
                if  (mBackgroundSizeChanged) { 
                    background.setBounds( 0 0 ,  mRight - mLeft, mBottom - mTop); 
                    mBackgroundSizeChanged =  false
               
   
                if  ((scrollX | scrollY) ==  0 ) { 
                    background.draw(canvas); 
                else 
                    canvas.translate(scrollX, scrollY); 
                    background.draw(canvas); 
                    canvas.translate(-scrollX, -scrollY); 
               
           
       
   
        // skip step 2 & 5 if possible (common case) 
        final  int  viewFlags = mViewFlags; 
        boolean  horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) !=  0
        boolean  verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) !=  0
        if  (!verticalEdges && !horizontalEdges) { 
            // Step 3, draw the content 
            if  (!dirtyOpaque) onDraw(canvas); 
   
            // Step 4, draw the children 
            dispatchDraw(canvas); 
   
            // Step 6, draw decorations (scrollbars) 
            onDrawScrollBars(canvas); 
   
            // we're done... 
            return
       
   
        ...... 
        ...... 
        ......

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

1
2
3
4
5
6
7
8
9
/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */ 
    protected  void  dispatchDraw(Canvas canvas) { 
   
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@Override 
protected  void  dispatchDraw(Canvas canvas) { 
     final  int  count = mChildrenCount; 
     final  View[] children = mChildren; 
     int  flags = mGroupFlags; 
   
     if  ((flags & FLAG_RUN_ANIMATION) !=  0  && canAnimate()) { 
         final  boolean  cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; 
   
         final  boolean  buildCache = !isHardwareAccelerated(); 
         for  ( int  i =  0 ; i < count; i++) { 
             final  View child = children[i]; 
             if  ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { 
                 final  LayoutParams params = child.getLayoutParams(); 
                 attachLayoutAnimationParameters(child, params, i, count); 
                 bindLayoutAnimation(child); 
                 if  (cache) { 
                     child.setDrawingCacheEnabled( true ); 
                     if  (buildCache) {                         
                         child.buildDrawingCache( true ); 
                    
                
            
        
   
         final  LayoutAnimationController controller = mLayoutAnimationController; 
         if  (controller.willOverlap()) { 
             mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; 
        
   
         controller.start(); 
   
         mGroupFlags &= ~FLAG_RUN_ANIMATION; 
         mGroupFlags &= ~FLAG_ANIMATION_DONE; 
   
         if  (cache) { 
             mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; 
        
   
         if  (mAnimationListener !=  null ) { 
             mAnimationListener.onAnimationStart(controller.getAnimation()); 
        
    
   
     int  saveCount =  0
     final  boolean  clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; 
     if  (clipToPadding) { 
         saveCount = canvas.save(); 
         canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, 
                 mScrollX + mRight - mLeft - mPaddingRight, 
                 mScrollY + mBottom - mTop - mPaddingBottom); 
   
    
   
     // We will draw our child's animation, let's reset the flag 
     mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; 
     mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; 
   
     boolean  more =  false
     final  long  drawingTime = getDrawingTime(); 
   
     if  ((flags & FLAG_USE_CHILD_DRAWING_ORDER) ==  0 ) { 
         for  ( int  i =  0 ; i < count; i++) { 
             final  View child = children[i]; 
             if  ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() !=  null ) { 
                 more |= drawChild(canvas, child, drawingTime); 
            
        
     else 
         for  ( int  i =  0 ; i < count; i++) { 
             final  View child = children[getChildDrawingOrder(count, i)]; 
             if  ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() !=  null ) { 
                 more |= drawChild(canvas, child, drawingTime); 
            
        
    
   
     // Draw any disappearing views that have animations 
     if  (mDisappearingChildren !=  null ) { 
         final  ArrayList<View> disappearingChildren = mDisappearingChildren; 
         final  int  disappearingCount = disappearingChildren.size() -  1
         // Go backwards -- we may delete as animations finish 
         for  ( int  i = disappearingCount; i >=  0 ; i--) { 
             final  View child = disappearingChildren.get(i); 
             more |= drawChild(canvas, child, drawingTime); 
        
    
   
     if  (debugDraw()) { 
         onDebugDraw(canvas); 
    
   
     if  (clipToPadding) { 
         canvas.restoreToCount(saveCount); 
    
   
     // mGroupFlags might have been updated by drawChild() 
     flags = mGroupFlags; 
   
     if  ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { 
         invalidate( true ); 
    
   
     if  ((flags & FLAG_ANIMATION_DONE) ==  0  && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) ==  0  && 
             mLayoutAnimationController.isDone() && !more) { 
         // We want to erase the drawing cache and notify the listener after the 
         // next frame is drawn because one extra invalidate() is caused by 
         // drawChild() after the animation is over 
         mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; 
         final  Runnable end =  new  Runnable() { 
            public  void  run() { 
                notifyAnimationListener(); 
           
         }; 
         post(end); 
    
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected  boolean  drawChild(Canvas canvas, View child,  long  drawingTime) { 
     ...... 
     ...... 
   
     if  (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && 
                 (child.mPrivateFlags & DRAW_ANIMATION) ==  0 ) { 
             return  more; 
        
   
         child.computeScroll(); 
   
         final  int  sx = child.mScrollX; 
         final  int  sy = child.mScrollY; 
   
         boolean  scalingRequired =  false
         Bitmap cache =  null
   
     ...... 
     ...... 
   
}

  只截取了部分代码,看到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类的使用,但愿你们到时候关注下,有疑问的同窗在下面留言,我会为你们解答的!

 

很荣幸我可以成为CSDN 2013年度博客之星评选的候选人,但愿继续获得你们的支持与鼓励,看到的朋友帮我投上宝贵的一票吧!

  投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/xiaanming

相关文章
相关标签/搜索