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

1008711發表於2019-02-27


一、滑動效果的產生

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

(1)、Android的座標系

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

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


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

(2)、檢視座標系

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

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進行總結,如下圖:

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


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

  • View提供的獲取座標方法
    • getTop():獲取到的是View自身的頂邊到其父佈局頂邊的距離。
    • getLeft():獲取到的是View自身的左邊到其父佈局最左邊的距離。
    • getRight():獲取到的是View自身的右邊到其父佈局左邊的距離。
    • getBottom():獲取到的是View自身的底邊到其父佈局頂邊的距離。
  • MotionEvent提供的方法
    • getX():獲取點選事件距離空間左邊的距離,即檢視座標。
    • getY():獲取點選事件距離控制元件頂邊的距離,即檢視座標。
    • getRawX():獲取點選事件距離整個螢幕左邊的距離,即絕對座標。
    • getRawY():獲取點選事件距離整個螢幕頂邊的距離,即絕對座標。

二、實現滑動的七種方式

當了解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隨著手指在螢幕上的滑動而滑動。初始化時顯示效果:

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


(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,那麼平移之後的可視區域如圖二。

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


圖一

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



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

我們發現,雖然設定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側邊欄滑動的佈局,如圖:

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


圖三


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



圖四

  • 初始化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在拖動過程座標發生變化時會呼叫此方法,包括兩個時間段:手動拖動和自動滾動。



相關文章