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

相關文章
相關標籤/搜索