1. 程式人生 > >Android開發之Scroller的使用詳解

Android開發之Scroller的使用詳解

其實關於Scroller這篇文章我早就想寫了,起初是因為看Xlistview的原始碼,接觸到了Scroller的使用,之後又在網上讀了大量的文章,之後自己寫了一個demo,對scroller也算是有了自己的看法。

我學習的道路是曲折的,所以我要把我會的東西寫出來,說不定對別人就有用呢!

一、Scroller簡介

先通過原始碼裡的註釋看下Scroller是做什麼用的

/**
 * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
 * or {@link OverScroller}) to collect the data you need to produce a scrolling
 * animation—for example, in response to a fling gesture. Scrollers track
 * scroll 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 at a rate that will make the scrolling animation look smooth.</p>
 *
 * <p>Here is a simple example:</p>
 *
 * <pre> private Scroller mScroller = new Scroller(context);
 * ...
 * public void zoomIn() {
 *     // Revert any animation currently in progress
 *     mScroller.forceFinished(true);
 *     // Start scrolling by providing a starting point and
 *     // the distance to travel
 *     mScroller.startScroll(0, 0, 100, 0);
 *     // Invalidate to request a redraw
 *     invalidate();
 * }</pre>
 *
 * <p>To track the changing positions of the x/y coordinates, use
 * {@link #computeScrollOffset}. The method returns a boolean to indicate
 * whether the scroller is finished. If it isn't, it means that a fling or
 * programmatic pan operation is still in progress. You can use this method to
 * find the current offsets of the x and y coordinates, for example:</p>
 *
 * <pre>if (mScroller.computeScrollOffset()) {
 *     // Get current x and y positions
 *     int currX = mScroller.getCurrX();
 *     int currY = mScroller.getCurrY();
 *    ...
 * }</pre>
 */
我做個簡單的翻譯,Scroller這個類對滑動進行了封裝,你可以通過Scroller來收集一些用來執行滑動動畫的資料——舉個例子,為了響應fling手勢,Scrollers會跟隨時間來追蹤滑動的偏移,但是它們不會自動地在你的view上應用這些位置。獲取並且以一個能夠使滑動動畫看起來流暢的速度來使用位置,這些都是由你來完成的。

mScroller,forceFinished(true);//讓所有正在進行的動畫都恢復原狀,也就是停用所有動畫

mScroller.startScroll(0,0,100,0);//前兩個引數是起始座標點,後兩個分別是x,y軸要滑動的距離。

invalidate();//請求重新繪製介面,從而觸發滑動

使用computeScrollOffset方法來追蹤x/y軸的座標,如果這個方法返回false,表示滑動還沒有結束,我們可以通過這個方法來找到當前滑動的x和y的座標,就像這樣:

if(mScroller.computeScrollOffset()){

    //獲取當前x和y的座標

    int currX = mScroller.getCurrX();
    int currY = mScroller.getCurrY();

}

好了,大致就是說,Scoller並不是真的能開始滑動效果的,它只是能夠記錄滑動動畫所需要的實時資料,具體滑動動畫還是得由你自己來實現的。說白了,使用Scroller類是為了給做出一個流暢的動畫,而不是pia一下,還沒開始就已經結束了。

二、computeScroll()介紹

/**
     * 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() {
    }
這個方法是被parent呼叫的,child會根據需要來更新它的值,例如mScrollX和mScrollY,如果child在用Scroller做滾動動畫的話,這個方法通常會被執行。

為了實現偏移控制,一般自定義View/ViewGroup都需要過載該方法 。

接下來看看這個方法是怎麼呼叫的

invaliate()會導致介面重繪,上面說過invalidate方法會觸發滑動動畫。而有經驗的朋友都知道,invalidate方法其實會引起介面重繪,所以我們從介面繪製說起,View的繪製包括三個主要過程,分別是measure,layout和draw。下面我們看View類的draw方法原始碼:

public void draw(Canvas canvas) { 
  ... ...
  /*
  * 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) {
  drawBackground(canvas);
  }
  ... ...
  // Step 2, save the canvas' layers
  ... ...
  // Step 3, draw the content
  if (!dirtyOpaque) onDraw(canvas);
  // Step 4, draw the children
  dispatchDraw(canvas);
  // Step 5, draw the fade effect and restore layers
  ... ...
  }

draw方法將介面繪製分成了六步,其中第三步是呼叫onDraw去繪製內容,第四步則是呼叫dispatchDraw去繪製子檢視。我們再看下dispatchDraw方法:
 @Override  
    protected void dispatchDraw(Canvas canvas){  
        ...  
          
        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);  
            }  
        }  
    }
最後再看drawChild的原始碼
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
        ...  
        child.computeScroll();  
        ...  
    }  

可以看到,最終會呼叫child的computeScroll(),這裡有一點要注意一下:因為invaliate()引起的是整個view樹的重繪,而這裡的viewgroup也有parent,根據上面的描述,viewgroup的computeScroll()也會被呼叫。我們需要在computeScroll()中來進行實際的移動操作,就象這樣

public void computeScroll() {
        if (mScroller.computeScrollOffset()&&currentY!=mScroller.getCurrY()) {
            //真正的滑動
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            currentY=mScroller.getCurrY();
            postInvalidate();
        }
    }
這裡我來解釋一下:1、mScroller.computeScrollOffset()在滑動沒有結束的時候,返回的是true,結束以後返回false

2、currentY!=mScroller.getCurrY() currentY是記錄當前滑動的y軸偏移量,這裡做了一個判斷,不讓它做重複的被執行,這裡雖然重複執行對滑動動畫也沒影響,至於為什麼後面說到scrollto的時候會解釋

3、mScroller.getCurrX() 返回這次滑動中在x軸上的偏移量,mScroller.getCurrY()返回的則是y軸上的。

三、scrollTo介紹

 /**
     * 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;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

這裡先說下mScrollX和mScrollY兩個引數的意思。mScrollX和mScrollY指的是檢視內容相對於檢視原始起始座標的偏移量,mScrollX和mScrollY的預設值為0,因為預設是沒有偏移的。另外需注意偏移量的正負問題,因為是相對檢視起始座標的,所以如果你是向右偏移那麼mScrollX應該是負數,而向左偏移mScrollX為正數。舉個例子,比如你定義了一個ImageView,其左上角的座標為(100,80),此時mScrollX和mScrollY值都為0(沒有偏移),現在你要把該ImageView移到(120,100)處,也就是右下方,那麼你的mScrollX應該是100-120=-20,mScrollY應該是80-100=-20,這下你該明白了吧。

從該方法中我們可以看出,它先判斷傳進來的(x, y)值是否和View的X, Y偏移量相等,如果相等,就是說view已經在移動了(x,y),也就沒必要移動了,這裡就解釋了上面的那個為什麼重複執行對滑動動畫沒有影響的問題了。如果不相等,就呼叫onScrollChanged()方法

 /**
     * This is called in response to an internal scroll in this view (i.e., the
     * view scrolled its own contents). This is typically as a result of
     * {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
     * called.
     *
     * @param l Current horizontal scroll origin.
     * @param t Current vertical scroll origin.
     * @param oldl Previous horizontal scroll origin.
     * @param oldt Previous vertical scroll origin.
     */
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            postSendViewScrolledAccessibilityEventCallback();
        }

        mBackgroundSizeChanged = true;

        final AttachInfo ai = mAttachInfo;
        if (ai != null) {
            ai.mViewScrollChanged = true;
        }
    }
看他的註釋,說的是這個方法是用來響應view內部滑動的,有一點需要提出來,scrollTo方法,如果當前控制元件是view,滾動的是它本身的內容,如果是viewgroup,則移動的他的子view。

四、startScroll()方法

最後來說下startScroll()方法,

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;
    }
結合上面說的mScrollX和mScrollY這兩個引數在來看這個方法就很簡單了,

startX:開始x軸的偏移量,是相對於檢視原始座標的,如果偏移量是正數,就會把檢視瞬間滑動到左邊,然後把該點作為滑動的起始點

startY:開始y軸的偏移量,是相對於檢視原始座標的,如果偏移量是正數,就會把檢視瞬間滑動到上邊,然後把該點作為滑動的起始點

dx:x軸需要滑動的距離,以startX是起點,相對於startX的偏移量,如果是正數,就會相對於startX向左滑動

dy:y軸需要滑動的距離,以startY是起點,相對於startY的偏移量,如果是正數,就會相對於startY向上滑動

duration:滑動持續的時間,單位是毫秒,預設是250毫秒

好了,關於Scroller的差不多都說完了,最後留個DEMO,可以看下效果。有什麼問題,歡迎留言或者聯絡我,一起探討。