天天看點

Android中滑屏實作----手把手教你如何實作觸摸滑屏以及Scroller類詳解 如何實作觸摸滑屏?   知識點介紹:              

        前言:  雖然本文标題的有點标題黨的感覺,但無論如何,通過這篇文章的學習以及你自己的實踐認知,寫個簡單的滑屏小

   Demo還是just so so的。

        友情提示:

            在繼續往下面讀之前,希望您對以下知識點有一定程度掌握,否則,繼續看下去對您意義也不大。

             1、掌握View(視圖)的"視圖坐标"以及"布局坐标",以及scrollTo()和scrollBy()方法的作用 ----- 必須了解

                      如果對這方面知識不太清楚的話,建議先看看我的這篇部落格

                   不誇張地說,這篇部落格理論上來說是我們這篇博文的基礎。

             2、知道onInterceptTouchEvent()以及onTouchEvent()對觸摸事件的分發流程         ---- 不是必須

             3、知道怎麼繪制自定義ViewGroup即可                                        ---- 不是必須

     OK。 繼續往下看,請一定有所準備 。大家跟着我一步一步來咯。

scrollBy()方法的作用,這兩個方法的主要作用是将View/ViewGroup移至指定的坐标中,并且将偏移量儲存起來。另外:

                  mScrollX 代表X軸方向的偏移坐标

                  mScrollY 代表Y軸方向的偏移坐标

          關于偏移量的設定我們可以參看下源碼:

package com.qin.customviewgroup;  

public class View {  

    ....  

    protected int mScrollX;   //該視圖内容相當于視圖起始坐标的偏移量   , X軸 方向      

    protected int mScrollY;   //該視圖内容相當于視圖起始坐标的偏移量   , Y軸方向  

    //傳回值  

    public final int getScrollX() {  

        return mScrollX;  

    }  

