1.简介 在上一篇文章SwipeBackLayout源代码分析 中,我们了解了ViewDragHelper
是可以帮助我们处理各种拖拽事件的类.使用好ViewDragHelper
能帮助我们做出各种酷炫的交互,今天我们就来分析一下ViewDragHelper
的使用与实现:
2.使用方法 我们这里就以翔总的这篇文章 中的例子来介绍一下ViewDragHelper
的使用.另外,本文中的demo可以在这里找到
首先我们创建一个DragLayout
类并继承自LinearLayout
,然后我们准备在DragLayout
放置三个View
第一个用来被我们拖动然后停止在松手的位置,第二个可以被我们拖动,松手的时候滑动到指定位置,第三个只可以通过触摸边缘来进行拖动,
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 public class DragLayout extends LinearLayout { private ViewDragHelper mDragger; private View mDragView; private View mAutoBackView; private View mEdgeTrackerView; private Point mAutoBackOriginPos = new Point(); public DragLayout (Context context) { this (context, null ); } public DragLayout (Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public DragLayout (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); initViewDragHelper(); } private void initViewDragHelper () { mDragger = ViewDragHelper.create(this ,myCallback); mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL); } ViewDragHelper.Callback myCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView (View child, int pointerId) { return child == mDragView || child == mAutoBackView; } @Override public void onViewReleased (View releasedChild, float xvel, float yvel) { if (releasedChild == mAutoBackView) { mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y); invalidate(); } } @Override public void onEdgeDragStarted (int edgeFlags, int pointerId) { mDragger.captureChildView(mEdgeTrackerView, pointerId); } @Override public int getViewHorizontalDragRange (View child) { return getMeasuredWidth() - child.getMeasuredWidth(); } @Override public int getViewVerticalDragRange (View child) { return getMeasuredHeight() - child.getMeasuredHeight(); } @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 computeScroll () { if (mDragger.continueSettling(true )) { invalidate(); } } @Override public boolean onInterceptTouchEvent (MotionEvent ev) { return mDragger.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent (MotionEvent event) { mDragger.processTouchEvent(event); return true ; } @Override protected void onFinishInflate () { super .onFinishInflate(); mDragView = getChildAt(0 ); mAutoBackView = getChildAt(1 ); mEdgeTrackerView = getChildAt(2 ); } @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(); } }
我们首先在构造方法里传入了当前类的对象和我们定义的ViewDragHelper.Callback
对象初始化了我们的ViewDragHelper
,然后我们希望所有的边缘触摸都能触发mEdgeTrackerView
的拖动,所以我们紧接着调用了mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
方法.
在我们定义的Callback
中,有多个回调方法,每个回调方法都有它的作用,在代码里注释比较清楚了,我们下面也会解析每一个Callback
中回调方法的作用.
第三步我们需要在onInterceptTouchEvent()
方法和onTouchEvent()
将事件委托给ViewDragHelper
去处理,这样ViewDragHelper
才能根据响应的事件并回调我们自己编写的Callback
接口来进行响应的处理,
由于ViewDragHelper
中的滑动是交给Srcoller
类来处理的所以这里我们要重写computeScroll()
方法,配合Scroller
完成滚动动画.
最后在onFinishInflate()
里获取到我们的View
对象即可.
3.类关系图 由于就一个类类图我们就不画了,但是作为一个强迫症患者,这个标题必须有…
4.源码分析 1.ViewDragHelper.Callback的实现 在分析ViewDragHelper
之前,我们先来分析一下Callback
的定义,看看Callback
都定义了哪些方法: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 public static abstract class Callback { public void onViewDragStateChanged (int state) {} public void onViewPositionChanged (View changedView, int left, int top, int dx, int dy) {} public void onViewCaptured (View capturedChild, int activePointerId) {} public void onViewReleased (View releasedChild, float xvel, float yvel) {} public void onEdgeTouched (int edgeFlags, int pointerId) {} public boolean onEdgeLock (int edgeFlags) { return false ; } public void onEdgeDragStarted (int edgeFlags, int pointerId) {} public int getOrderedChildIndex (int index) { return index; } public int getViewHorizontalDragRange (View child) { return 0 ; } public int getViewVerticalDragRange (View child) { return 0 ; } public abstract boolean tryCaptureView (View child, int pointerId) ; public int clampViewPositionHorizontal (View child, int left, int dx) { return 0 ; } public int clampViewPositionVertical (View child, int top, int dy) { return 0 ; } }
想必注释已经很清楚了,正是这些回调方法,再结合ViewDragHelper
中的各种方法,来帮助我们实现各种各样的拖拽的效果。
2.shouldInterceptTouchEvent()方法的实现 在这里我们假设大家都清楚了Android
的事件分发机制,如果不清楚请看这里 ,要想处理触摸事件,我们需要在onInterceptTouchEvent(MotionEvent ev)
方法里判断是否需要拦截这次触摸事件,如果此方法返回true
则触摸事件将会交给onTouchEvent(MotionEvent event)
处理,这样我们就能处理触摸事件了,所以我们在上面的使用方法里会这样写:
1 2 3 4 5 6 7 8 9 10 11 @Override public boolean onInterceptTouchEvent (MotionEvent ev) { return mDragger.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent (MotionEvent event) { mDragger.processTouchEvent(event); return true ; }
这样就将是否拦截触摸事件,以及处理触摸事件委托给ViewDragHelper
来处理了,所以我们先来看看ViewDragHelper
中shouldInterceptTouchEvent();
方法的实现:
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 public boolean shouldInterceptTouchEvent (MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); if (action == MotionEvent.ACTION_DOWN) { cancel(); } if (mVelocityTracker == null ) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); final int pointerId = MotionEventCompat.getPointerId(ev, 0 ); saveInitialMotion(x, y, pointerId); final View toCapture = findTopChildUnder((int ) x, (int ) y); if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0 ) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break ; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); final float x = MotionEventCompat.getX(ev, actionIndex); final float y = MotionEventCompat.getY(ev, actionIndex); saveInitialMotion(x, y, pointerId); if (mDragState == STATE_IDLE) { final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0 ) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } } else if (mDragState == STATE_SETTLING) { final View toCapture = findTopChildUnder((int ) x, (int ) y); if (toCapture == mCapturedView) { tryCaptureViewForDrag(toCapture, pointerId); } } break ; } case MotionEvent.ACTION_MOVE: { if (mInitialMotionX == null || mInitialMotionY == null ) break ; final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0 ; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; final View toCapture = findTopChildUnder((int ) x, (int ) y); final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy); if (pastSlop) { final int oldLeft = toCapture.getLeft(); final int targetLeft = oldLeft + (int ) dx; final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, targetLeft, (int ) dx); final int oldTop = toCapture.getTop(); final int targetTop = oldTop + (int ) dy; final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, (int ) dy); final int horizontalDragRange = mCallback.getViewHorizontalDragRange( toCapture); final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture); if ((horizontalDragRange == 0 || horizontalDragRange > 0 && newLeft == oldLeft) && (verticalDragRange == 0 || verticalDragRange > 0 && newTop == oldTop)) { break ; } } reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { break ; } if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { break ; } } saveLastMotion(ev); break ; } case MotionEventCompat.ACTION_POINTER_UP: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); clearMotionHistory(pointerId); break ; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { cancel(); break ; } } return mDragState == STATE_DRAGGING; }
上面就是整个shouldInterceptTouchEvent()
的实现,上面的注释也足够清楚了,我们这里就先不分析某一种触摸事件,大家可以看到我上面留了几个TODO,下文会一起分析,这里我假设大家都已经对触摸事件分发处理都有充分的理解了,我们下面就直接看ViewDragHelper
里processTouchEvent()
方法的实现.
3.processTouchEvent()方法的实现 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 public void processTouchEvent (MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); ...(省去部分代码) switch (action) { case MotionEvent.ACTION_DOWN: { ...(省去部分代码) break ; } case MotionEventCompat.ACTION_POINTER_DOWN: { ...(省去部分代码) break ; } case MotionEvent.ACTION_MOVE: { if (mDragState == STATE_DRAGGING) { final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, index); final float y = MotionEventCompat.getY(ev, index); final int idx = (int ) (x - mLastMotionX[mActivePointerId]); final int idy = (int ) (y - mLastMotionY[mActivePointerId]); dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); saveLastMotion(ev); } else { final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0 ; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { break ; } final View toCapture = findTopChildUnder((int ) x, (int ) y); if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) { break ; } } saveLastMotion(ev); } break ; } case MotionEventCompat.ACTION_POINTER_UP: { final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { int newActivePointer = INVALID_POINTER; final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0 ; i < pointerCount; i++) { final int id = MotionEventCompat.getPointerId(ev, i); if (id == mActivePointerId) { continue ; } final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); if (findTopChildUnder((int ) x, (int ) y) == mCapturedView && tryCaptureViewForDrag(mCapturedView, id)) { newActivePointer = mActivePointerId; break ; } } if (newActivePointer == INVALID_POINTER) { releaseViewForPointerUp(); } } clearMotionHistory(pointerId); break ; } case MotionEvent.ACTION_UP: { if (mDragState == STATE_DRAGGING) { releaseViewForPointerUp(); } cancel(); break ; } case MotionEvent.ACTION_CANCEL: { if (mDragState == STATE_DRAGGING) { dispatchViewReleased(0 , 0 ); } cancel(); break ; } } }
上面就是processTouchEvent()
方法的实现,我们省去了部分大致与shouldInterceptTouchEvent()
相同的逻辑代码,通过事件传递机制我们知道,如果程序已经进入到processTouchEvent()
中,也就意味着触摸事件就不会再向下传递,都会交给此方法处理,所以在这里我们就需要处理拖拽事件了,通过上面的注释,我们也看到了在MotionEvent.ACTION_MOVE
,MotionEventCompat.ACTION_POINTER_UP
,MotionEvent.ACTION_UP
和MotionEvent.ACTION_CANCEL
都分别进行了处理 ,我们知道触摸事件大致的流程是:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
再配合事件的分发机制,我们就能很清晰的分析出一次完整的事件调用过程,所以整个ViewDragHelper
的拖拽过程也能很清晰的分为三个步骤:
捕获拖拽目标View -> 拖拽目标View -> 处理目标View释放操作
最后我们再分析上面两段代码的6个TODO:
4.saveInitialMotion()方法 1 2 3 4 5 6 7 8 9 10 11 12 private void saveInitialMotion (float x, float y, int pointerId) { ensureMotionHistorySizeForId(pointerId); mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; mInitialEdgesTouched[pointerId] = getEdgesTouched((int ) x, (int ) y); mPointersDown |= 1 << pointerId; }
5.findTopChildUnder()方法 1 2 3 4 5 6 7 8 9 10 11 public View findTopChildUnder (int x, int y) { final int childCount = mParentView.getChildCount(); for (int i = childCount - 1 ; i >= 0 ; i--) { final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() && y < child.getBottom()) { return child; } } return null ; }
代码很简单就是根据x
和y
坐标和来找到指定View
,注意这里回调了callback
中的getOrderedChildIndex()
方法,所以我们可以在这里返回指定的View
的index
.
6.checkTouchSlop()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private boolean checkTouchSlop (View child, float dx, float dy) { if (child == null ) { return false ; } final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0 ; final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0 ; if (checkHorizontal && checkVertical) { return dx * dx + dy * dy > mTouchSlop * mTouchSlop; } else if (checkHorizontal) { return Math.abs(dx) > mTouchSlop; } else if (checkVertical) { return Math.abs(dy) > mTouchSlop; } return false ; }
用来根据mTouchSlop
最小拖动的距离来判断是否属于拖动,mTouchSlop
根据我们设定的灵敏度决定.
7.tryCaptureViewForDrag()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 boolean tryCaptureViewForDrag (View toCapture, int pointerId) { if (toCapture == mCapturedView && mActivePointerId == pointerId) { return true ; } if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { mActivePointerId = pointerId; captureChildView(toCapture, pointerId); return true ; } return false ; }
可以看到如果可以捕获View
则调用了captureChildView()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void captureChildView (View childView, int activePointerId) { if (childView.getParent() != mParentView) { throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + "of the ViewDragHelper's tracked parent view (" + mParentView + ")" ); } mCapturedView = childView; mActivePointerId = activePointerId; mCallback.onViewCaptured(childView, activePointerId); setDragState(STATE_DRAGGING); }
如果程序执行到这里,就证明View
已经处于拖拽状态了,后续的触摸操作,将直接根据mDragState
为STATE_DRAGGING
的状态处理.
8.dragTo()方法的实现 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 private void dragTo (int left, int top, int dx, int dy) { int clampedX = left; int clampedY = top; final int oldLeft = mCapturedView.getLeft(); final int oldTop = mCapturedView.getTop(); if (dx != 0 ) { clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft); } if (dy != 0 ) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop); } if (dx != 0 || dy != 0 ) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); } }
因为dragTo()
方法是在processTouchEvent()
中的MotionEvent.ACTION_MOVE case
被调用所以当程序运行到这里时View
就会不断的被拖动了。如果一旦手指释放则最终会调用releaseViewForPointerUp()
方法
8.releaseViewForPointerUp()方法的实现 1 2 3 4 5 6 7 8 9 10 11 12 private void releaseViewForPointerUp () { mVelocityTracker.computeCurrentVelocity(1000 , mMaxVelocity); final float xvel = clampMag( VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); final float yvel = clampMag( VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); dispatchViewReleased(xvel, yvel); }
计算完加速度后就调用了dispatchViewReleased()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void dispatchViewReleased (float xvel, float yvel) { mReleaseInProgress = true ; mCallback.onViewReleased(mCapturedView, xvel, yvel); mReleaseInProgress = false ; if (mDragState == STATE_DRAGGING) { setDragState(STATE_IDLE); } }
所以最后释放后的处理交给了callback
中的onViewReleased()
方法,如果我们什么都不做,那么这个被拖拽的View
就是停止在当前位置,或者我们可以调用ViewDragHelper
提供给我们的这几个方法:
settleCapturedViewAt(int finalLeft, int finalTop) 以松手前的滑动速度为初速动,让捕获到的View自动滚动到指定位置。只能在Callback的onViewReleased()中调用。
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 以松手前的滑动速度为初速动,让捕获到的View在指定范围内fling。只能在Callback的onViewReleased()中调用。
smoothSlideViewTo(View child, int finalLeft, int finalTop) 指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用。
引用自这篇文章 ,具体释放后的原理我们就不分析了,其实就是配合Scroller
这个类来实现,具体也可以参照上面这篇文章。好,我们关于ViewDragHelper
的源码分析就到这里.
5.开源项目中的使用 ViewDragHelper
在各种关于拖拽和各种手势动画的开源库中使用广泛,我这里就简要列出一些,大家可以多去看看是如何使用ViewDragHelper
的:
6.个人评价 ViewDragHelper
的出现,大大简化了我们开发相关触摸和拖拽功能的复杂度和代码量,帮助我们比较容易的实现各种效果,让我们开发酷炫的交互更加容易了。但是从一些开源项目中发现,ViewDragHelper
中还是有一些不足之处,比如给Scroller
提供了一个固定的Interpolator
,导致如果我们想实现例如反弹效果的话,还要把ViewDragHelper
的代码拷贝一份并修改Interpolator
,这样做肯定是不太好的.当然建议我们自己修改一个ViewDragHelper
后如果项目里有多处使用,可以包装成一个提供给我们自己项目的模块使用,防止出现更多的多余代码.