Android的滑動分析以及各種實現

bkbv6005 7年前發布 | 14K 次閱讀 安卓開發 Android開發 移動開發

一、滑動效果的產生

滑動一個View,本質區別就是移動一個View。改變當前View所在的坐標,原理和動畫相似不斷改變坐標位置實現。實現View的滑動就必須監聽滑動的事件,并且根據事件傳入的坐標,動態且不斷改變View的坐標,從而實現View跟隨用戶觸摸的滑動而滑動。

(1)、Android的坐標系

Android中將屏幕最左上角的頂點作為Android坐標系的原點,從這個點向右是X軸正方向,從這個點向下是Y軸正方向,如下圖:

系統提供了getLocationOnScreen(int location[])這樣的方法來獲取Android坐標系中點的位置,即該視圖左上角在Android坐標系中的坐標。在觸控事件中使用getRawX()、getRawY()方法所獲得的坐標同樣是Android坐標系中的坐標。

(2)、視圖坐標系

Android中除了上面所說的這種坐標系之外,還有一個視圖坐標系,它描述了子視圖在父視圖中的位置關系。這兩種坐標系并不矛盾也不復雜,他們的作用是相互相成的。與Android坐標系類似,視圖坐標系同樣是以原點向右為X軸正方向,以原點向下為Y軸正方向,只不過在視圖坐標系中,原點不再是Android坐標系中的屏幕最左上角,而是以父視圖左上角為坐標原點,如下圖:

在觸控事件中,通過getX()、getY()所獲得的坐標系就是視圖坐標系中的坐標。

(3)、觸控事件——MotionEvent

觸控事件MotionEvent在用戶交互中,占著舉足輕重的地位。首先看看MotionEvent封裝的一些常用事件常量,定義了觸控事件的不同類型。

//單點觸摸按下動作 
public static final int ACTION_DOWN             = 0; 
 
//單點觸摸離開動作 
public static final int ACTION_UP               = 1; 
 
//觸摸點移動動作 
public static final int ACTION_MOVE             = 2; 
 
//觸摸動作取消 
public static final int ACTION_CANCEL           = 3; 
 
//觸摸動作超出邊界 
public static final int ACTION_OUTSIDE          = 4; 
 
//多點觸摸按下動作 
public static final int ACTION_POINTER_DOWN     = 5; 
 
//多點離開動作 
public static final int ACTION_POINTER_UP       = 6;  

通常情況會在onTouchEvent(MotionEvent event)方法中通過event.getAction()方法來獲取觸控事件的類型,并使用switch-case方法來進行篩選,這個代碼的模式基本固定:

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    //獲取當前輸入點的X、Y坐標(視圖坐標) 
    int x = (int) event.getX(); 
    int y = (int) event.getY(); 
    switch (event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
            //處理按下事件 
            break; 
        case MotionEvent.ACTION_MOVE: 
            //處理移動事件 
            break; 
        case MotionEvent.ACTION_UP: 
            //處理離開事件 
            break; 
    } 
    return true; 
}  

在不涉及多點操作的情況下,通常可以使用以上代碼來完成觸控事件的監聽。

在Android中系統提供了非常多的方法來獲取坐標值、相對距離等。方法豐富固然好,下面對坐標系的API進行總結,如下圖:

