Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Scroller大揭秘

Android Scroller大揭秘

編輯:關於Android編程

在學習使用Scroller之前,需要明白scrollTo()、scrollBy()方法。

一、View的scrollTo()、scrollBy()

scrollTo、scrollBy方法是View中的,因此任何的View都可以通過這兩種方法進行移動。首先要明白的是,scrollTo、scrollBy滑動的是View中的內容(而且還是整體滑動),而不是View本身。我們的滑動控件如SrollView可以限定寬、高大小,以及在布局中的位置,但是滑動控件中的內容(或者裡面的childView)可以是無限長、寬的,我們調用View的scrollTo、scrollBy方法,相當於是移動滑動控件中的畫布Canvas,然後進行重繪,屏幕上也就顯示相應的內容。如下: \ 1、getScrollX()、getScrollY() 在學習scrollTo()、scrollBy()之前,先來了解一下getScrollX()、getScrollY()方法。 getScrollX()、getScrollY()得到的是偏移量,是相對自己初始位置的滑動偏移距離,只有當有scroll事件發生時,這兩個方法才能有值,否則getScrollX()、getScrollY()都是初始時的值0,而不管你這個滑動控件在哪裡。所謂自己初始位置是指,控件在剛開始顯示時、沒有滑動前的位置。以getScrollX()為例,其源碼如下:
public final int getScrollX() {
    return mScrollX;
}
可以看到getScrollX()直接返回的就是mScrollX,代表水平方向上的偏移量,getScrollY()也類似。偏移量mScrollX的正、負代表著,滑動控件中的內容相對於初始位置在水平方向上偏移情況,mScrollX為正代表著當前內容相對於初始位置向左偏移了mScrollX的距離,mScrollX為負表示當前內容相對於初始位置向右偏移了mScrollX的距離。 這裡的坐標系和我們平常的認知正好相反。為了以後更方便的處理滑動相關坐標和偏移,在處理偏移、滑動相關的功能時,我們就可以把坐標反過來看,如下圖: \   因為滑動控件中的內容是整體進行滑動的,同時也是相對於自己顯示時的初始位置的偏移,對於View中內容在偏移時的參考坐標原點(注意是內容視圖的坐標原點,不是圖中說的滑動控件的原點),可以選擇初始位置的某一個地方,因為滑動時整體行為,在進行滑動的時候從這個選擇的原點出進行分析即可。   2、scrollTo()、scrollBy() scrollTo(int x,int y)移動的是View中的內容,而滑動控件中的內容都是整體移動的,scrollTo(int x,int y)中的參數表示View中的內容要相對於內容初始位置移動x和y的距離,即將內容移動到距離內容初始位置x和y的位置。正如前面所說,在處理偏移、滑動問題時坐標系和平常認知的坐標系是相反的。以一個例子說明scrollTo():   說明:圖中黃色矩形區域表示的是一個可滑動的View控件,綠色虛線矩形為滑動控件中的滑動內容。注意這裡的坐標是相反的。(例子來源於:http://blog.csdn.net/bigconvience/article/details/26697645)   (1)調用scrollTo(100,0)表示將View中的內容移動到距離內容初始顯示位置的x=100,y=0的地方,效果如下圖: \ \ (2)調用scrollTo(0,100)效果如下圖: \ \ (3)調用scrollTo(100,100)效果如下圖: \ \ (4)調用scrollTo(-100,0)效果如下圖: \ \ 通過上面幾個圖,可以清楚看到scrollTo的作用和滑動坐標系的關系。在實際使用中,我們一般是在onTouchEvent()方法中處理滑動事件,在MotionEvent.ACTION_MOVE時調用scrollTo(int x,int y)進行滑動,在調用scrollTo(int x,int y)前,我們先要計算出兩個參數值,即水平和垂直方向需要滑動的距離,如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
	int y = (int) event.getY();
    int action = event.getAction();
    switch (action){
	case MotionEvent.ACTION_DOWN:
			mLastY = y;
            break;
    case MotionEvent.ACTION_MOVE:
			int dy = mLastY - y;//本次手勢滑動了多大距離
            int oldScrollY = getScrollY();//先計算之前已經偏移了多少距離
            int scrollY = oldScrollY + dy;//本次需要偏移的距離=之前已經偏移的距離+本次手勢滑動了多大距離
            if(scrollY < 0){
                scrollY = 0;
			}
			if(scrollY > getHeight() - mScreenHeight){
                scrollY = getHeight() - mScreenHeight;
			}
            scrollTo(getScrollX(),scrollY);
			mLastY = y;
            break;
	}
	return true;
}
上面在計算參數時,分為了三步。第一是,通過int dy = mLastY - y;得到本次手勢在屏幕上滑動了多少距離,這裡要特別注意這個相減順序,因為這裡的坐標與平常是相反的,因此,手勢滑動距離是按下時的坐標mLastY - 當前的坐標y;第二是,通過oldScrollY = getScrollY();獲得滑動內容之前已經距初始位置便宜了多少;第三是,計算本次需要偏移的參數int scrollY = oldScrollY + dy; 後面通過兩個if條件進行了邊界處理,然後調用scrollTo進行滑動。調用完scrollTo後,新的偏移量又重新產生了。從scrollTo源碼中可以看到:
public void scrollTo(int x, int y) {
		if (mScrollX != x || mScrollY != y) {
			int oldX = mScrollX;
			int oldY = mScrollY;
			mScrollX = x;//賦值新的x偏移量
			mScrollY = y;//賦值新的y偏移量
			invalidateParentCaches();
			onScrollChanged(mScrollX, mScrollY, oldX, oldY);
	        if (!awakenScrollBars()) {
	            postInvalidateOnAnimation();
			}
		}
	}
