在瞭解View拖拽之前,應該瞭解android的事件傳遞機制
ViewDragHelper 可以方便我們快速實現View拖拽功能。主要步驟如下
- 創建ViewDragHelper實例
- 實現ViewDragHelper的CallBack編寫
- 處理ViewGroup觸摸事件
下面就是一個可以實現View拖拽實現的ViewGroup
public class VDHLinearLayout extends LinearLayout {
ViewDragHelper dragHelper;
public VDHLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
}
- 創建ViewDragHelper實例
dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {});
/*
函數原型
@param forParent 當前的ViewGroup
@param sensitivity 設置拖拽靈敏度,數字越大越靈敏。默認給1就行(即系統的默認值)
@param cb 觸摸過程的回調加函數
*/
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity,
@NonNull Callback cb) {
- 實現ViewDragHelper.Callback相關方法
new ViewDragHelper.Callback() {
/*
返回true表示捕獲當前view,如果不想使某個view移動,可以在內部判斷當前View飯後返回false
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
/*
child垂直移動的距離,top 表示y軸座標,相對於ViewGroup而言。dy表示偏移的距離
返回值確定view最終的y軸座標,如果不想垂直移動return 0
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
/*
child垂直移動的距離,left表示x軸座標,相對於ViewGroup而言。dx表示偏移的距離
返回值確定view最終的x軸座標,如果不想水平移動return 0
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
}
- 處理ViewGroup觸摸事件(原理參考)android的事件傳遞機制
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
佈局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.qwe.VDHLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".SecoundActivity">
<TextView
android:id="@+id/text_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="not clickable"
android:background="@mipmap/ic_launcher_round"
/>
<TextView
android:id="@+id/text_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="clickable"
android:clickable="true"
android:background="@mipmap/ic_launcher_round"
/>
</com.example.qwe.VDHLinearLayout>
此時我們把text_2 指定clickable屬性,此時我們發現控件沒辦法移動了。這是因爲text_2 控件消耗了ACTION_DOWN事件,導致ViewGroup沒有調用onTouchEvent。
我們看看onTouchEvent中的dragHelper.processTouchEvent做了一些什麼
public void processTouchEvent(@NonNull MotionEvent ev) {
...
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
tryCaptureViewForDrag(toCapture, pointerId);
...
}
...
}
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
...
//這裏設置窗口狀態爲STATE_DRAGGING
captureChildView(toCapture, pointerId);
...
}
public void captureChildView(@NonNull View childView, int activePointerId) {
...
setDragState(STATE_DRAGGING);
...
}
所以在ViewGroup的onTouchEvent裏面將狀態設置爲STATE_DRAGGING,此時我們如果移動窗口ACTION_MOVE,dragHelper.processTouchEvent 將返回true,攔截Move事件。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
return mDragState == STATE_DRAGGING;
}
因爲clickable消耗了ACTION_DOWN導致ViewGroup的onTouchEvent無法被調用 ,從而導致tryCaptureViewForDrag沒有調用,mDragState 狀態不爲STATE_DRAGGING。所以後面當觸發ACTION_MOVE的時候返回false(mDragState == STATE_DRAGGING)故無法移動。此時看看processTouchEvent的ACTION_MOVE事件處理。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
...
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
...
final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
&& (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
break;
}
}
...
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
...
}
從上述代碼我們發現如果mCallback.getViewHorizontalDragRange = 0 && mCallback.getViewVerticalDragRange = 0
那麼程序將Break,過濾掉下面tryCaptureViewForDrag的執行。從而導致mDragState 沒有辦法設置爲STATE_DRAGGING,從而返回false(mDragState == STATE_DRAGGING)。所以爲了使tryCaptureViewForDrag執行,那麼我們需要重寫mCallback.getViewHorizontalDragRange 或者 mCallback.getViewVerticalDragRange 返回非0就可以了。
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
return getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
return getMeasuredHeight();
}
此時在拖拽具有clickable 的view就OK了。