1. 程式人生 > >Android ViewDragHelper完全解析 自定義ViewGroup神器

Android ViewDragHelper完全解析 自定義ViewGroup神器

                     

一、概述

在自定義ViewGroup中,很多效果都包含使用者手指去拖動其內部的某個View(eg:側滑選單等),針對具體的需要去寫好onInterceptTouchEventonTouchEvent這兩個方法是一件很不容易的事,需要自己去處理:多手指的處理、加速度檢測等等。 好在官方在v4的支援包中提供了ViewDragHelper這樣一個類來幫助我們方便的編寫自定義ViewGroup。簡單看一下它的註釋:

 

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number   of useful operations and state tracking for allowing a user to drag and reposition   views within their parent ViewGroup.

本篇部落格將重點介紹ViewDragHelper的使用,並且最終去實現一個類似DrawerLayout的一個自定義的ViewGroup。(ps:官方的DrawerLayout就是用此類實現)

二、入門小示例

首先我們通過一個簡單的例子來看看其快捷的用法,分為以下幾個步驟:

  1. 建立例項
  2. 觸控相關的方法的呼叫
  3. ViewDragHelper.Callback例項的編寫

(一) 自定義ViewGroup

package com.zhy.learn.view;import android.content.Context;import android.support.v4.widget.ViewDragHelper;import
android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.LinearLayout;/** * Created by zhy on 15/6/3. */public class VDHLayout extends LinearLayout{    private ViewDragHelper mDragger;    public VDHLayout(Context context, AttributeSet attrs)    {        super
(context, attrs);        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()        {            @Override            public boolean tryCaptureView(View child, int pointerId)            {                return true;            }            @Override            public int clampViewPositionHorizontal(View child, int left, int dx)            {                return left;            }            @Override            public int clampViewPositionVertical(View child, int top, int dy)            {                return top;            }        });    }   @Override    public boolean onInterceptTouchEvent(MotionEvent event)    {        return mDragger.shouldInterceptTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        mDragger.processTouchEvent(event);        return true;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

可以看到,上面整個自定義ViewGroup的程式碼非常簡潔,遵循上述3個步驟:

1、建立例項

mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()        {        });
  • 1
  • 2
  • 3

建立例項需要3個引數,第一個就是當前的ViewGroup,第二個sensitivity,主要用於設定touchSlop:

helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
  • 1

可見傳入越大,mTouchSlop的值就會越小。第三個引數就是Callback,在使用者的觸控過程中會回撥相關方法,後面會細說。