scrollTo是相對於初始位置來進行移動的,而scrollBy(int x ,int y)則是相對於上一次移動的距離來進行本次移動。scrollBy其實還是依賴於scrollTo的,如下源碼:
public void scrollBy(int x, int y) {
	    scrollTo(mScrollX + x, mScrollY + y);
	}
可以看到,使用scrollBy其實就是省略了我們在計算scrollTo參數時的第三步而已,因為scrollBy內部已經自己幫我加上了第三步的計算。因此scrollBy的作用就是相當於在上一次的偏移情況下進行本次的偏移。   一個完整的水平方向滑動的例子:
public class MyViewPager extends ViewGroup {

    private int mLastX;

    public MyViewPager(Context context) {
        super(context);
        init(context);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for(int i = 0; i < count; i++){
            View child = getChildAt(i);
            child.measure(widthMeasureSpec,heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        Log.d("TAG","--l-->"+l+",--t-->"+t+",-->r-->"+r+",--b-->"+b);
        for(int i = 0; i < count; i++){
            View child = getChildAt(i);
            child.layout(i * getWidth(), t, (i+1) * getWidth(), b);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = mLastX - x;
                int oldScrollX = getScrollX();//原來的偏移量
                int preScrollX = oldScrollX + dx;//本次滑動後形成的偏移量
                if(preScrollX > (getChildCount() - 1) * getWidth()){
                    preScrollX = (getChildCount() - 1) * getWidth();
                }
                if(preScrollX < 0){
                    preScrollX = 0;
                }
                scrollTo(preScrollX,getScrollY());
                mLastX = x;
                break;
        }
        return true;
    }
}
布局文件:



    
        

        

        

        
    
效果如圖: \  

二、Scroller滑動輔助類

根據我們上面的分析,可知View的scrollTo()、scrollBy()是瞬間完成的,當我們的手指在屏幕上移動時,內容會跟著手指滑動,但是當我們手指一抬起時,滑動就會停止,如果我們想要有一種慣性的滾動過程效果和回彈效果,此時就需要使用Scroller輔助類。 但是注意的是,Scroller本身不會去移動View,它只是一個移動計算輔助類,用於跟蹤控件滑動的軌跡,只相當於一個滾動軌跡記錄工具,最終還是通過View的scrollTo、scrollBy方法完成View的移動的。 在使用Scroller類之前,先了解其重要的兩個方法: (1)startScroll()
public void startScroll(int startX, int startY, int dx, int dy, int duration)
開始一個動畫控制,由(startX,startY)在duration時間內前進(dx,dy)個單位,即到達偏移坐標為(startX+dx,startY+dy)處。 (2)computeScrollOffset()
public boolean computeScrollOffset()
滑動過程中,根據當前已經消逝的時間計算當前偏移的坐標點,保存在mCurrX和mCurrY值中。 上面兩個方法的源碼如下:
public class Scroller {
private int mStartX;//水平方向,滑動時的起點偏移坐標
private int mStartY;//垂直方向,滑動時的起點偏移坐標
private int mFinalX;//滑動完成後的偏移坐標,水平方向
private int mFinalY;//滑動完成後的偏移坐標,垂直方向

private int mCurrX;//滑動過程中,根據消耗的時間計算出的當前的滑動偏移距離,水平方向
private int mCurrY;//滑動過程中,根據消耗的時間計算出的當前的滑動偏移距離,垂直方向
private int mDuration; //本次滑動的動畫時間
private float mDeltaX;//滑動過程中,在達到mFinalX前還需要滑動的距離,水平方向
private float mDeltaY;//滑動過程中,在達到mFinalX前還需要滑動的距離,垂直方向

public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}

/**
     * 開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,即到達偏移坐標為(startX+dx , startY+dy)處
*/
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;
}

/**
     * 滑動過程中,根據當前已經消逝的時間計算當前偏移的坐標點,保存在mCurrX和mCurrY值中
     * @return
*/
public boolean computeScrollOffset() {
	if (mFinished) {//已經完成了本次動畫控制,直接返回為false
		return false;
	}
	int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
        if (timePassed < mDuration) {
		switch (mMode) {
		case SCROLL_MODE:
		final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
		mCurrX = mStartX + Math.round(x * mDeltaX);//計算出當前的滑動偏移位置,x軸
		mCurrY = mStartY + Math.round(x * mDeltaY);//計算出當前的滑動偏移位置,y軸
		break;
		...
            }
        }else {
		mCurrX = mFinalX;
		mCurrY = mFinalY;
		mFinished = true;
	}
	return true;
	}
    ...
}
Scroller類中最重要的兩個方法就是startScroll()和computeScrollOffset(),但是Scroller類只是一個滑動計算輔助類,它的startScroll()和computeScrollOffset()方法中也只是對一些軌跡參數進行設置和計算,真正需要進行滑動還是得通過View的scrollTo()、scrollBy()方法。為此,View中提供了computeScroll()方法來控制這個滑動流程。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. 
 * 由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪制  
 */