    public final int getScrollY() {  

        return mScrollY;  

    public void scrollTo(int x, int y) {  

        //偏移位置發生了改變  

        if (mScrollX != x || mScrollY != y) {  

            int oldX = mScrollX;  

            int oldY = mScrollY;  

            mScrollX = x;  //賦新值,儲存目前便宜量  

            mScrollY = y;  

            //回調onScrollChanged方法  

            onScrollChanged(mScrollX, mScrollY, oldX, oldY);  

            if (!awakenScrollBars()) {  

                invalidate();  //一般都引起重繪  

            }  

        }  

    // 看出原因了吧 。。 mScrollX 與 mScrollY 代表我們目前偏移的位置 , 在目前位置繼續偏移(x ,y)個機關  

    public void scrollBy(int x, int y) {  

        scrollTo(mScrollX + x, mScrollY + y);  

    //...  

}  

     于是,在任何時刻我們都可以擷取該View/ViewGroup的偏移位置了,即調用getScrollX()方法和getScrollY()方法

         在初次看Launcher滑屏的時候,我就對Scroller類的學習感到非常蛋疼,完全失去了繼續研究的欲望。如今,沒得辦法,

  得重新看Launcher子產品,基本上将Launcher大部分類以及功能給掌握了。當然,也花了一天時間來學習Launcher裡的滑屏實作

 ,基本上業是撥開雲霧見真知了。

       我們知道想把一個View偏移至指定坐标(x,y)處,利用scrollTo()方法直接調用就OK了,但我們不能忽視的是,該方法本身

   來的的副作用:非常迅速的将View/ViewGroup偏移至目标點,而沒有對這個偏移過程有任何控制,對使用者而言可能是不太

   友好的。于是,基于這種偏移控制,Scroller類被設計出來了,該類的主要作用是為偏移過程制定一定的控制流程(後面我們會

   知道的更多),進而使偏移更流暢,更完美。

     可能上面說的比較懸乎,道理也沒有講透。下面我就根據特定情景幫助大家分析下:

        情景: 從上海如何到武漢?

            普通的人可能會想,so easy : 飛機、輪船、11路公共汽車...

            文藝的人可能會想,  小 case : 時空忍術(火影的招數)、翻個筋鬥(孫大聖的招數)...

     不管怎麼樣,我們想出來的套路可能有兩種:

               1、有個時間控制過程才能抵達(緩慢的前進)                              -----     對應于Scroller的作用

                      假設做火車,這個過程可能包括: 火車速率,花費周期等;

               2、瞬間抵達(超神太快了,都眩暈了,使用者體驗不太好)                     ------   對應于scrollTo()的作用

    模拟Scroller類的實作功能:

        假設從上海做動車到武漢需要10個小時,行進距離為1000km ,火車速率200/h 。采用第一種時間控制方法到達武漢的

   整個配合過程可能如下:

        我們每隔一段時間(例如1小時),計算火車應該行進的距離,然後調用scrollTo()方法,行進至該處。10小時過完後,

    我們也就達到了目的地了。

    相信大家心裡應該有個感覺了。我們就分析下源碼裡去看看Scroller類的相關方法.

     其源代碼(部分)如下: 路徑位于 \frameworks\base\core\java\android\widget\Scroller.java

public class Scroller  {  

    private int mStartX;    //起始坐标點 ,  X軸方向  

    private int mStartY;    //起始坐标點 ,  Y軸方向  

    private int mCurrX;     //目前坐标點  X軸, 即調用startScroll函數後,經過一定時間所達到的值  

    private int mCurrY;     //目前坐标點  Y軸, 即調用startScroll函數後,經過一定時間所達到的值  

    private float mDeltaX;  //應該繼續滑動的距離, X軸方向  

    private float mDeltaY;  //應該繼續滑動的距離, Y軸方向  

    private boolean mFinished;  //是否已經完成本次滑動操作, 如果完成則為 true  

    //構造函數  

    public Scroller(Context context) {  

        this(context, null);  

    public final boolean isFinished() {  

        return mFinished;  

    //強制結束本次滑屏操作  

    public final void forceFinished(boolean finished) {  

        mFinished = finished;  

    public final int getCurrX() {  

        return mCurrX;  

     /* 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. */    

    //根據目前已經消逝的時間計算目前的坐标點,儲存在mCurrX和mCurrY值中  

    public boolean computeScrollOffset() {  

        if (mFinished) {  //已經完成了本次動畫控制,直接傳回為false  

            return false;  

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);  

        if (timePassed < mDuration) {  

            switch (mMode) {  

            case SCROLL_MODE:  

                float x = (float)timePassed * mDurationReciprocal;  

                ...  

                mCurrX = mStartX + Math.round(x * mDeltaX);  

                mCurrY = mStartY + Math.round(x * mDeltaY);  

                break;  

            ...  

        else {  

            mCurrX = mFinalX;  

            mCurrY = mFinalY;  

            mFinished = true;  

        return true;  

    //開始一個動畫控制,由(startX , startY)在duration時間内前進(dx,dy)個機關,即到達坐标為(startX+dx , startY+dy)出  

    public void startScroll(int startX, int startY, int dx, int dy, int duration) {  

        mFinished = false;  

        mDuration = duration;  

        mStartTime = AnimationUtils.currentAnimationTimeMillis();  

        mStartX = startX;       mStartY = startY;  

        mFinalX = startX + dx;  mFinalY = startY + dy;  

        mDeltaX = dx;            mDeltaY = dy;  

        ...  

     其中比較重要的兩個方法為:

            public void startScroll(int startX, int startY, int dx, int dy, int duration)

                   函數功能說明:根據目前已經消逝的時間計算目前的坐标點,儲存在mCurrX和mCurrY值中

                  函數功能說明:開始一個動畫控制,由(startX , startY)在duration時間内前進(dx,dy)個機關,到達坐标為

                      (startX+dx , startY+dy)處。

        PS : 強烈建議大家看看該類的源碼,便于後續了解。

       為了易于控制滑屏控制,Android架構提供了 computeScroll()方法去控制這個流程。在繪制View時,會在draw()過程調用該

  方法。是以, 再配合使用Scroller執行個體,我們就可以獲得目前應該的偏移坐标,手動使View/ViewGroup偏移至該處。

     computeScroll()方法原型如下,該方法位于ViewGroup.java類中      

/** 

     * 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必須實作方法體  

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

     其調用過程位于View繪制流程draw()過程中,如下:

@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();  

   Demo說明:

           我們簡單的複用了之前寫的一個自定義ViewGroup,與以前一次有差別的是,我們沒有調用scrollTo()方法去進行瞬間

       偏移。 本次做法如下:

                   第一、調用Scroller執行個體去産生一個偏移控制(對應于startScroll()方法)

                   第二、手動調用invalid()方法去重新繪制,剩下的就是在 computeScroll()裡根據目前已經逝去的時間,擷取目前

                       應該偏移的坐标(由Scroller執行個體對應的computeScrollOffset()計算而得),

                   第三、目前應該偏移的坐标,調用scrollBy()方法去緩慢移動至該坐标處。

  截圖如下:

                                         原始界面                                     點選按鈕或者觸摸屏之後的顯示界面

        附:由于滑動截屏很難,隻是簡單的截取了兩個個靜态圖檔,觸摸的話可以實作左右滑動切屏了。

           更多知識點,請看代碼注釋。。

//自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換  

public class MultiViewGroup extends ViewGroup {  

    //startScroll開始移至下一屏  

    public void startMove(){  

        curScreen ++ ;  

        Log.i(TAG, "----startMove---- curScreen " + curScreen);  

        //使用動畫控制偏移過程 , 3s内到位  

        mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000);  

        //其實點選按鈕的時候,系統會自動重新繪制View,我們還是手動加上吧。  

        invalidate();  

        //使用scrollTo一步到位  

        //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0);  

    // 由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪制  

    @Override  

    public void computeScroll() {     

        // TODO Auto-generated method stub  

        Log.e(TAG, "computeScroll");  

        // 如果傳回true,表示動畫還沒有結束  

        // 因為前面startScroll,是以隻有在startScroll完成時 才會為false  

        if (mScroller.computeScrollOffset()) {  

            Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());  

            // 産生了動畫效果,根據目前值 每次滾動一點  

            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  

            Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());  

            //此時同樣也需要重新整理View ,否則效果可能有誤差  

            postInvalidate();  

        else  

            Log.i(TAG, "have done the scoller -----");  

    //馬上停止移動,如果已經超過了下一屏的一半,我們強制滑到下一個螢幕  

    public void stopMove(){  

        Log.v(TAG, "----stopMove ----");  

        if(mScroller != null){  

            //如果動畫還沒結束,我們就按下了結束的按鈕,那我們就結束該動畫,即馬上滑動指定位置  

            if(!mScroller.isFinished()){  

                int scrollCurX= mScroller.getCurrX() ;  

                //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原螢幕  

                // 這樣的一個簡單公式意思是:假設目前滑屏偏移值即 scrollCurX 加上每個螢幕一半的寬度,除以每個螢幕的寬度就是  

                //  我們目标屏所在位置了。 假如每個螢幕寬度為320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏  

                //即(500 + 320/2)/320 = 2  

                int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ;  

                Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX);  

                Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen);  

                mScroller.abortAnimation();  

                //停止了動畫,我們馬上滑倒目标位置  

                scrollTo(descScreen *getWidth() , 0);  

                curScreen = descScreen ; //糾正目标屏位置  

            else  

                Log.i(TAG, "----OK mScroller.is  finished ---- ");  

        }     

      其實網上有很多關于Launcher實作滑屏的博文,基本上也把道理闡釋的比較明白了 。我這兒也是基于自己的了解,将一些

 重要方面的知識點給補充下,希望能幫助大家了解。

      想要實作滑屏操作,值得考慮的事情包括如下幾個方面:

        其中:onInterceptTouchEvent()主要功能是控制觸摸事件的分發,例如是子視圖的點選事件還是滑動事件。

        其他所有處理過程均在onTouchEvent()方法裡實作了。

            1、螢幕的滑動要根據手指的移動而移動  ---- 主要實作在onTouchEvent()方法中

            2、當手指松開時,可能我們并沒有完全滑動至某個螢幕上,這是我們需要手動判斷目前偏移至去計算目标屏(目前屏或者

               前後屏),并且優雅的偏移到目标屏(當然是用Scroller執行個體咯)。

           3、調用computeScroll ()去實作緩慢移動過程。

           功能:  根據觸摸位置計算每像素的移動速率。

           常用方法有:     

                     public void addMovement (MotionEvent ev)

                   功能:添加觸摸對象MotionEvent , 用于計算觸摸速率。   

                   功能:以每像素units機關考核移動速率。額,其實我也不太懂,賦予值1000即可。

                   參照源碼 該units的意思如下:

                           參數 units : The units you would like the velocity in.  A value of 1

                             provides pixels per millisecond, 1000 provides pixels per second, etc.

                           功能:獲得X軸方向的移動速率。

           功能: 獲得一些關于timeouts(時間)、sizes(大小)、distances(距離)的标準常量值 。

           常用方法:

                  public int getScaledEdgeSlop()

                      說明:獲得一個觸摸移動的最小像素值。也就是說,隻有超過了這個值,才代表我們該滑屏處理了。

                 public static int getLongPressTimeout()

                     說明:獲得一個執行長按事件監聽(onLongClickListener)的值。也就是說,對某個View按下觸摸時,隻有超過了

         這個時間值在,才表示我們該對該View回調長按事件了;否則,小于這個時間點松開手指,隻執行onClick監聽

        我能寫下來的也就這麼多了,更多的東西參考代碼注釋吧。 在掌握了上面我羅列的知識後(重點scrollTo、Scroller類),

    其他方面的知識都是關于點與點之間的計算了以及觸摸事件的分發了。這方面感覺也沒啥可寫的。

    private static String TAG = "MultiViewGroup";  

    private int curScreen = 0 ;  //目前螢幕  

    private Scroller mScroller = null ; //Scroller對象執行個體  

    public MultiViewGroup(Context context) {  

        super(context);  

        mContext = context;  

        init();  

    public MultiViewGroup(Context context, AttributeSet attrs) {  

        super(context, attrs);  

    //初始化  

    private void init() {     

        //初始化Scroller執行個體  

        mScroller = new Scroller(mContext);  

        // 初始化3個 LinearLayout控件  

        //初始化一個最小滑動距離  

        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();  

    //兩種狀态: 是否處于滑屏狀态  

    private static final int TOUCH_STATE_REST = 0;  //什麼都沒做的狀态  

    private static final int TOUCH_STATE_SCROLLING = 1;  //開始滑屏的狀态  

    private int mTouchState = TOUCH_STATE_REST; //預設是什麼都沒做的狀态  

    //--------------------------   

    //處理觸摸事件 ~  

    public static int  SNAP_VELOCITY = 600 ;  //最小的滑動速率  

    private int mTouchSlop = 0 ;              //最小滑動距離,超過了,才認為開始滑動  

    private float mLastionMotionX = 0 ;       //記住上次觸摸屏的位置  

    //處理觸摸的速率  

    private VelocityTracker mVelocityTracker = null ;  

    // 這個感覺沒什麼作用 不管true還是false 都是會執行onTouchEvent的 因為子view裡面onTouchEvent傳回false了  

    public boolean onInterceptTouchEvent(MotionEvent ev) {  

        Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);  

        final int action = ev.getAction();  

        //表示已經開始滑動了,不需要走該Action_MOVE方法了(第一次時可能調用)。  

        //該方法主要用于使用者快速松開手指,又快速按下的行為。此時認為是出于滑屏狀态的。  

        if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {  

            return true;  

        final float x = ev.getX();  

        final float y = ev.getY();  

        switch (action) {  

        case MotionEvent.ACTION_MOVE:  

            Log.e(TAG, "onInterceptTouchEvent move");  

            final int xDiff = (int) Math.abs(mLastionMotionX - x);  

            //超過了最小滑動距離,就可以認為開始滑動了  

            if (xDiff > mTouchSlop) {  

                mTouchState = TOUCH_STATE_SCROLLING;  

            break;  

        case MotionEvent.ACTION_DOWN:  

            Log.e(TAG, "onInterceptTouchEvent down");  

            mLastionMotionX = x;  

            mLastMotionY = y;  

            Log.e(TAG, mScroller.isFinished() + "");  

            mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;  

        case MotionEvent.ACTION_CANCEL:  

        case MotionEvent.ACTION_UP:  

            Log.e(TAG, "onInterceptTouchEvent up or cancel");  

            mTouchState = TOUCH_STATE_REST;  

        Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST);  

        return mTouchState != TOUCH_STATE_REST;  

    public boolean onTouchEvent(MotionEvent event){  

        super.onTouchEvent(event);  

        Log.i(TAG, "--- onTouchEvent--> " );  

        Log.e(TAG, "onTouchEvent start");  

        //獲得VelocityTracker對象,并且添加滑動對象  

        if (mVelocityTracker == null) {  

            mVelocityTracker = VelocityTracker.obtain();  

        mVelocityTracker.addMovement(event);  

        //觸摸點  

        float x = event.getX();  

        float y = event.getY();  

        switch(event.getAction()){  

            //如果螢幕的動畫還沒結束,你就按下了,我們就結束上一次動畫,即開始這次新ACTION_DOWN的動畫  

            if(mScroller != null){  

                if(!mScroller.isFinished()){  

                    mScroller.abortAnimation();   

                }  

            mLastionMotionX = x ; //記住開始落下的螢幕點  

            break ;  

            int detaX = (int)(mLastionMotionX - x ); //每次滑動螢幕,螢幕應該移動的距離  

            scrollBy(detaX, 0);//開始緩慢滑屏咯。 detaX > 0 向右滑動 , detaX < 0 向左滑動 ,  

            Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX );  

            mLastionMotionX = x ;  

            final VelocityTracker velocityTracker = mVelocityTracker  ;  

            velocityTracker.computeCurrentVelocity(1000);  

            //計算速率  

            int velocityX = (int) velocityTracker.getXVelocity() ;    

            Log.e(TAG , "---velocityX---" + velocityX);  

            //滑動速率達到了一個标準(快速向右滑屏,傳回上一個螢幕) 馬上進行切屏處理  

            if (velocityX > SNAP_VELOCITY && curScreen > 0) {  

                // Fling enough to move left  

                Log.e(TAG, "snap left");  

                snapToScreen(curScreen - 1);  

            //快速向左滑屏,傳回下一個螢幕)  

            else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){  

                Log.e(TAG, "snap right");  

                snapToScreen(curScreen + 1);  

            //以上為快速移動的 ,強制切換螢幕  

            else{  

                //我們是緩慢移動的,是以先判斷是保留在本螢幕還是到下一螢幕  

                snapToDestination();  

            //回收VelocityTracker對象  

            if (mVelocityTracker != null) {  

                mVelocityTracker.recycle();  

                mVelocityTracker = null;  

            //修正mTouchState值  

            mTouchState = TOUCH_STATE_REST ;  

        return true ;  

    ////我們是緩慢移動的,是以需要根據偏移值判斷目标屏是哪個?  

    private void snapToDestination(){  

        //目前的偏移位置  

        int scrollX = getScrollX() ;  

        int scrollY = getScrollY() ;  

        Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX);  

        //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原螢幕      

        //直接使用這個公式判斷是哪一個螢幕 前後或者自己  

        //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原螢幕  

        // 這樣的一個簡單公式意思是:假設目前滑屏偏移值即 scrollCurX 加上每個螢幕一半的寬度,除以每個螢幕的寬度就是  

        //  我們目标屏所在位置了。 假如每個螢幕寬度為320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏  

        int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ;  

        Log.e(TAG, "### onTouchEvent  ACTION_UP### dx destScreen " + destScreen);  

        snapToScreen(destScreen);  

    //真正的實作跳轉螢幕的方法  

    private void snapToScreen(int whichScreen){   

        //簡單的移到目标螢幕,可能是目前屏或者下一螢幕  

        //直接跳轉過去,不太友好  

        //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0);  

        //為了友好性,我們在增加一個動畫效果  

        //需要再次滑動的距離 屏或者下一螢幕的繼續滑動距離  

        curScreen = whichScreen ;  

        //防止螢幕越界,即超過螢幕數  

        if(curScreen > getChildCount() - 1)  

            curScreen = getChildCount() - 1 ;  

        //為了達到下一螢幕或者目前螢幕,我們需要繼續滑動的距離.根據dx值,可能想左滑動,也可能像又滑動  

        int dx = curScreen * getWidth() - getScrollX() ;  

        Log.e(TAG, "### onTouchEvent  ACTION_UP### dx is " + dx);  

        mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2);  

        //由于觸摸事件不會重新繪制View,是以此時需要手動重新整理View 否則沒效果  

    //開始滑動至下一屏  

        ...       

    //了解停止滑動  

    // measure過程  

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

       ...  

    // layout過程  

    protected void onLayout(boolean changed, int l, int t, int r, int b) {  

   最後,希望大家能多多實踐,最好能寫個小Demo出來。那樣才正在的掌握了所學所得。

   本文代碼示例下載下傳位址:

     最後, 請一定要自己實踐了解。否則,效果也不會很好。

本文轉自wanqi部落格園部落格,原文連結:http://www.cnblogs.com/wanqieddy/archive/2012/05/05/2484534.html如需轉載請自行聯系原作者

繼續閱讀