這些方法可以分為如下兩個類別:

  • View提供的獲取坐標方法
    • getTop():獲取到的是View自身的頂邊到其父布局頂邊的距離。
    • getLeft():獲取到的是View自身的左邊到其父布局最左邊的距離。
    • getRight():獲取到的是View自身的右邊到其父布局左邊的距離。
    • getBottom():獲取到的是View自身的底邊到其父布局頂邊的距離。
    </li>
  • MotionEvent提供的方法
    • getX():獲取點擊事件距離空間左邊的距離,即視圖坐標。
    • getY():獲取點擊事件距離控件頂邊的距離,即視圖坐標。
    • getRawX():獲取點擊事件距離整個屏幕左邊的距離,即絕對坐標。
    • getRawY():獲取點擊事件距離整個屏幕頂邊的距離,即絕對坐標。
    • </ul> </li> </ul>

      二、實現滑動的七種方式

      當了解Android坐標系和觸控事件后,我們再來看看如何使用系統提供的API來實現動態地修改一個View坐標,即實時滑動效果。而不管采用哪一種方式,其實現的思想基本是一致的,當觸摸View時,系統記下當前觸摸點坐標,當手指移動時,系統記下移動后的觸摸點坐標,從而獲取到相對于前一次坐標點的偏移量,并通過偏移量來修改View的坐標,這樣不斷重復,實現滑動過程。

      通過一個實例看看Android中該如何實現滑動效果,定義一個View,處于LinearLayout中,實現一個簡單布局:

      <?xml version="1.0" encoding="utf-8"?> 
       
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
       
      android:layout_width="match_parent" 
       
      android:layout_height="match_parent" 
       
      android:orientation="vertical"> 
       
      <com.xjf.drawview.DragView1 
       
      android:layout_width="100dp" 
       
      android:layout_height="100dp" /> 
       
      </LinearLayout>  
      

      我們的目的就是讓這個自定義的View隨著手指在屏幕上的滑動而滑動。初始化時顯示效果:

      (1)、layout方法

      在View繪制時,會調用onLayout()方法來設置顯示的位置。同樣,可以通過修改View的left,top,right,bottom四個屬性來控制View的坐標。與前面提供的模板代碼一樣,在每次回調onTouchEvent的時候,我們都來獲取一下觸摸點的坐標,代碼如下:

      //獲取當前輸入點的X、Y坐標(視圖坐標) 
       
      int x = (int) event.getX(); 
       
      int y = (int) event.getY();  
      

      接著,在Action_DOWN事件中記錄觸摸點的坐標,如下:

      case MotionEvent.ACTION_DOWN: 
       
      // 記錄觸摸點坐標 
       
      lastX = x; 
       
      lastY = y; 
       
      break;  
      

      最后,可以在Action_MOVE事件中計算偏移量,并將偏移量作用到Layout方法中,在目前Layout的left,top,right,bottom基礎上,增加計算出來的偏移量,代碼如下所示:

      case MotionEvent.ACTION_MOVE: 
       
      // 計算偏移量 
       
      int offsetX = x - lastX; 
       
      int offsetY = y - lastY; 
       
      // 在當前left、top、right、bottom的基礎上加上偏移量 
       
      layout(getLeft() + offsetX, 
       
      getTop() + offsetY, 
       
      getRight() + offsetX, 
       
      getBottom() + offsetY); 
       
      break;  
      

      這樣沒錯移動后,View都會調用Layout方法來對自己重新布局,從而達到移動View的效果。

      上面的代碼中,使用的是getX()、getY()方法來獲取坐標值,即通過視圖坐標來獲取偏移量。當然,同樣可以使用getRawX()、getRawY()來獲取坐標,并使用絕對坐標來計算偏移量,代碼如下:

      // 視圖坐標方式 
       
      @Override 
       
      public boolean onTouchEvent(MotionEvent event) { 
       
      int x = (int) event.getRawX(); 
       
      int y = (int) event.getRawY(); 
       
      switch (event.getAction()) { 
       
      case MotionEvent.ACTION_DOWN: 
       
      // 記錄觸摸點坐標 
       
      lastX = x; 
       
      lastY = y; 
       
      break; 
       
      case MotionEvent.ACTION_MOVE: 
       
      // 計算偏移量 
       
      int offsetX = x - lastX; 
       
      int offsetY = y - lastY; 
       
      // 在當前left、top、right、bottom的基礎上加上偏移量 
       
      layout(getLeft() + offsetX, 
       
      getTop() + offsetY, 
       
      getRight() + offsetX, 
       
      getBottom() + offsetY); 
       
      //重新設置初始化坐標 
       
      lastX = x; 
       
      lastY = y; 
       
      break; 
       
      } 
       
      return true; 
       
      }  
      

      使用絕對坐標系,有一點非常需要注意的地方,就是在每次執行完ACTION_MOVE的邏輯后,一定要重新設置初始化坐標,這樣才能準確地獲取偏移量。

      (2)、offsetLeftAndRight()與offsetTopAndBottom()

      這個方法相當于系統提供的一個對左右、上下移動的API的封裝。當計算出偏移量后,只需要使用如下代碼就可以完成View的重新布局,效果與使用Layout方法一樣,代碼如下所示:

      //同時對left和right進行偏移 
       
      offsetLeftAndRight(offsetX); 
       
      //同時對top和bottom進行偏移 
       
      offsetTopAndBottom(offsetY);  
      

      這里的offsetX、offsetY與在layout方法中計算offset方法一樣。

      (3)、LayoutParams

      LayoutParams保存了一個View的布局參數。因此可以在程序中,通過改變LayoutParams來動態地修改一個布局的位置參數,從而達到改變View位置的效果。我們可以很方便在程序中使用getLayoutParams()來獲取一個View的LayoutParams。當然,計算偏移量的方法與在Layout方法中計算offset也是一樣。當獲取到偏移量之后,就可以通過setLayoutParams來改變其LayoutParams,代碼如下:

      LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); 
       
      layoutParams.leftMargin = getLeft() + offsetX; 
       
      layoutParams.topMargin = getTop() + offsetY; 
       
      setLayoutParams(layoutParams);  
      

      這里getLayoutParams()獲取LayoutParams時,需要根據View所在View父布局的類型來設置不同的類型,比如這里將View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。這一切的前提是你必須要有一個父布局,不然系統無法獲取LayoutParams。

      在通過改變LayoutParams來改變一個View的位置時,通常改變的是這個View的Margin屬性,所以除了使用布局的LayoutParams之外,還可以使用ViewGroup.MarginLayoutParams來實現這一一個功能,代碼:

      ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); 
       
      layoutParams.leftMargin = getLeft() + offsetX; 
       
      layoutParams.topMargin = getTop() + offsetY; 
       
      setLayoutParams(layoutParams);  
      

      我們可以發現,使用ViewGroup.MarginLayoutParams更加的方便,不需要考慮父布局的類型,當然它們的本質都是一樣。

      (4)、scrollTo與scrollBy

      在一個View中,系統提供了scrollTo、scrollBy兩種方式來改變一個View的位置。這兩個方法的區別非常好理解,與英文中To與By的區別類似,scrollTo(x,y)表示移動到一個具體的坐標點(x,y),而scrollBy(dx,dy)表示移動的增量為dx,dy。

      與前面幾種方式相同,在獲取偏移量后使用scrollBy來移動View,代碼如下:

      int offsetX = x - lastX; 
       
      int offsetY = y - lastY; 
       
      scrollBy(offsetX, offsetY);  
      

      但是,當我們拖動View的時候,你會發現View并沒有移動,其實方法沒錯,View確實移動了,只是移動的并不是我們想要的東西。scrollTo、scrollBy方法移動的是View的content,即讓View的內容移動,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移動的將是所有子View,如果在View中使用,那么移動的將是View的內容,例如TextView,content就是它的文本,ImageView,content就是它的drawable對象。

      通過以上的分析,現在知道為什么不能再View中使用這兩個方法來拖動這個View了。那么我們就該View所在的ViewGroup中來使用scrollBy方法,移動它的子View,代碼如下:

      ((View) getParent()).scrollBy(offsetX, offsetY); 
      

      但是再次拖動View的時候,你會發現View雖然移動了,但卻在亂動,并不是我們想要的跟隨觸摸點的移動而移動。這里先看一下視圖移動,不妨這樣想象一下手機屏幕是一個中空的蓋板,蓋板下面是一個巨大的畫布,也就是我們想要顯示的視圖。當把這個蓋板蓋在畫布上的某一處時,透過中間空的矩形,我們看見了手機屏幕上顯示的視圖,而畫布上其他地方的視圖,則被蓋板蓋住了無法看見。我們的視圖與這個例子非常類似,我們沒有看見視圖,并不代表它就不存在,有可能只是在屏幕外面而已。當調用scrollBy方法時,可以想象為外面的蓋板在移動,這么說比較抽象。

      下圖一中間的矩形相當于屏幕,及可視區域。后面的content就相當于畫布,代表視圖。可以看到,只有視圖的中間部分目前是可視的,其他部分都不可見。在可見區域中,我們設置了一個Button,它的坐標為(20,10)。

      下面使用scrollBy方法,將蓋板(屏幕、可視區域),在水平方向上向X軸正方向(向右)平移20,在豎直方向上向Y軸正方向(下方)平移10,那么平移之后的可視區域如圖二。

      圖一

      圖二、移動之后的可視區域

      我們發現,雖然設置scrollBy(20,10),偏移量均為X軸、Y軸正方向上的正數,但是在屏幕的可視區域內,Button卻向X軸、Y軸負方向上移動了。這就是因為參考系選擇的不同,而產生的不同效果。

      通過上面的分析可以發現,如果講scrollBy中的參數dx和dy設置為正數,那么content講向坐標軸負方向移動,如果將scrollBy中的參數dx和dy設置為負數,那么content將向坐標軸正方向移動,因此回到前面的例子,要實現跟隨著手指移動而滑動的效果,就必須將偏移量改為負值,代碼如下:

      int offsetX = x - lastX; 
       
      int offsetY = y - lastY; 
       
      ((View) getParent()).scrollBy(-offsetX, -offsetY);  
      

      現在在運行一次發現和前面幾種方式效果相同了,類似地使用絕對坐標時,也可以通過使用scrollTo發方法來實現這一效果。

      (5)、Scroller

      前面提到了scrollBy、scrollTo方法,就不得不再來說一說Scroller類。Scroller類與scrollBy、scrollTo方法十分相似。什么區別?先看例子,如果要完成這樣一個效果;通過點擊按鈕,讓一個ViewGroup的子View向右移動100個像素。問題看起來很簡單,只要在按鈕的點擊事件中使用前面的scrollBy方法設置下偏移量就可以了嗎?確實這樣可以讓一個子ViewGroup中的子View平移,但是不管使用scrollBy還是scrollTo方法,子view的平移都是瞬間發生的,在事件執行的時候平移就已經完成了,這樣的效果會讓人感覺非常突然,Google建議使用自然的過度動畫來實現移動效果。因此Scroller類就這樣誕生了,通過Scroller類可以實現平滑移動的效果,而不是瞬間就完成移動。

      Scroller類的實現原理,其實它與前面使用的scrollTo和scrollBy方法來實現子View跟隨手指移動的原理基本類似,雖然scrollBy芳芳法是讓子View瞬間從某點移動到另一個點,但是由于在ACTION_MOVE事件中不斷獲取手指移動的微小的偏移量,這樣就將一段距離劃分成了N個非常小的偏移量。雖然每個偏移量里面,通過scrollBy方法進行了瞬間移動,但是在整體上卻可以獲得一個平滑移動的效果。這個原理與動畫的實現原理也是基本類似的,它們都是利用了人眼的視覺暫留特性。

      下面我們使用Scroller類實現平滑移動,在這個實例中,同樣讓子View跟隨手指的滑動而滑動,但是在手指離開屏蔽時,讓子View平滑的移動到初始化位置,即屏幕左上角。使用Scroller類需要如下三個步驟:

      • 初始化Scroller

      首先通過它的構造方法來創建一個Scroller對象,代碼如下所示:

      // 初始化Scroller 
      mScroller = new Scroller(context);  
      
      • 重寫computerScroller方法,實現模擬滑動

      下面我們需要重寫computerScroller()芳芳法,它是使用Scroller類的核心,系統在繪制View的時候會在draw()方法中調用該方法。這個方法實際就是使用的scrollTo方法。再結合Scroller對象,幫助獲取到當前滾動值。我們可以通過不斷地瞬間移動一個小的距離來實現整體上的平滑移動效果。代碼如下:

      @Override 
      public void computeScroll() { 
          super.computeScroll(); 
          // 判斷Scroller是否執行完畢 
          if (mScroller.computeScrollOffset()) { 
              ((View) getParent()).scrollTo( 
                      mScroller.getCurrX(), 
                      mScroller.getCurrY()); 
              // 通過重繪來不斷調用computeScroll 
              invalidate(); 
          } 
      }  
      

      Scroller類提供了computeScrollOffset()方法來判斷是否完成了整個滑動,同時也提供了getCurrX()、getCurrY()方法來獲得當前的滑動坐標。在上面的代碼中,唯一需要注意的是invalidate()方法,因為只能在computeScroller()方法中獲取模擬過程中的scrollX和scrollY坐標。但computeScroll()方法是不會自動調用的,只能通過invalidate()->draw()->computeScroll()來間接調用compuetScroll()方法,所以需要在compuetScroll()方法中調用invaliDate()方法,實現循環獲取scrollX和scrollY的目的。而當模擬過程結束后,scroller.compuetScrollOffset()方法會返回false,而中斷循環,完成平滑移動過程。

      • startScroll開啟模擬過程

      我們在需要使用平滑移動的事件中,使用Scroller類的startScroll()方法來開啟平滑移動過程。startScroll()方法具有兩個重載方法。

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

      可以看到它們的區別就是一個具有指定的支持時長,而另一個沒有。很好理解,與在動畫中設置duration和使用默認的顯示時長是一個道理。其他四個坐標,則與他們的命名含義相同,就是起始坐標與偏移量。在獲取坐標時,通常可以使用getScrollX()和getScrollY()方法來獲取父視圖中content所滑動到的點的坐標,需要注意的是這個值的正負,它與在scrollBy、scrollTo中講解的情況是一樣的。

      根據以上三步,就可以使用Scroller類實現平滑移動,在構造方法中初始化Scroller對象,重寫View的computerScroll()方法,最后監聽手指離開屏蔽的事件,并在該事件中調用startScroll()方法完成平滑移動。監聽手指離開屏幕的事件,只需要在onTouchEvent中增加一個ACTION_UP監聽選項即可,代碼如下所示:

      case MotionEvent.ACTION_UP: 
          // 手指離開時,執行滑動過程 
          View viewGroup = ((View) getParent()); 
          mScroller.startScroll( 
                  viewGroup.getScrollX(), 
                  viewGroup.getScrollY(), 
                  -viewGroup.getScrollX(), 
                  -viewGroup.getScrollY()); 
          invalidate(); 
          break;  
      

      在startScroll()方法中我們獲取子View移動的距離-getScrollX()、getScrollY(),并將偏移量設置為其相反數,從而將子View滑動到原位置。這里的invalidate()方法是用來通知View進行重繪,調用computeScroll()的模擬過程。當然,也可以給startScroll()方法增加一個duration的參數來設置滑動的持續時長。

      (6)、屬性動畫

      屬性動畫請參見我的另一篇:Android全套動畫使用技巧

      (7)、ViewDragHelper

      Google在其support庫中為我們提供了DrawerLayout和SlidingPaneLayout兩個布局來幫助開發者實現側邊欄滑動的效果。這兩個新的布局方便我們創建自己的滑動布局界面,在這兩個強大布局背后有一個功能強大的類——ViewDragHelper。通過ViewDragHelper,基本可以實現各種不同的滑動、拖放需求,因此這個方法也是各種滑動解決方案中的終結絕招。

      下面演示一個使用ViewDragHelper創建一個QQ側邊欄滑動的布局,如圖:

      圖三

      圖四

      • 初始化ViewDragHelper

      首先需要初始化ViewDragHelper,ViewDragHelper通常定義在一個ViewGroup的內部,通過靜態工廠方法進行初始化,代碼如下:

      mViewDragHelper = ViewDragHelper.create(this, callback); 
      

      第一個參數監聽的View,通常需要一個ViewGroup,即parentView;第二個參數是一個Callback回調,這個回調就是整個ViewDragHelper的邏輯核心。

      • 攔截事件

      重寫攔截事件,將事件傳遞給ViewDragHelper進行處理;

      @Override 
       
      public boolean onInterceptTouchEvent(MotionEvent ev) { 
       
      return mViewDragHelper.shouldInterceptTouchEvent(ev); 
       
      } 
       
      @Override 
       
      public boolean onTouchEvent(MotionEvent event) { 
       
      //將觸摸事件傳遞給ViewDragHelper,此操作必不可少 
       
      mViewDragHelper.processTouchEvent(event); 
       
      return true; 
       
      }  
      
      • 處理computeScroll()

      使用ViewDragHelper同樣需要重寫computeScroll()方法,因為ViewDragHelper內部也是通過Scroller來實現平滑移動的。

      @Override 
      public void computeScroll() { 
          if (mViewDragHelper.continueSettling(true)) { 
              ViewCompat.postInvalidateOnAnimation(this); 
          } 
      }  
      
      • 處理回調Callback

      創建一個ViewDragHelper.Callback

      private ViewDragHelper.Callback getCallback = new ViewDragHelper.Callback() { 
          @Override 
          public boolean tryCaptureView(View child, int pointerId) { 
              return false; 
          } 
      };  
      

      as自動重寫tryCaptureView()方法,通過這個方法可以指定在創建ViewDragHelper時,參數parentView中的哪一個子Vieww可以被移動,例如我們在這個實例中自定義一個ViewGroup,里面定義了兩個子View——Menu View和MainView,如下代碼:

      // 何時開始檢測觸摸事件 
      @Override 
      public boolean tryCaptureView(View child, int pointerId) { 
          //如果當前觸摸的child是mMainView時開始檢測 
          return mMainView == child; 
      }  
      

      具體垂直滑動方法clampViewPositionVertical()和水平滑動方法clampViewPositionHorizontal()。實現滑動這個兩個方法必須寫,默認返回值是0,即不發生滑動,當然如果只重寫clampViewPositionVertical()或clampViewPositionHorizontal()中的一個,那么就只會實現該方向上的滑動效果。

      // 處理垂直滑動 
      @Override 
      public int clampViewPositionVertical(View child, int top, int dy) { 
          return top; 
      } 
       
      // 處理水平滑動 
      @Override 
      public int clampViewPositionHorizontal(View child, int left, int dx) { 
          return left; 
      }  
      

      clampViewPositionVertical(View child, int top, int dy)中的參數top,代表在垂直方向上child移動的距離,dy則表示比較前一次的增量。clampViewPositionHorizontal(View child, int left, int dx)也是類似的含義,通常情況下只需要返回top和left即可,但需要更加精確地計算padding等屬性的時候,就需要對left進行一些處理,并返回合適大小的值。

      通過重寫上面的三個方法,就可以實現基本的滑動效果。當用手拖動MainView的時候,它就可有跟隨手指的滑動而滑動了,代碼:

      private ViewDragHelper.Callback callback =  new ViewDragHelper.Callback() { 
       
                  // 何時開始檢測觸摸事件 
                  @Override 
                  public boolean tryCaptureView(View child, int pointerId) { 
                      //如果當前觸摸的child是mMainView時開始檢測 
                      return mMainView == child; 
                  } 
        
       
                  // 處理垂直滑動 
                  @Override 
                  public int clampViewPositionVertical(View child, int top, int dy) { 
                      return 0; 
                  } 
       
                  // 處理水平滑動 
                  @Override 
                  public int clampViewPositionHorizontal(View child, int left, int dx) { 
                      return left; 
                  } 
       
                   
              };  
      

      在前面的Scroller中講解時實現一個效果——手指離開屏幕后,View滑動回到初始位置。現在使用ViewDragHelper實現,在ViewDragHelper.Callback中,系統提供了這樣的方法——onViewReleased(),通過重寫這個方法,可以非常簡單地實現當手指離開屏幕后實現的操作。這個方法內部是使用Scroller類實現的,這也是前面重寫computeScroll()方法的原因。

      @Override 
      public void onViewReleased(View releasedChild, float xvel, float yvel) { 
          super.onViewReleased(releasedChild, xvel, yvel); 
          //手指抬起后緩慢移動到指定位置 
          if (mMainView.getLeft() < 500) { 
              //關閉菜單 
              //等同于Scroll的startScroll方法 
              mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); 
              ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); 
          } else { 
              //打開菜單 
              mViewDragHelper.smoothSlideViewTo(mMainView,300,0); 
              ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); 
          } 
      }  
      

      設置讓MainView移動后左邊距小于500像素的時候,就使用smoothSlideViewTo()方法來講MainView還原到初始狀態,即坐標(0,0),左邊距大于500則將MainView移動到(300,0)坐標,即顯示MainView。

      //ViewDragHelper 
       
      mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); 
       
      ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);   
      
      //Scroller 
       
      mScroller.startScroll(x,y,dx,dy); 
       
      invalidate();  
      

      滑動的時候,在自定義ViewGroup的onFinishInflate()方法中,按照順序將子View分別定義成MenuView和MainView,并在onSizeChanged方法中獲得View的寬度。如果需要根據View的寬度來處理滑動后的效果,就可以使用這個值判斷。

      /*** 
       * 加載完布局文件后調用 
       */ 
      @Override 
      protected void onFinishInflate() { 
          super.onFinishInflate(); 
          mMenuView = getChildAt(0); 
          mMainView = getChildAt(1); 
      } 
       
      @Override 
      protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
          super.onSizeChanged(w, h, oldw, oldh); 
          mWidth = mMenuView.getMeasuredWidth(); 
      }  
      

      最后,整個通過ViewDragHelper實現QQ側滑功能代碼:

      package com.xjf.drawview; 
       
      import android.content.Context; 
      import android.support.v4.view.ViewCompat; 
      import android.support.v4.widget.ViewDragHelper; 
      import android.util.AttributeSet; 
      import android.view.MotionEvent; 
      import android.view.View; 
      import android.widget.FrameLayout; 
       
      public class DragViewGroup extends FrameLayout { 
       
          private ViewDragHelper mViewDragHelper; 
          private View mMenuView, mMainView; 
          private int mWidth; 
       
          public DragViewGroup(Context context) { 
              super(context); 
              initView(); 
          } 
       
          public DragViewGroup(Context context, AttributeSet attrs) { 
              super(context, attrs); 
              initView(); 
          } 
       
          public DragViewGroup(Context context, 
                               AttributeSet attrs, int defStyleAttr) { 
              super(context, attrs, defStyleAttr); 
              initView(); 
          } 
       
          /*** 
           * 加載完布局文件后調用 
           */ 
          @Override 
          protected void onFinishInflate() { 
              super.onFinishInflate(); 
              mMenuView = getChildAt(0); 
              mMainView = getChildAt(1); 
          } 
       
          @Override 
          protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
              super.onSizeChanged(w, h, oldw, oldh); 
              mWidth = mMenuView.getMeasuredWidth(); 
          } 
       
          @Override 
          public boolean onInterceptTouchEvent(MotionEvent ev) { 
              return mViewDragHelper.shouldInterceptTouchEvent(ev); 
          } 
       
          @Override 
          public boolean onTouchEvent(MotionEvent event) { 
              //將觸摸事件傳遞給ViewDragHelper,此操作必不可少 
              mViewDragHelper.processTouchEvent(event); 
              return true; 
          } 
       
          private void initView() { 
              mViewDragHelper = ViewDragHelper.create(this, callback); 
          } 
       
          private ViewDragHelper.Callback callback = 
                  new ViewDragHelper.Callback() { 
       
                      // 何時開始檢測觸摸事件 
                      @Override 
                      public boolean tryCaptureView(View child, int pointerId) { 
                          //如果當前觸摸的child是mMainView時開始檢測 
                          return mMainView == child; 
                      } 
       
                      // 觸摸到View后回調 
                      @Override 
                      public void onViewCaptured(View capturedChild, 
                                                 int activePointerId) { 
                          super.onViewCaptured(capturedChild, activePointerId); 
                      } 
       
                      // 當拖拽狀態改變,比如idle,dragging 
                      @Override 
                      public void onViewDragStateChanged(int state) { 
                          super.onViewDragStateChanged(state); 
                      } 
       
                      // 當位置改變的時候調用,常用與滑動時更改scale等 
                      @Override 
                      public void onViewPositionChanged(View changedView, 
                                                        int left, int top, int dx, int dy) { 
                          super.onViewPositionChanged(changedView, left, top, dx, dy); 
                      } 
       
                      // 處理垂直滑動 
                      @Override 
                      public int clampViewPositionVertical(View child, int top, int dy) { 
                          return 0; 
                      } 
       
                      // 處理水平滑動 
                      @Override 
                      public int clampViewPositionHorizontal(View child, int left, int dx) { 
                          return left; 
                      } 
       
                      // 拖動結束后調用 
                      @Override 
                      public void onViewReleased(View releasedChild, float xvel, float yvel) { 
                          super.onViewReleased(releasedChild, xvel, yvel); 
                          //手指抬起后緩慢移動到指定位置 
                          if (mMainView.getLeft() < 500) { 
                              //關閉菜單 
                              //相當于Scroller的startScroll方法 
                              mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); 
                              ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); 
                          } else { 
                              //打開菜單 
                              mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0); 
                              ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); 
                          } 
                      } 
                  }; 
       
          @Override 
          public void computeScroll() { 
              if (mViewDragHelper.continueSettling(true)) { 
                  ViewCompat.postInvalidateOnAnimation(this); 
              } 
          } 
      }  
      

      除此之外,ViewDragHelper很多強大的功能還沒得到展示,在ViewDragHelper.Callback中,系統定義了大量的監聽事件來幫助我們處理各種事件,如下:

      • onViewCaptured()這個事件在用戶觸摸到View后回調
      • onViewDragStateChanged()這個事件在拖拽狀態改變時回調,比如idle,dragging等狀態

      STATE_IDLE:View當前沒有被拖拽也沒執行動畫,只是安靜地待在原地

      STATE_DRAGGING:View當前正在被拖動,由于用戶輸入或模擬用戶輸入導致View位置正在改變

      STATE_SETTLING:View當前正被安頓到指定位置,由fling手勢或預定義的非交互動作觸發

      • onViewPositionChanged()//view在拖動過程坐標發生變化時會調用此方法,包括兩個時間段:手動拖動和自動滾動。

       

      來自:http://mobile.51cto.com/android-538295.htm

       

 本文由用戶 bkbv6005 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!