public void computeScroll() { //空方法 ,自定義滑動功能的ViewGroup必須實現方法體  
      
} 
因此Scroller類的基本使用流程可以總結如下: (1)首先通過Scroller類的startScroll()開始一個滑動動畫控制,裡面進行了一些軌跡參數的設置和計算; (2)在調用startScroll()的後面調用invalidate();引起視圖的重繪操作,從而觸發ViewGroup中的computeScroll()被調用; (3)在computeScroll()方法中,先調用Scroller類中的computeScrollOffset()方法,裡面根據當前消耗時間進行軌跡坐標的計算,然後取得計算出的當前滑動的偏移坐標,調用View的scrollTo()方法進行滑動控制,最後也需要調用invalidate();進行重繪。 如下的一個簡單代碼示例:
 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);
        int x = (int) ev.getX();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                mLastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = mLastX - x;
                int oldScrollX = getScrollX();//原來的偏移量
                int preScrollX = oldScrollX + dx;//本次滑動後形成的偏移量
                if(preScrollX > (getChildCount() - 1) * getWidth()){
                    preScrollX = (getChildCount() - 1) * getWidth();
                }
                if(preScrollX < 0){
                    preScrollX = 0;
                }
                //開始滑動動畫    
                mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,0);//第一步
                //注意,一定要進行invalidate刷新界面,觸發computeScroll()方法,因為單純的startScroll()是屬於Scroller的,只是一個輔助類,並不會觸發界面的繪制
                invalidate();
                mLastX = x;
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//第二步
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//第三步
            invalidate();
        }
    }
  下面是一個完整的例子:一個類似ViewPager的Demo,效果圖如下: \ 代碼如下:
public class MyViewPager3 extends ViewGroup {

    private int mLastX;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    private int mTouchSlop;
    private int mMaxVelocity;
    /**
     * 當前顯示的是第幾個屏幕
     */
    private int mCurrentPage = 0;

    public MyViewPager3(Context context) {
        super(context);
        init(context);
    }

