在自定義viewgroup的時候 要重寫onInterceptTouchEvent
和onTouchEvent 這2個方法 是非常麻煩的事情,好在谷歌後來
推出了ViewDragHelper這個類。可以極大方便我們自定義viewgroup.
先看一個簡單效果 一個layout裡有2個圖片 其中有一個可以滑動 一個不能滑
這個效果其實還蠻簡單的(原諒我讓臭腳不能動 讓BABY動)
佈局檔案:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical"> 6 7 <com.example.administrator.viewdragertestapp.DragLayout 8 android:layout_width="match_parent" 9 android:layout_height="match_parent" 10 android:orientation="vertical"> 11 12 <ImageView 13 android:id="@+id/iv1" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:layout_gravity="center_horizontal" 17 android:src="@drawable/a1"></ImageView> 18 19 <ImageView 20 android:id="@+id/iv2" 21 android:layout_width="wrap_content" 22 android:layout_height="wrap_content" 23 android:layout_gravity="center_horizontal" 24 android:src="@drawable/a2"></ImageView> 25 26 27 </com.example.administrator.viewdragertestapp.DragLayout> 28 29 </LinearLayout>
然後我們看一下自定義的layout 如何實現2個子view 一個可以滑動 一個不能滑動的
1 package com.example.administrator.viewdragertestapp; 2 3 import android.content.Context; 4 import android.support.v4.widget.ViewDragHelper; 5 import android.util.AttributeSet; 6 import android.view.MotionEvent; 7 import android.view.View; 8 import android.widget.ImageView; 9 import android.widget.LinearLayout; 10 import android.widget.TextView; 11 12 /** 13 * Created by Administrator on 2015/8/12. 14 */ 15 public class DragLayout extends LinearLayout { 16 17 private ViewDragHelper mDragger; 18 19 private ViewDragHelper.Callback callback; 20 21 private ImageView iv1; 22 private ImageView iv2; 23 24 @Override 25 protected void onFinishInflate() { 26 iv1 = (ImageView) this.findViewById(R.id.iv1); 27 iv2 = (ImageView) this.findViewById(R.id.iv2); 28 super.onFinishInflate(); 29 30 } 31 32 public DragLayout(Context context) { 33 super(context); 34 35 } 36 37 public DragLayout(Context context, AttributeSet attrs) { 38 super(context, attrs); 39 callback = new DraggerCallBack(); 40 //第二個引數就是滑動靈敏度的意思 可以隨意設定 41 mDragger = ViewDragHelper.create(this, 1.0f, callback); 42 } 43 44 class DraggerCallBack extends ViewDragHelper.Callback { 45 46 //這個地方實際上函式返回值為true就代表可以滑動 為false 則不能滑動 47 @Override 48 public boolean tryCaptureView(View child, int pointerId) { 49 if (child == iv2) { 50 return false; 51 } 52 return true; 53 } 54 55 @Override 56 public int clampViewPositionHorizontal(View child, int left, int dx) { 57 return left; 58 } 59 60 @Override 61 public int clampViewPositionVertical(View child, int top, int dy) { 62 return top; 63 } 64 } 65 66 67 @Override 68 public boolean onInterceptTouchEvent(MotionEvent ev) { 69 //決定是否攔截當前事件 70 return mDragger.shouldInterceptTouchEvent(ev); 71 } 72 73 @Override 74 public boolean onTouchEvent(MotionEvent event) { 75 //處理事件 76 mDragger.processTouchEvent(event); 77 return true; 78 } 79 80 81 }
然後再完善一下這個layout,剛才滑動的時候我們的view 出了螢幕的邊界很不美觀 現在我們修改2個函式 讓滑動的範圍
在這個螢幕之內(準確的說是在這個layout之內,因為我們的佈局檔案layout充滿了螢幕 所以看上去是在螢幕內)
1 //這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。 2 // 我們要讓view滑動的範圍在我們的layout之內 3 //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。 4 //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍 5 //除此之外就是如果你的layout設定了padding的話,也可以讓子view的活動範圍在padding之內的. 6 7 @Override 8 public int clampViewPositionHorizontal(View child, int left, int dx) { 9 //取得左邊界的座標 10 final int leftBound = getPaddingLeft(); 11 //取得右邊界的座標 12 final int rightBound = getWidth() - child.getWidth() - leftBound; 13 //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left 14 //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值 15 //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值 16 return Math.min(Math.max(left, leftBound), rightBound); 17 } 18 19 //縱向的註釋就不寫了 自己體會 20 @Override 21 public int clampViewPositionVertical(View child, int top, int dy) { 22 final int topBound = getPaddingTop(); 23 final int bottomBound = getHeight() - child.getHeight() - topBound; 24 return Math.min(Math.max(top, topBound), bottomBound); 25 }
我們看下效果
然後我們可以再加上一個回彈的效果,就是你把babay拉倒一個位置 然後鬆手他會自動回彈到初始位置
其實思路很簡單 就是你鬆手的時候 回到初始的座標位置即可。
1 package com.example.administrator.viewdragertestapp; 2 3 import android.content.Context; 4 import android.graphics.Point; 5 import android.support.v4.widget.ViewDragHelper; 6 import android.util.AttributeSet; 7 import android.view.MotionEvent; 8 import android.view.View; 9 import android.widget.ImageView; 10 import android.widget.LinearLayout; 11 import android.widget.TextView; 12 13 /** 14 * Created by Administrator on 2015/8/12. 15 */ 16 public class DragLayout extends LinearLayout { 17 18 private ViewDragHelper mDragger; 19 20 private ViewDragHelper.Callback callback; 21 22 private ImageView iv1; 23 private ImageView iv2; 24 25 private Point initPointPosition = new Point(); 26 27 @Override 28 protected void onFinishInflate() { 29 iv1 = (ImageView) this.findViewById(R.id.iv1); 30 iv2 = (ImageView) this.findViewById(R.id.iv2); 31 super.onFinishInflate(); 32 33 } 34 35 public DragLayout(Context context) { 36 super(context); 37 38 } 39 40 public DragLayout(Context context, AttributeSet attrs) { 41 super(context, attrs); 42 callback = new DraggerCallBack(); 43 //第二個引數就是滑動靈敏度的意思 可以隨意設定 44 mDragger = ViewDragHelper.create(this, 1.0f, callback); 45 } 46 47 class DraggerCallBack extends ViewDragHelper.Callback { 48 49 //這個地方實際上函式返回值為true就代表可以滑動 為false 則不能滑動 50 @Override 51 public boolean tryCaptureView(View child, int pointerId) { 52 if (child == iv2) { 53 return false; 54 } 55 return true; 56 } 57 58 59 //這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。 60 // 我們要讓view滑動的範圍在我們的layout之內 61 //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。 62 //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍 63 //除此之外就是如果你的layout設定了padding的話,也可以讓子view的活動範圍在padding之內的. 64 65 @Override 66 public int clampViewPositionHorizontal(View child, int left, int dx) { 67 //取得左邊界的座標 68 final int leftBound = getPaddingLeft(); 69 //取得右邊界的座標 70 final int rightBound = getWidth() - child.getWidth() - leftBound; 71 //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left 72 //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值 73 //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值 74 return Math.min(Math.max(left, leftBound), rightBound); 75 } 76 77 //縱向的註釋就不寫了 自己體會 78 @Override 79 public int clampViewPositionVertical(View child, int top, int dy) { 80 final int topBound = getPaddingTop(); 81 final int bottomBound = getHeight() - child.getHeight() - topBound; 82 return Math.min(Math.max(top, topBound), bottomBound); 83 } 84 85 @Override 86 public void onViewReleased(View releasedChild, float xvel, float yvel) { 87 //鬆手的時候 判斷如果是這個view 就讓他回到起始位置 88 if (releasedChild == iv1) { 89 //這邊程式碼你跟進去去看會發現最終呼叫的是startScroll這個方法 所以我們就明白還要在computeScroll方法裡重新整理 90 mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y); 91 invalidate(); 92 } 93 } 94 } 95 96 @Override 97 public void computeScroll() { 98 if (mDragger.continueSettling(true)) { 99 invalidate(); 100 } 101 } 102 103 @Override 104 protected void onLayout(boolean changed, int l, int t, int r, int b) { 105 super.onLayout(changed, l, t, r, b); 106 //佈局完成的時候就記錄一下位置 107 initPointPosition.x = iv1.getLeft(); 108 initPointPosition.y = iv1.getTop(); 109 } 110 111 @Override 112 public boolean onInterceptTouchEvent(MotionEvent ev) { 113 //決定是否攔截當前事件 114 return mDragger.shouldInterceptTouchEvent(ev); 115 } 116 117 @Override 118 public boolean onTouchEvent(MotionEvent event) { 119 //處理事件 120 mDragger.processTouchEvent(event); 121 return true; 122 } 123 124 125 }
看下效果:
到這裡有人會發現 這樣做的話imageview就無法響應點選事件了。繼續修改這個程式碼讓iv可以響應點選事件並且可以響應
滑動事件。
首先修改xml 把click屬性設定為true 這個程式碼就不上了,然後修改我們的程式碼 其實就是增加2個函式
1 @Override 2 public int getViewHorizontalDragRange(View child) { 3 return getMeasuredWidth() - child.getMeasuredWidth(); 4 } 5 6 @Override 7 public int getViewVerticalDragRange(View child) { 8 return getMeasuredHeight()-child.getMeasuredHeight(); 9 }
然後看下效果:
這個地方 如果你學過android 事件傳遞的話很好理解,因為如果你子view可以響應點選事件的話,那說明你消費了這個事件。
如果你消費了這個事件話 就會先走dragger的 onInterceptTouchEvent這個方法。我們跟進去看看這個方法
1 case MotionEvent.ACTION_MOVE: { 2 if (mInitialMotionX == null || mInitialMotionY == null) break; 3 4 // First to cross a touch slop over a draggable view wins. Also report edge drags. 5 final int pointerCount = MotionEventCompat.getPointerCount(ev); 6 for (int i = 0; i < pointerCount; i++) { 7 final int pointerId = MotionEventCompat.getPointerId(ev, i); 8 final float x = MotionEventCompat.getX(ev, i); 9 final float y = MotionEventCompat.getY(ev, i); 10 final float dx = x - mInitialMotionX[pointerId]; 11 final float dy = y - mInitialMotionY[pointerId]; 12 13 final View toCapture = findTopChildUnder((int) x, (int) y); 14 final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy); 15 if (pastSlop) { 16 // check the callback's 17 // getView[Horizontal|Vertical]DragRange methods to know 18 // if you can move at all along an axis, then see if it 19 // would clamp to the same value. If you can't move at 20 // all in every dimension with a nonzero range, bail. 21 final int oldLeft = toCapture.getLeft(); 22 final int targetLeft = oldLeft + (int) dx; 23 final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, 24 targetLeft, (int) dx); 25 final int oldTop = toCapture.getTop(); 26 final int targetTop = oldTop + (int) dy; 27 final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, 28 (int) dy); 29 final int horizontalDragRange = mCallback.getViewHorizontalDragRange( 30 toCapture); 31 final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture); 32 if ((horizontalDragRange == 0 || horizontalDragRange > 0 33 && newLeft == oldLeft) && (verticalDragRange == 0 34 || verticalDragRange > 0 && newTop == oldTop)) { 35 break; 36 } 37 } 38 reportNewEdgeDrags(dx, dy, pointerId); 39 if (mDragState == STATE_DRAGGING) { 40 // Callback might have started an edge drag 41 break; 42 } 43 44 if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { 45 break; 46 } 47 } 48 saveLastMotion(ev); 49 break; 50 }
注意看29行到末尾 你會發現 只有當
horizontalDragRange 和verticalDragRange
大於0的時候 對應的move事件才會捕獲。否則就是丟棄直接丟給子view自己處理了
另外還有一個效果就是 假如我們的 baby被拉倒了邊界處,
我們的手指不需要拖動baby這個iv,手指直接在邊界的其他地方拖動此時也能把這個iv拖走。
這個效果其實也可以實現,無非就是捕捉你手指在邊界處的動作 然後傳給你要拖動的view即可。
程式碼非常簡單 兩行即可
再重寫一個回撥函式 然後加個監聽
1 @Override 2 public void onEdgeDragStarted(int edgeFlags, int pointerId) { 3 mDragger.captureChildView(iv1, pointerId); 4 }
1 mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
這個效果在模擬器上不知道為啥 滑鼠拖不動,GIF圖片我就不上了大家可以自己在手機裡跑一下就可以。
上面的那些效果實際上都是DrawerLayout 等類似抽屜效果裡經常用到的函式,有興趣的同學可以
看下原始碼。