2、觸控相關方法

 @Override    public boolean onInterceptTouchEvent(MotionEvent event)    {        return mDragger.shouldInterceptTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        mDragger.processTouchEvent(event);        return true;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

onInterceptTouchEvent中通過使用mDragger.shouldInterceptTouchEvent(event)來決定我們是否應該攔截當前的事件。onTouchEvent中通過mDragger.processTouchEvent(event)處理事件。

3、實現ViewDragHelper.CallCack相關方法

new ViewDragHelper.Callback()        {            @Override            public boolean tryCaptureView(View child, int pointerId)            {                return true;            }            @Override            public int clampViewPositionHorizontal(View child, int left, int dx)            {                return left;            }            @Override            public int clampViewPositionVertical(View child, int top, int dy)            {                return top;            }        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

ViewDragHelper中攔截和處理事件時,需要會回撥CallBack中的很多方法來決定一些事,比如:哪些子View可以移動、對個移動的View的邊界的控制等等。

上面複寫的3個方法:

  • tryCaptureView如何返回ture則表示可以捕獲該view,你可以根據傳入的第一個view引數決定哪些可以捕獲
  • clampViewPositionHorizontal,clampViewPositionVertical可以在該方法中對child移動的邊界進行控制,left , top 分別為即將移動到的位置,比如橫向的情況下,我希望只在ViewGroup的內部移動,即:最小>=paddingleft,最大<=ViewGroup.getWidth()-paddingright-child.getWidth。就可以按照如下程式碼編寫:
 @Override            public int clampViewPositionHorizontal(View child, int left, int dx)            {                final int leftBound = getPaddingLeft();                final int rightBound = getWidth() - mDragView.getWidth() - leftBound;                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);                return newLeft;            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

經過上述3個步驟,我們就完成了一個簡單的自定義ViewGroup,可以自由的拖動子View。

簡單看一下佈局檔案

(二) 佈局檔案

<com.zhy.learn.view.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android"                              xmlns:tools="http://schemas.android.com/tools"                              android:layout_width="match_parent"                              android:orientation="vertical"                              android:layout_height="match_parent"    >    <TextView        android:layout_margin="10dp"        android:gravity="center"        android:layout_gravity="center"        android:background="#44ff0000"        android:text="I can be dragged !"        android:layout_width="100dp"        android:layout_height="100dp"/>    <TextView        android:layout_margin="10dp"        android:layout_gravity="center"        android:gravity="center"        android:background="#44ff0000"        android:text="I can be dragged !"        android:layout_width="100dp"        android:layout_height="100dp"/>    <TextView        android:layout_margin="10dp"        android:layout_gravity="center"        android:gravity="center"        android:background="#44ff0000"        android:text="I can be dragged !"        android:layout_width="100dp"        android:layout_height="100dp"/></com.zhy.learn.view.VDHLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

我們的自定義ViewGroup中有三個TextView。

當前效果:

可以看到短短數行程式碼就可以玩起來了~~~

有了直觀的認識以後,我們還需要對ViewDragHelper.CallBack裡面的方法做下深入的理解。首先我們需要考慮的是:我們的ViewDragHelper不僅僅說只能夠去讓子View去跟隨我們手指移動,我們繼續往下學習其他的功能。

三、功能展示

ViewDragHelper還能做以下的一些操作:

  • 邊界檢測、加速度檢測(eg:DrawerLayout邊界觸發拉出)
  • 回撥Drag Release(eg:DrawerLayout部分,手指擡起,自動展開/收縮)
  • 移動到某個指定的位置(eg:點選Button,展開/關閉Drawerlayout)

那麼我們接下來對我們最基本的例子進行改造,包含上述的幾個操作。

首先看一下我們修改後的效果:

簡單的為每個子View添加了不同的操作:

第一個View,就是演示簡單的移動 第二個View,演示除了移動後,鬆手自動返回到原本的位置。(注意你拖動的越快,返回的越快) 第三個View,邊界移動時對View進行捕獲。

好了,看完效果圖,來看下程式碼的修改:

修改後的程式碼

package com.zhy.learn.view;import android.content.Context;import android.graphics.Point;import android.support.v4.widget.ViewDragHelper;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.LinearLayout;/** * Created by zhy on 15/6/3. */public class VDHLayout extends LinearLayout{    private ViewDragHelper mDragger;    private View mDragView;    private View mAutoBackView;    private View mEdgeTrackerView;    private Point mAutoBackOriginPos = new Point();    public VDHLayout(Context context, AttributeSet attrs)    {        super(context, attrs);        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback()        {            @Override            public boolean tryCaptureView(View child, int pointerId)            {                //mEdgeTrackerView禁止直接移動                return child == mDragView || child == mAutoBackView;            }            @Override            public int clampViewPositionHorizontal(View child, int left, int dx)            {                return left;            }            @Override            public int clampViewPositionVertical(View child, int top, int dy)            {                return top;            }            //手指釋放的時候回撥            @Override            public void onViewReleased(View releasedChild, float xvel, float yvel)            {                //mAutoBackView手指釋放時可以自動回去                if (releasedChild == mAutoBackView)                {                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);                    invalidate();                }            }            //在邊界拖動時回撥            @Override            public void onEdgeDragStarted(int edgeFlags, int pointerId)            {                mDragger.captureChildView(mEdgeTrackerView, pointerId);            }        });        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event)    {        return mDragger.shouldInterceptTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        mDragger.processTouchEvent(event);        return true;    }    @Override    public void computeScroll()    {        if(mDragger.continueSettling(true))        {            invalidate();        }    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b)    {        super.onLayout(changed, l, t, r, b);        mAutoBackOriginPos.x = mAutoBackView.getLeft();        mAutoBackOriginPos.y = mAutoBackView.getTop();    }    @Override    protected void onFinishInflate()    {        super.onFinishInflate();        mDragView = getChildAt(0);        mAutoBackView = getChildAt(1);        mEdgeTrackerView = getChildAt(2);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113

佈局檔案我們僅僅是換了下文字和背景色就不重複貼了。

第一個View基本沒做任何修改。

第二個View,我們在onLayout之後儲存了最開啟的位置資訊,最主要還是重寫了Callback中的onViewReleased,我們在onViewReleased中判斷如果是mAutoBackView則呼叫settleCapturedViewAt回到初始的位置。大家可以看到緊隨其後的程式碼是invalidate();因為其內部使用的是mScroller.startScroll,所以別忘了需要invalidate()以及結合computeScroll方法一起。

第三個View,我們在onEdgeDragStarted回撥方法中,主動通過captureChildView對其進行捕獲,該方法可以繞過tryCaptureView,所以我們的tryCaptureView雖然併為返回true,但卻不影響。注意如果需要使用邊界檢測需要新增上mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

到此,我們已經介紹了Callback中常用的回撥方法了,當然還有一些方法沒有介紹,接下來我們修改下我們的佈局檔案,我們把我們的TextView全部加上clickable=true,意思就是子View可以消耗事件。再次執行,你會發現本來可以拖動的View不動了,(如果有拿Button測試的兄弟應該已經發現這個問題了,我希望你看到這了,而不是已經提問了,哈~)。

原因是什麼呢?主要是因為,如果子View不消耗事件,那麼整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent,在onTouchEvent的DOWN的時候就確定了captureView。如果消耗事件,那麼就會先走onInterceptTouchEvent方法,判斷是否可以捕獲,而在判斷的過程中會去判斷另外兩個回撥的方法:getViewHorizontalDragRangegetViewVerticalDragRange,只有這兩個方法返回大於0的值才能正常的捕獲。

所以,如果你用Button測試,或者給TextView添加了clickable = true ,都記得重寫下面這兩個方法:

@Overridepublic int getViewHorizontalDragRange(View child){     return getMeasuredWidth()-child.getMeasuredWidth();}@Overridepublic int getViewVerticalDragRange(View child){     return getMeasuredHeight()-child.getMeasuredHeight();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

方法的返回值應當是該childView橫向或者縱向的移動的範圍,當前如果只需要一個方向移動,可以只複寫一個。

到此,我們列一下所有的Callback方法,看看還有哪些沒用過的:

  • onViewDragStateChanged 

     

    當ViewDragHelper狀態發生變化時回撥(IDLE,DRAGGING,SETTING[自動滾動時])

  • onViewPositionChanged

     

    當captureview的位置發生改變時回撥

  • onViewCaptured

     

    當captureview被捕獲時回撥

  • onViewReleased 已用

  • onEdgeTouched

     

    當觸控到邊界時回撥。

  • onEdgeLock

     

    true的時候會鎖住當前的邊界,false則unLock。

  • onEdgeDragStarted 已用

  • getOrderedChildIndex

     

    改變同一個座標(x,y)去尋找captureView位置的方法。(具體在:findTopChildUnder方法中)

  • getViewHorizontalDragRange 已用

  • getViewVerticalDragRange 已用
  • tryCaptureView 已用
  • clampViewPositionHorizontal 已用
  • clampViewPositionVertical 已用

ok,至此所有的回撥方法都有了一定的認識。

總結下,方法的大致的回撥順序:

shouldInterceptTouchEvent:DOWN:    getOrderedChildIndex(findTopChildUnder)    ->onEdgeTouchedMOVE:    getOrderedChildIndex(findTopChildUnder)    ->getViewHorizontalDragRange &       getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)    ->clampViewPositionHorizontal&      clampViewPositionVertical    ->onEdgeDragStarted    ->tryCaptureView    ->onViewCaptured    ->onViewDragStateChangedprocessTouchEvent:DOWN:    getOrderedChildIndex(findTopChildUnder)    ->tryCaptureView    ->onViewCaptured    ->onViewDragStateChanged    ->onEdgeTouchedMOVE:    ->STATE==DRAGGING:dragTo    ->STATE!=DRAGGING:        onEdgeDragStarted        ->getOrderedChildIndex(findTopChildUnder)        ->getViewHorizontalDragRange&          getViewVerticalDragRange(checkTouchSlop)        ->tryCaptureView        ->onViewCaptured        ->onViewDragStateChanged
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

ok,上述是正常情況下大致的流程,當然整個過程可能會存在很多判斷不成立的情況。

從上面也可以解釋,我們在之前TextView(clickable=false)的情況下,沒有編寫getViewHorizontalDragRange方法時,是可以移動的。因為直接進入processTouchEvent的DOWN,然後就onViewCaptured、onViewDragStateChanged(進入DRAGGING狀態),接下來MOVE就直接dragTo了。

而當子View消耗事件的時候,就需要走shouldInterceptTouchEvent,MOVE的時候經過一系列的判斷(getViewHorizontalDragRange,clampViewPositionVertical等),才能夠去tryCaptureView。

ok,到此ViewDragHelper的入門用法我們就介紹結束了,下一篇,我們將使用ViewDragHelper去自己實現一個DrawerLayout。 有興趣的也可以根據本文,以及DrawerLayout的原始碼去實現了~

參考連結

~~have a nice day ~~

ok~

 

群號:463081660,歡迎入群

   

新浪微博

   

微信公眾號:hongyangAndroid   (歡迎關注,第一時間推送博文資訊)