1. 程式人生 > >Android自定義元件系列【2】——Scroller類

Android自定義元件系列【2】——Scroller類

在上一篇中介紹了View類的scrollTo和scrollBy兩個方法,對這兩個方法不太瞭解的朋友可以先看《自定義View及ViewGroup

scrollTo和scrollBy雖然實現了檢視的偏移,但是卻沒有更好的控制移動過程,移動是瞬間進行的。Scroller類就是為解決這個問題而設計的。

開啟Scroller的原始碼,可以看到startScroll方法:

    /**
     * Start scrolling by providing a starting point and the distance to travel.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    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;
    }
可以看到,這個方法的作用是將View從一個起始位置通過給定移動偏移量和時間執行一段動畫移動到目標位置。下面再來看一下View類提供的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() {
    }

為了易於控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個流程。在繪製View時,會在draw()過程呼叫該方法。為了實現偏移控制,一般自定義View/ViewGroup都需要過載該方法 。

@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);  
        }  
    }  
}  
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
    ...  
    child.computeScroll();  
    ...  
} 
下面我們來用《Android Scroller類的詳細分析》給我們提供的程式碼分析一下
package com.example.testscrollto;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Scroller;

public class MainActivity extends Activity {
	private static final String TAG = "TestScrollerActivity";
	LinearLayout lay1, lay2, lay0;
	private Scroller mScroller;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		mScroller = new Scroller(this);
		lay1 = new MyLinearLayout(this);
		lay2 = new MyLinearLayout(this);

		lay1.setBackgroundColor(this.getResources().getColor(
				android.R.color.darker_gray));
		lay2.setBackgroundColor(this.getResources().getColor(
				android.R.color.white));
		lay0 = new ContentLinearLayout(this);
		lay0.setOrientation(LinearLayout.VERTICAL);
		LinearLayout.LayoutParams p0 = new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.FILL_PARENT,
				LinearLayout.LayoutParams.FILL_PARENT);
		this.setContentView(lay0, p0);

		LinearLayout.LayoutParams p1 = new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.FILL_PARENT,
				LinearLayout.LayoutParams.FILL_PARENT);
		p1.weight = 1;
		lay0.addView(lay1, p1);
		
		LinearLayout.LayoutParams p2 = new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.FILL_PARENT,
				LinearLayout.LayoutParams.FILL_PARENT);
		p2.weight = 1;
		
		lay0.addView(lay2, p2);
		MyButton btn1 = new MyButton(this);
		MyButton btn2 = new MyButton(this);
		btn1.setText("btn in layout1");
		btn2.setText("btn in layout2");
		btn1.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				mScroller.startScroll(0, 0, -30, -30, 50);
			}
		});
		btn2.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				mScroller.startScroll(20, 20, -50, -50, 50);
			}
		});
		lay1.addView(btn1);
		lay2.addView(btn2);
	}

	class MyButton extends Button {
		public MyButton(Context ctx) {
			super(ctx);
		}

		@Override
		protected void onDraw(Canvas canvas) {
			super.onDraw(canvas);
			Log.d(TAG, this.toString() + " onDraw------");
		}
	}

	class MyLinearLayout extends LinearLayout {
		public MyLinearLayout(Context ctx) {
			super(ctx);
		}

		@Override
		/**
		 * 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() {
			Log.d(TAG, this.toString() + " computeScroll-----------");
			if (mScroller.computeScrollOffset())// 如果mScroller沒有呼叫startScroll,這裡將會返回false。
			{
				// 因為呼叫computeScroll函式的是MyLinearLayout例項,
				// 所以呼叫scrollTo移動的將是該例項的孩子,也就是MyButton例項
				scrollTo(mScroller.getCurrX(), 0);
				Log.d(TAG, "getCurrX = " + mScroller.getCurrX());

				// 繼續讓系統重繪
				getChildAt(0).invalidate();
			}
		}
	}

	class ContentLinearLayout extends LinearLayout {
		public ContentLinearLayout(Context ctx) {
			super(ctx);
		}

		@Override
		protected void dispatchDraw(Canvas canvas) {
			Log.d(TAG, "contentview dispatchDraw");
			super.dispatchDraw(canvas);
		}
	}
}


點選上面按鈕,log輸出如下:


當點選Button的時候背景發生了變化,就需要重繪,這時button呼叫invalidate方法請求重繪,這就是scroll動態效果的觸發源,Scroller物件的例項是一個封裝位置和速度的變數,startScroll()方法是對一些成員變數進行設定,設定的唯一效果是導致mScroller.computeScrollOffset()方法返回true.在button重繪的同時mScroller.startScroll()方法被呼叫,此時mScroller變數設定了有效的值。
接下來的過程就是View自上而下的繪製了。在繪製lay1的時候就會呼叫drawChild方法,這時候就會執行computeScrll()方法:

		public void computeScroll() {
			Log.d(TAG, this.toString() + " computeScroll-----------");
			if (mScroller.computeScrollOffset())// 如果mScroller沒有呼叫startScroll,這裡將會返回false。
			{
				// 因為呼叫computeScroll函式的是MyLinearLayout例項,
				// 所以呼叫scrollTo移動的將是該例項的孩子,也就是MyButton例項
				scrollTo(mScroller.getCurrX(), 0);
				Log.d(TAG, "getCurrX = " + mScroller.getCurrX());

				// 繼續讓系統重繪
				getChildAt(0).invalidate();
			}
		}
直到startScroll中設定的時間到了,mScroller.computeScrollOffset()才返回false.