ViewDragHelper这个类。非常方便的帮我们实现了自定义View(ViewGroup)滑动相关的功能.
下面一步一步学习:
先看一个简单效果 一个layout里有2个图片 其中有一个可以滑动 一个不能滑
代码:
xml文件:
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 <app.com.drag.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/p1"/> 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/p2"/> 25 26 27 </app.com.drag.DragLayout> 28 29 </LinearLayout>
然后就是自定义的layout 如何实现2个子view 一个可以滑动 一个不能滑动的
1 public class DragLayout extends LinearLayout { 2 3 private ViewDragHelper mDragger; 4 5 private ViewDragHelper.Callback callback; 6 7 private ImageView iv1; 8 private ImageView iv2; 9 10 @Override 11 protected void onFinishInflate() { 12 iv1 = (ImageView) this.findViewById(R.id.iv1); 13 iv2 = (ImageView) this.findViewById(R.id.iv2); 14 super.onFinishInflate(); 15 16 } 17 18 public DragLayout(Context context) { 19 super(context); 20 21 } 22 23 public DragLayout(Context context, AttributeSet attrs) { 24 super(context, attrs); 25 callback = new DraggerCallBack(); 26 //第二个参数就是滑动灵敏度的意思 可以随意设置 27 mDragger = ViewDragHelper.create(this, 1.0f, callback); 28 } 29 30 class DraggerCallBack extends ViewDragHelper.Callback { 31 32 //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动 33 @Override 34 public boolean tryCaptureView(View child, int pointerId) { 35 if (child == iv2) { 36 return false; 37 } 38 return true; 39 } 40 41 @Override 42 public int clampViewPositionHorizontal(View child, int left, int dx) { 43 return left; 44 } 45 46 @Override 47 public int clampViewPositionVertical(View child, int top, int dy) { 48 return top; 49 } 50 } 51 52 53 @Override 54 public boolean onInterceptTouchEvent(MotionEvent ev) { 55 //决定是否拦截当前事件 56 return mDragger.shouldInterceptTouchEvent(ev); 57 } 58 59 @Override 60 public boolean onTouchEvent(MotionEvent event) { 61 //处理事件 62 mDragger.processTouchEvent(event); 63 return true; 64 } 65 66 67 }
然后再完善一下这个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 public class DragLayout extends LinearLayout { 2 3 private ViewDragHelper mDragger; 4 5 private ViewDragHelper.Callback callback; 6 7 private ImageView iv1; 8 private ImageView iv2; 9 10 private Point initPointPosition = new Point(); 11 12 @Override 13 protected void onFinishInflate() { 14 iv1 = (ImageView) this.findViewById(R.id.iv1); 15 iv2 = (ImageView) this.findViewById(R.id.iv2); 16 super.onFinishInflate(); 17 18 } 19 20 public DragLayout(Context context) { 21 super(context); 22 23 } 24 25 public DragLayout(Context context, AttributeSet attrs) { 26 super(context, attrs); 27 callback = new DraggerCallBack(); 28 //第二个参数就是滑动灵敏度的意思 可以随意设置 29 mDragger = ViewDragHelper.create(this, 1.0f, callback); 30 } 31 32 class DraggerCallBack extends ViewDragHelper.Callback { 33 34 //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动 35 @Override 36 public boolean tryCaptureView(View child, int pointerId) { 37 if (child == iv2) { 38 return false; 39 } 40 return true; 41 } 42 43 44 //这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。 45 // 我们要让view滑动的范围在我们的layout之内 46 //实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。 47 //如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围 48 //除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的. 49 50 @Override 51 public int clampViewPositionHorizontal(View child, int left, int dx) { 52 //取得左边界的坐标 53 final int leftBound = getPaddingLeft(); 54 //取得右边界的坐标 55 final int rightBound = getWidth() - child.getWidth() - leftBound; 56 //这个地方的含义就是 如果left的值 在leftbound和rightBound之间 那么就返回left 57 //如果left的值 比 leftbound还要小 那么就说明 超过了左边界 那我们只能返回给他左边界的值 58 //如果left的值 比rightbound还要大 那么就说明 超过了右边界,那我们只能返回给他右边界的值 59 return Math.min(Math.max(left, leftBound), rightBound); 60 } 61 62 //纵向的注释就不写了 自己体会 63 @Override 64 public int clampViewPositionVertical(View child, int top, int dy) { 65 final int topBound = getPaddingTop(); 66 final int bottomBound = getHeight() - child.getHeight() - topBound; 67 return Math.min(Math.max(top, topBound), bottomBound); 68 } 69 70 @Override 71 public void onViewReleased(View releasedChild, float xvel, float yvel) { 72 //松手的时候 判断如果是这个view 就让他回到起始位置 73 if (releasedChild == iv1) { 74 //这边代码你跟进去去看会发现最终调用的是startScroll这个方法 所以我们就明白还要在computeScroll方法里刷新 75 mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y); 76 invalidate(); 77 } 78 } 79 } 80 81 @Override 82 public void computeScroll() { 83 if (mDragger.continueSettling(true)) { 84 invalidate(); 85 } 86 } 87 88 @Override 89 protected void onLayout(boolean changed, int l, int t, int r, int b) { 90 super.onLayout(changed, l, t, r, b); 91 //布局完成的时候就记录一下位置 92 initPointPosition.x = iv1.getLeft(); 93 initPointPosition.y = iv1.getTop(); 94 } 95 96 @Override 97 public boolean onInterceptTouchEvent(MotionEvent ev) { 98 //决定是否拦截当前事件 99 return mDragger.shouldInterceptTouchEvent(ev); 100 } 101 102 @Override 103 public boolean onTouchEvent(MotionEvent event) { 104 //处理事件 105 mDragger.processTouchEvent(event); 106 return true; 107 } 108 109 110 }
看下效果:
到这里有人会发现 这样做的话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即可。
代码非常简单 两行即可
再重写一个回调函数 然后加个监听
@Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { mDragger.captureChildView(iv1, pointerId); }
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
这个效果在模拟器上不知道为啥 鼠标拖不动,GIF图片我就不上了大家可以自己在手机里跑一下就可以。
上面的那些效果实际上都是DrawerLayout 等类似抽屉效果里经常用到的函数,有兴趣的同学可以看下源码。
上面的博客内容是参考http://www.cnblogs.com/punkisnotdead/p/4724825.html 写的.只是记录下,自己日后方便看,有问题大家一起交流
下面在总结下ViewDragHelper的调用时机:
shouldInterceptTouchEvent: DOWN: getOrderedChildIndex(findTopChildUnder) ->onEdgeTouched MOVE: getOrderedChildIndex(findTopChildUnder) ->getViewHorizontalDragRange & getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次) ->clampViewPositionHorizontal& clampViewPositionVertical ->onEdgeDragStarted ->tryCaptureView ->onViewCaptured ->onViewDragStateChanged processTouchEvent: DOWN: getOrderedChildIndex(findTopChildUnder) ->tryCaptureView ->onViewCaptured ->onViewDragStateChanged ->onEdgeTouched MOVE: ->STATE==DRAGGING:dragTo ->STATE!=DRAGGING: onEdgeDragStarted ->getOrderedChildIndex(findTopChildUnder) ->getViewHorizontalDragRange& getViewVerticalDragRange(checkTouchSlop) ->tryCaptureView ->onViewCaptured ->onViewDragStateChanged
当然整个过程可能会存在很多判断不成立的情况.
这个总结参考的http://blog.csdn.net/lmj623565791/article/details/46858663 博客