    public MyViewPager3(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MyViewPager3(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mScroller = new Scroller(context);
        ViewConfiguration config = ViewConfiguration.get(context);
        mTouchSlop = config.getScaledPagingTouchSlop();
        mMaxVelocity = config.getScaledMinimumFlingVelocity();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for(int i = 0; i < count; i++){
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        Log.d("TAG","--l-->"+l+",--t-->"+t+",-->r-->"+r+",--b-->"+b);
        for(int i = 0; i < count; i++){
            View child = getChildAt(i);
            child.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);
        int x = (int) ev.getX();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                mLastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = mLastX - x;
                /* 注釋的裡面是使用startScroll()來進行滑動的
                int oldScrollX = getScrollX();//原來的偏移量
                int preScrollX = oldScrollX + dx;//本次滑動後形成的偏移量
                if (preScrollX > (getChildCount() - 1) * getWidth()) {
                    preScrollX = (getChildCount() - 1) * getWidth();
                    dx = preScrollX - oldScrollX;
                }
                if (preScrollX < 0) {
                    preScrollX = 0;
                    dx = preScrollX - oldScrollX;
                }
                mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, 0);
                //注意,使用startScroll後面一定要進行invalidate刷新界面,觸發computeScroll()方法,因為單純的startScroll()是屬於Scroller的,只是一個輔助類,並不會觸發界面的繪制
                invalidate();
                */
                //但是一般在ACTION_MOVE中我們直接使用scrollTo或者scrollBy更加方便
                scrollBy(dx,0);
                mLastX = x;
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000);
                int initVelocity = (int) velocityTracker.getXVelocity();
                if(initVelocity > mMaxVelocity && mCurrentPage > 0){//如果是快速的向右滑,則需要顯示上一個屏幕
                    Log.d("TAG","----------------快速的向右滑--------------------");
                    scrollToPage(mCurrentPage - 1);
                }else if(initVelocity < -mMaxVelocity && mCurrentPage < (getChildCount() - 1)){//如果是快速向左滑動,則需要顯示下一個屏幕
                    Log.d("TAG","----------------快速的向左滑--------------------");
                    scrollToPage(mCurrentPage + 1);
                }else{//不是快速滑動的情況,此時需要計算是滑動到
                    Log.d("TAG","----------------慢慢的滑動--------------------");
                    slowScrollToPage();
                }
                recycleVelocityTracker();
                break;
        }
        return true;
    }

    /**
     * 緩慢滑動抬起手指的情形,需要判斷是停留在本Page還是往前、往後滑動
     */
    private void slowScrollToPage() {
        //當前的偏移位置
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        //判斷是停留在本Page還是往前一個page滑動或者是往後一個page滑動
        int whichPage = (getScrollX() + getWidth() / 2 ) / getWidth() ;
        scrollToPage(whichPage);
    }

    /**
     * 滑動到指定屏幕
     * @param indexPage
     */
    private void scrollToPage(int indexPage) {
        mCurrentPage = indexPage;
        if(mCurrentPage > getChildCount() - 1){
            mCurrentPage = getChildCount() - 1;
        }
        //計算滑動到指定Page還需要滑動的距離
        int dx = mCurrentPage * getWidth() - getScrollX();
        mScroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx) * 2);//動畫時間設置為Math.abs(dx) * 2 ms
        //記住,使用Scroller類需要手動invalidate
        invalidate();
    }

    @Override
    public void computeScroll() {
        Log.d("TAG", "---------computeScrollcomputeScrollcomputeScroll--------------");
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private void initVelocityTrackerIfNotExists() {
        if(mVelocityTracker == null){
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

}
布局文件如下:

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

            android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#999" >
                    android:layout_width="300dp"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/test1" />

                    android:layout_width="300dp"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/test2" />

                    android:layout_width="300dp"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/test3" />

                    android:layout_width="300dp"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/test4" />
    

    簡單總結: (1)Scroller類能夠幫助我們實現高級的滑動功能,如手指抬起後的慣性滑動功能。使用流程為,首先通過Scroller類的startScroll()+invalidate()觸發View的computeScroll(),在computeScroll()中讓Scroller類去計算最新的坐標信息,拿到最新的坐標偏移信息後還是要調用View的scrollTo來實現滑動。可以看到,使用Scroller的整個流程比較簡單,關鍵的是控制滑動的一些邏輯計算,比如上面例子中的計算什麼時候該往哪一頁滑動... (2)Android後面推出了OverScroller類,OverScroller在整體功能上和Scroller類似,使用也相同。OverScroller類可以完全代替Scroller,相比Scroller,OverScroller主要是增加了對滑動到邊界的一些控制,如增加一些回彈效果等,功能更加強大。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved