ViewDragHelper源码解析

ViewDragHelper实例的创建

ViewDragHelper重载了两个create()静态方法
public static ViewDragHelper create(ViewGroup forParent, Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); }

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { final ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); return helper; }
forParent是我们自定义的ViewGroup,cb是控制子View拖拽需要的回调对象,sensitivity是用来调节mTouchSlop的值。sensitivity越大,mTouchSlop越小,对滑动的检测就越敏感。

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { if (forParent == null) { throw new IllegalArgumentException("Parent view may not be null"); } if (cb == null) { throw new IllegalArgumentException("Callback may not be null"); } mParentView = forParent; mCallback = cb; //ViewConfiguration类里定义了View相关的一系列时间、大小、距离等常量 final ViewConfiguration vc = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; //mEdgeSize表示边缘触摸的范围 mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); mTouchSlop = vc.getScaledTouchSlop(); //mMaxVelocity、mMinVelocity是fling时的最大、最小速率,单位是像素每秒 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); //mScroller是View滚动的辅助类 mScroller = ScrollerCompat.create(context, sInterpolator);}

对Touch事件的处理

ViewDragHelper使用
当mParentView(自定义ViewGroup)被触摸时,首先会调用mParentView的onInterceptTouchEvent(MotionEvent ev),接着就调用shouldInterceptTouchEvent(MotionEvent ev)
public boolean shouldInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); if (action == MotionEvent.ACTION_DOWN) { // Reset things for a new event stream, just in case we didn't get // the whole previous stream. cancel(); } if (mVelocityTracker == null) { //mVelocityTracker记录下触摸的各个点信息,稍后可以用来计算本次滑动 //的速率,每次发生ACTION_DOWN事件都会调用cancel(),而在cancel() //方法里mVelocityTracker又被清空了,所以mVelocityTracker 记录下的 //是本次ACTION_DOWN事件直至ACTION_UP事件发生后(下次 //ACTION_DOWN事件发生前)的所有触摸点的信息 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)保存手势的初始信息,即 //ACTION_DOWN发生时的触摸点坐标(x、y)、触摸手指编号 //(pointerId),如果触摸到了mParentView的边缘还会记录触摸的是哪 //个边缘。 saveInitialMotion(x, y, pointerId); //调用findTopChildUnder((int) x, (int) y)来获取当前触摸点下最顶层的子 //View final View toCapture = findTopChildUnder((int) x, (int) y); // Catch a settling view if possible. //mDragState成员变量,它共有三种取值: //STATE_IDLE:所有的View处于静止空闲状态 //STATE_DRAGGING:某个View正在被用户拖动(用户正在与设备交互) //STATE_SETTLING:某个View正在安置状态中(用户并没有交互操作), //就是自动滚动的过程中mCapturedView默认为null,所以一开始不会执行 //这里的代码,mDragState处于STATE_SETTLING状态时才会执行 //tryCaptureViewForDrag(),执行的情况到后面再分析,这里先跳过 if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { //Callback.onEdgeTouched向外部通知mParentView的某些边缘被触摸到 //了,mInitialEdgesTouched是在刚才调用过的saveInitialMotion方法里 //进行赋值的 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } // 其他case暂且省略 } //ACTION_DOWN 部分处理完了,跳过switch语句块,剩下的代码就只有 //return mDragState == STATE_DRAGGING;。在ACTION_DOWN部分 //没有对mDragState进行赋值,其默认值为STATE_IDLE,所以此处返 //false。 return mDragState == STATE_DRAGGING;}

findTopChildUnder源码:
如果在同一个位置有两个子View重叠,想要让下层的子View被选中,那么就要实现Callback里的getOrderedChildIndex(int index)方法来改变查找子View的顺序;例如topView(上层View)的index是4,bottomView(下层View)的index是3,按照正常的遍历查找方式(getOrderedChildIndex()默认直接返回index),会选择到topView。所以在重叠view的情况下重写getOrderedChildIndex
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; }

那么返回false后接下来应该是会调用哪个方法呢,根据,ViewGroup的Touch事件的分发机制的解析,接下来会在mParentView的所有子View中寻找响应这个Touch事件的View(会调用每个子View的dispatchTouchEvent()方法,dispatchTouchEvent里一般又会调用onTouchEvent());
如果没有子View消费这次事件(子View的dispatchTouchEvent()返回都是false),会调用mParentView的super.dispatchTouchEvent(ev),即View中的dispatchTouchEvent(ev),然后调用mParentView的onTouchEvent()方法,再调用ViewDragHelper的processTouchEvent(MotionEvent ev)方法。此时(ACTION_DOWN事件发生时)mParentView的onTouchEvent()要返回true,onTouchEvent()才能继续接受到接下来的ACTION_MOVE、ACTION_UP等事件,否则无法完成拖动(除了ACTION_DOWN外的其他事件发生时返回true或false都不会影响接下来的事件接受),因为拖动的相关代码是写在processTouchEvent()里的ACTION_MOVE部分的。要注意的是返回true后mParentView的onInterceptTouchEvent()就不会收到后续的ACTION_MOVE、ACTION_UP等事件了。

如果有子View消费了本次ACTION_DOWN事件,mParentView的onTouchEvent()就收不到ACTION_DOWN事件了,也就是ViewDragHelper的processTouchEvent(MotionEvent ev)收不到ACTION_DOWN事件了。不过只要该View没有调用过requestDisallowInterceptTouchEvent(true),mParentView的onInterceptTouchEvent()的ACTION_MOVE部分还是会执行的,如果在此时返回了true拦截了ACTION_MOVE事件,processTouchEvent()里的ACTION_MOVE部分也就会正常执行,拖动也就没问题了。onInterceptTouchEvent()的ACTION_MOVE部分具体做了怎样的处理,稍后再来解析。

接下来对这两种情况逐一解析。

假设没有子View消费这次事件,根据刚才的分析最终就会调用processTouchEvent(MotionEvent ev)的ACTION_DOWN部分
跟shouldInterceptTouchEvent()里ACTION_DOWN那部分基本一致,唯一区别就是这里没有约束条件直接调用了tryCaptureViewForDrag()方法.
boolean tryCaptureViewForDrag(View toCapture, int pointerId) { if (toCapture == mCapturedView && mActivePointerId == pointerId) { // Already done! return true; } if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { mActivePointerId = pointerId; captureChildView(toCapture, pointerId); return true; } return false;}
Callback的tryCaptureView(View child, int pointerId)方法,把当前触摸到的View和触摸手指编号传递了过去,在tryCaptureView()中决定是否需要拖动当前触摸到的View,如果要拖动当前触摸到的View就在tryCaptureView()中返回true,让ViewDragHelper把当前触摸的View捕获下来,接着就调用了captureChildView(toCapture, pointerId)方法

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);}
在captureChildView(toCapture, pointerId)中将要拖动的View和触摸的手指编号记录下来,并调用Callback的onViewCaptured(childView, activePointerId)通知外部有子View被捕获到了,再调用setDragState()设置当前的状态为STATE_DRAGGING,看setDragState()源码:
void setDragState(int state) { if (mDragState != state) { mDragState = state; mCallback.onViewDragStateChanged(state); if (mDragState == STATE_IDLE) { mCapturedView = null; } }}
状态改变后会调用Callback的onViewDragStateChanged()通知状态的变化。
假设ACTION_DOWN发生后在mParentView的onTouchEvent()返回了true,接下来就会执行ACTION_MOVE部分
public void processTouchEvent(MotionEvent ev) { switch (action) { // 省略其他case... 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 { // Check to see if any pointer is now over a draggable view. 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) { // Callback might have started an edge drag. break; } final View toCapture = findTopChildUnder((int) x, (int) y); if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); } break; }
如果一直没松手,这部分代码会一直调用。这里先判断mDragState是否为STATE_DRAGGING,而唯一调用setDragState(STATE_DRAGGING)的地方就是tryCaptureViewForDrag()了,刚才在ACTION_DOWN里调用过tryCaptureViewForDrag(),现在又要分两种情况。如果刚才在ACTION_DOWN里捕获到要拖动的View,那么就执行if部分的代码,这个稍后解析,先考虑没有捕获到的情况。没有捕获到的话,mDragState依然是STATE_IDLE,然后会执行else部分的代码。这里主要就是检查有没有哪个手指触摸到了要拖动的View上,触摸上了就尝试捕获它,然后让mDragState变为STATE_DRAGGING,之后就会执行if部分的代码了,这个稍后解析,先考虑没有捕获到的情况。没有捕获到的话,mDragState依然是STATE_IDLE,然后会执行else部分的代码。这里主要就是检查有没有哪个手指触摸到了要拖动的View上,触摸上了就尝试捕获它,然后让mDragState变为STATE_DRAGGING,之后就会执行if部分的代码了。这里还有两个方法涉及到了Callback里的方法,需要来解析一下,分别是reportNewEdgeDrags()和checkTouchSlop(),先看reportNewEdgeDrags():
reportNewEdgeDrags()对四个边缘都做了一次检查,检查是否在某些边缘产生拖动了,如果有拖动,就将有拖动的边缘记录在mEdgeDragsInProgress中,再调用Callback的onEdgeDragStarted(int edgeFlags, int pointerId)通知某个边缘开始产生拖动了。虽然reportNewEdgeDrags()会被调用很多次(因为processTouchEvent()的ACTION_MOVE部分会执行很多次),但mCallback.onEdgeDragStarted(dragsStarted, pointerId)只会调用一次,具体的要看checkNewEdgeDrag()这个方法:
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { final float absDelta = Math.abs(delta); final float absODelta = Math.abs(odelta); if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || (mEdgeDragsLocked[pointerId] & edge) == edge || (mEdgeDragsInProgress[pointerId] & edge) == edge || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { return false; } if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { mEdgeDragsLocked[pointerId] |= edge; return false; } return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;}

  • checkNewEdgeDrag()返回true表示在指定的edge(边缘)开始产生拖动了。
  • 方法的两个参数delta和odelta需要解释一下,odelta里的o应该代表opposite,这是什么意思呢,以reportNewEdgeDrags()里调用checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)为例,我们要监测左边缘的触摸情况,所以主要监测的是x轴方向上的变化,这里delta为dx,odelta为dy,也就是说delta是指我们主要监测的方向上的变化,odelta是另外一个方向上的变化,后面要判断假另外一个方向上的变化是否要远大于主要方向上的变化,所以需要另外一个方向上的距离变化的值。
  • mInitialEdgesTouched是在ACTION_DOWN部分的saveInitialMotion()里生成的,ACTION_DOWN发生时触摸到的边缘会被记录在mInitialEdgesTouched中。如果ACTION_DOWN发生时没有触摸到边缘,或者触摸到的边缘不是指定的edge,就直接返回false了。
  • mTrackingEdges是由setEdgeTrackingEnabled(int edgeFlags)设置的,当我们想要追踪监听边缘触摸时才需要调用setEdgeTrackingEnabled(int edgeFlags),如果我们没有调用过它,这里就直接返回false了。
  • mEdgeDragsLocked它在这个方法里被引用了多次,它在整个ViewDragHelper里唯一被赋值的地方就是这里的第12行,所以默认值是0,第6行mEdgeDragsLocked[pointerId] & edge) == edge执行的结果是false。我们再跳到11到14行看看,absDelta < absODelta * 0.5f的意思是检查在次要方向上移动的距离是否远超过主要方向上移动的距离,如果是再调用Callback的onEdgeLock(edge)检查是否需要锁定某个边缘,如果锁定了某个边缘,那个边缘就算触摸到了也不会被记录在mEdgeDragsInProgress里了,也不会收到Callback的onEdgeDragStarted()通知了。并且将锁定的边缘记录在mEdgeDragsLocked变量里,再次调用本方法时就会在第6行进行判断了,第6行里如果检测到给定的edge被锁定,就直接返回false了。
  • 回到第7行的(mEdgeDragsInProgress[pointerId] & edge) == edge,mEdgeDragsInProgress是保存已发生过拖动事件的边缘的,如果给定的edge已经保存过了,那就没必要再检测其他东西了,直接返回false了。
  • 第8行(absDelta <= mTouchSlop && absODelta <= mTouchSlop)很简单了,就是检查本次移动的距离是不是太小了,太小就不处理了。
  • 最后一句返回的时候再次检查给定的edge有没有记录过,确保了每个边缘只会调用一次reportNewEdgeDrags的mCallback.onEdgeDragStarted(dragsStarted, pointerId)

checkTouchSlop()方法主要就是检查手指移动的距离有没有超过触发处理移动事件的最短距离(mTouchSlop)了,注意dx和dy指的是当前触摸点到ACTION_DOWN触摸到的点的距离。这里先检查Callback的getViewHorizontalDragRange(child)和getViewVerticalDragRange(child)是否大于0,如果想让某个View在某个方向上滑动,就要在那个方向对应的方法里返回大于0的数。否则在processTouchEvent()的ACTION_MOVE部分就不会调用tryCaptureViewForDrag()来捕获当前触摸到的View了,拖动也就没办法进行了。

回到processTouchEvent()的ACTION_MOVE部分,假设现在我们的手指已经滑动到可以被捕获到的View上了,也都正常的实现了Callback中的相关方法,让tryCaptureViewForDrag()正常的捕获到触摸到的View了,下一次ACTION_MOVE时就执行if部分的代码了,也就是开始不停的调用dragTo()对mCaptureView进行真正拖动了,看dragTo()方法:
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); mCapturedView.offsetLeftAndRight(clampedX - oldLeft); } if (dy != 0) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); mCapturedView.offsetTopAndBottom(clampedY - oldTop); } if (dx != 0 || dy != 0) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); }}
参数dx和dy是前后两次ACTION_MOVE移动的距离,left和top分别为mCapturedView.getLeft() + dx, mCapturedView.getTop() + dy,也就是期望的移动后的坐标,对View的getLeft()等方法不理解的请参阅Android View坐标getLeft, getRight, getTop, getBottom
这里通过调用offsetLeftAndRight()和offsetTopAndBottom()来完成对mCapturedView移动,这两个是View中定义的方法,看它们的源码就知道内部是通过改变View的mLeft、mRight、mTop、mBottom,即改变View在父容器中的坐标位置,达到移动View的效果,所以如果调用mCapturedView的layout(int l, int t, int r, int b)方法也可以实现移动View的效果。
具体要移动到哪里,由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()来决定的,如果不想在水平方向上移动,在clampViewPositionHorizontal(View child, int left, int dx)里直接返回child.getLeft()就可以了,这样clampedX - oldLeft的值为0,这里调用mCapturedView.offsetLeftAndRight(clampedX - oldLeft)就不会起作用了。垂直方向上同理。
最后会调用Callback的onViewPositionChanged(mCapturedView, clampedX, clampedY,clampedDx, clampedDy)通知捕获到的View位置改变了,并把最终的坐标(clampedX、clampedY)和最终的移动距离(clampedDx、 clampedDy)传递过去。
ACTION_MOVE部分就算告一段落了,接下来应该是用户松手触发ACTION_UP,或者是达到某个条件导致后续的ACTION_MOVE被mParentView的上层View给拦截了而收到ACTION_CANCEL,一起来看这两个部分:
public void processTouchEvent(MotionEvent ev) { // 省略 switch (action) { // 省略其他case case MotionEvent.ACTION_UP: { if (mDragState == STATE_DRAGGING) { releaseViewForPointerUp(); } cancel(); break; } case MotionEvent.ACTION_CANCEL: { if (mDragState == STATE_DRAGGING) { dispatchViewReleased(0, 0); } cancel(); break; } }}
这两个部分都是重置所有的状态记录,并通知View被放开了,再看下releaseViewForPointerUp()和dispatchViewReleased()的源码:
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);}
releaseViewForPointerUp()里也调用了dispatchViewReleased(),只不过传递了速率给它,这个速率就是由processTouchEvent()的mVelocityTracker追踪算出来的。再看dispatchViewReleased():
private void dispatchViewReleased(float xvel, float yvel) { mReleaseInProgress = true; mCallback.onViewReleased(mCapturedView, xvel, yvel); mReleaseInProgress = false; if (mDragState == STATE_DRAGGING) { // onViewReleased didn't call a method that would have changed //this. Go idle. setDragState(STATE_IDLE); }}
这里调用Callback的onViewReleased(mCapturedView, xvel, yvel)通知外部捕获到的View被释放了,而在onViewReleased()前后有个mReleaseInProgress值得注意,注释里说唯一可以调用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()里了。

首先这两个方法是干什么的呢。在现实生活中保龄球的打法是,先做扔的动作让球的速度达到最大,然后突然松手,由于惯性,保龄球就以最后松手前的速度为初速度抛出去了,直至自然停止,或者撞到边界停止,这种效果叫fling。
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)就是对捕获到的View做出这种fling的效果,用户在屏幕上滑动松手之前也会有一个滑动的速率。fling也引出来的一个问题,就是不知道View最终会滚动到哪个位置,最后位置是在启动fling时根据最后滑动的速度来计算的(flingCapturedView的四个参数int minLeft, int minTop, int maxLeft, int maxTop可以限定最终位置的范围),假如想要让View滚动到指定位置应该怎么办,答案就是使用settleCapturedViewAt(int finalLeft, int finalTop)。

为什么唯一可以调用settleCapturedViewAt()和flingCapturedView()的地方是Callback的onViewReleased()呢?看看它们的源码
public boolean settleCapturedViewAt(int finalLeft, int finalTop) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased"); } return forceSettleCapturedViewAt(finalLeft,finalTop,(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker,mActivePointerId),(int)VelocityTrackerCompat.getYVelocity(mVelocityTracker,mActivePointerId));}

public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + "Callback#onViewReleased"); } mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), minLeft, maxLeft, minTop, maxTop); setDragState(STATE_SETTLING);}
这两个方法里一开始都会判断mReleaseInProgress为false,如果为false就会抛一个IllegalStateException异常,而mReleaseInProgress唯一为true的时候就是在dispatchViewReleased()里调用onViewReleased()的时候。
Scroller的用法请参阅Android中滑屏实现----手把手教你如何实现触摸滑屏以及Scroller类详解 ,或者自行解读Scroller源码,代码量不多。
ViewDragHelper还有一个移动View的方法是smoothSlideViewTo(View child, int finalLeft, int finalTop),看下它的源码:
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { mCapturedView = child; mActivePointerId = INVALID_POINTER; boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) { // If we're in an IDLE state to begin with and aren't moving anywhere, we // end up having a non-null capturedView with an IDLE dragState mCapturedView = null; } return continueSliding;}
可以看到它不受mReleaseInProgress的限制,所以可以在任何地方调用,效果和settleCapturedViewAt()类似,因为它们最终都调用了forceSettleCapturedViewAt()来启动自动滚动,区别在于settleCapturedViewAt()会以最后松手前的滑动速率为初速度将View滚动到最终位置,而smoothSlideViewTo()滚动的初速度是0。forceSettleCapturedViewAt()里有地方调用了Callback里的方法,所以再来看看这个方法:
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { final int startLeft = mCapturedView.getLeft(); final int startTop = mCapturedView.getTop(); final int dx = finalLeft - startLeft; final int dy = finalTop - startTop; if (dx == 0 && dy == 0) { // Nothing to do. Send callbacks, be done. mScroller.abortAnimation(); setDragState(STATE_IDLE); return false; } final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); mScroller.startScroll(startLeft, startTop, dx, dy, duration); setDragState(STATE_SETTLING); return true;}
可以看到自动滑动是靠Scroll类完成,在这里生成了调用mScroller.startScroll()需要的参数。再来看看计算滚动时间的方法computeSettleDuration():
private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); final int absDx = Math.abs(dx); final int absDy = Math.abs(dy); final int absXVel = Math.abs(xvel); final int absYVel = Math.abs(yvel); final int addedVel = absXVel + absYVel; final int addedDistance = absDx + absDy; final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx / addedDistance; final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy / addedDistance; int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); return (int) (xduration * xweight + yduration * yweight);}
clampMag()方法确保参数中给定的速率在正常范围之内。最终的滚动时间还要经过computeAxisDuration()算出来,通过它的参数可以看到最终的滚动时间是由dx、xvel、mCallback.getViewHorizontalDragRange()共同影响的。看computeAxisDuration():
private int computeAxisDuration(int delta, int velocity, int motionRange) { if (delta == 0) { return 0; } final int width = mParentView.getWidth(); final int halfWidth = width / 2; final float distanceRatio = Math.min(f, (float) Math.abs(delta) / width); final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio); int duration; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float range = (float) Math.abs(delta) / motionRange; duration = (int) ((range + 1) * BASE_SETTLE_DURATION); } return Math.min(duration, MAX_SETTLE_DURATION);}
如果给定的速率velocity不为0,就通过距离除以速率来算出时间;如果velocity为0,就通过要滑动的距离(delta)除以总的移动范围(motionRange,就是Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)来算出时间。最后还会对计算出的时间做过滤,最终时间反正是不会超过MAX_SETTLE_DURATION的,源码里的取值是600毫秒,所以不用担心在Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回错误的数而导致自动滚动时间过长了。

在调用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()时,还需要实现mParentView的computeScroll()

至此,整个触摸流程和ViewDragHelper的重要的方法都过了一遍。之前在讨论shouldInterceptTouchEvent()的ACTION_DOWN部分执行完后应该再执行什么的时候,还有一种情况没有展开详解,就是有子View消费了本次ACTION_DOWN事件的情况,现在来看看这种情况。
假设现在shouldInterceptTouchEvent()的ACTION_DOWN部分执行完了,也有子View消费了这次的ACTION_DOWN事件,那么接下来就会调用mParentView的onInterceptTouchEvent()的ACTION_MOVE部分,不明白为什么的请参阅Andriod 从源码的角度详解View,ViewGroup的Touch事件的分发机制,接着调用ViewDragHelper的shouldInterceptTouchEvent()的ACTION_MOVE部分:
public boolean shouldInterceptTouchEvent(MotionEvent ev) { // 省略... switch (action) { // 省略其他case... case MotionEvent.ACTION_MOVE: { // First to cross a touch slop over a draggable view wins. Also report edge drags. 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) { // check the callback's // getView[Horizontal|Vertical]DragRange methods to know // if you can move at all along an axis, then see if it // would clamp to the same value. If you can't move at // all in every dimension with a nonzero range, bail. 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) { // Callback might have started an edge drag break; } if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); break; } // 省略其他case... } return mDragState == STATE_DRAGGING;}
如果有多个手指触摸到屏幕上了,对每个触摸点都检查一下,看当前触摸的地方是否需要捕获某个View。这里先用findTopChildUnder(int x, int y)寻找触摸点处的子View,再用checkTouchSlop(View child, float dx, float dy)检查当前触摸点到ACTION_DOWN触摸点的距离是否达到了mTouchSlop,达到了才会去捕获View。
接着看19~41行if (pastSlop){...}部分,这里检查在某个方向上是否可以进行拖动,检查过程涉及到getView[Horizontal|Vertical]DragRange和clampViewPosition[Horizontal|Vertical]四个方法。如果getView[Horizontal|Vertical]DragRange返回都是0,就会认作是不会产生拖动。clampViewPosition[Horizontal|Vertical]返回的是被捕获的View的最终位置,如果和原来的位置相同,说明我们没有期望它移动,也就会认作是不会产生拖动的。不会产生拖动就会在39行直接break,不会执行后续的代码,而后续代码里有调用tryCaptureViewForDrag(),所以不会产生拖动也就不会去捕获View了,拖动也不会进行了。
如果检查到可以在某个方向上进行拖动,就会调用后面的tryCaptureViewForDrag()捕获子View,如果捕获成功,mDragState就会变成STATE_DRAGGING,shouldInterceptTouchEvent()返回true,mParentView的onInterceptTouchEvent()返回true,后续的移动事件就会在mParentView的onTouchEvent()执行了,最后执行的就是mParentView的processTouchEvent()的ACTION_MOVE部分,拖动正常进行。

回头再看之前在shouldInterceptTouchEvent()的ACTION_DOWN部分留下的坑:
public boolean shouldInterceptTouchEvent(MotionEvent ev) { // 省略其他部分... switch (action) { // 省略其他case... case MotionEvent.ACTION_DOWN: { // 省略其他部分... // Catch a settling view if possible. if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } // 省略其他部分... } // 省略其他case... } return mDragState == STATE_DRAGGING;}
现在应该明白这部分代码会在什么情况下执行了。当我们松手后捕获的View处于自动滚动的过程中时,用户再次触摸屏幕,就会执行这里的tryCaptureViewForDrag()尝试捕获View,如果捕获成功,mDragState就变为STATE_DRAGGING了,shouldInterceptTouchEvent()就返回true了,然后就是mParentView的onInterceptTouchEvent()返回true,接着执行mParentView的onTouchEvent(),再执行processTouchEvent()的ACTION_DOWN部分。此时(ACTION_DOWN事件发生时)mParentView的onTouchEvent()要返回true,onTouchEvent()才能继续接受到接下来的ACTION_MOVE、ACTION_UP等事件,否则无法完成拖动。

至此整个事件传递流程和ViewDragHelper的重要方法基本都解析完了,shouldInterceptTouchEvent()和processTouchEvent()的ACTION_POINTER_DOWN、ACTION_POINTER_UP部分就留给读者自己解析了。

参考:
Android ViewDragHelper源码解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容