ViewDragHelper源碼解析

ViewDragHelper實(shí)例的創(chuàng)建

ViewDragHelper重載了兩個(gè)create()靜態(tài)方法
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拖拽需要的回調(diào)對(duì)象,sensitivity是用來調(diào)節(jié)mTouchSlop的值。sensitivity越大,mTouchSlop越小,對(duì)滑動(dòng)的檢測(cè)就越敏感。

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相關(guān)的一系列時(shí)間、大小、距離等常量 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時(shí)的最大、最小速率,單位是像素每秒 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); //mScroller是View滾動(dòng)的輔助類 mScroller = ScrollerCompat.create(context, sInterpolator);}

對(duì)Touch事件的處理

ViewDragHelper使用
當(dāng)mParentView(自定義ViewGroup)被觸摸時(shí),首先會(huì)調(diào)用mParentView的onInterceptTouchEvent(MotionEvent ev),接著就調(diào)用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記錄下觸摸的各個(gè)點(diǎn)信息,稍后可以用來計(jì)算本次滑動(dòng) //的速率,每次發(fā)生ACTION_DOWN事件都會(huì)調(diào)用cancel(),而在cancel() //方法里mVelocityTracker又被清空了,所以mVelocityTracker 記錄下的 //是本次ACTION_DOWN事件直至ACTION_UP事件發(fā)生后(下次 //ACTION_DOWN事件發(fā)生前)的所有觸摸點(diǎn)的信息 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)保存手勢(shì)的初始信息,即 //ACTION_DOWN發(fā)生時(shí)的觸摸點(diǎn)坐標(biāo)(x、y)、觸摸手指編號(hào) //(pointerId),如果觸摸到了mParentView的邊緣還會(huì)記錄觸摸的是哪 //個(gè)邊緣。 saveInitialMotion(x, y, pointerId); //調(diào)用findTopChildUnder((int) x, (int) y)來獲取當(dāng)前觸摸點(diǎn)下最頂層的子 //View final View toCapture = findTopChildUnder((int) x, (int) y); // Catch a settling view if possible. //mDragState成員變量,它共有三種取值: //STATE_IDLE:所有的View處于靜止空閑狀態(tài) //STATE_DRAGGING:某個(gè)View正在被用戶拖動(dòng)(用戶正在與設(shè)備交互) //STATE_SETTLING:某個(gè)View正在安置狀態(tài)中(用戶并沒有交互操作), //就是自動(dòng)滾動(dòng)的過程中mCapturedView默認(rèn)為null,所以一開始不會(huì)執(zhí)行 //這里的代碼,mDragState處于STATE_SETTLING狀態(tài)時(shí)才會(huì)執(zhí)行 //tryCaptureViewForDrag(),執(zhí)行的情況到后面再分析,這里先跳過 if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { //Callback.onEdgeTouched向外部通知mParentView的某些邊緣被觸摸到 //了,mInitialEdgesTouched是在剛才調(diào)用過的saveInitialMotion方法里 //進(jìn)行賦值的 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } // 其他case暫且省略 } //ACTION_DOWN 部分處理完了,跳過switch語句塊,剩下的代碼就只有 //return mDragState == STATE_DRAGGING;。在ACTION_DOWN部分 //沒有對(duì)mDragState進(jìn)行賦值,其默認(rèn)值為STATE_IDLE,所以此處返 //false。 return mDragState == STATE_DRAGGING;}

findTopChildUnder源碼:
如果在同一個(gè)位置有兩個(gè)子View重疊,想要讓下層的子View被選中,那么就要實(shí)現(xiàn)Callback里的getOrderedChildIndex(int index)方法來改變查找子View的順序;例如topView(上層View)的index是4,bottomView(下層View)的index是3,按照正常的遍歷查找方式(getOrderedChildIndex()默認(rèn)直接返回index),會(huì)選擇到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后接下來應(yīng)該是會(huì)調(diào)用哪個(gè)方法呢,根據(jù),ViewGroup的Touch事件的分發(fā)機(jī)制的解析,接下來會(huì)在mParentView的所有子View中尋找響應(yīng)這個(gè)Touch事件的View(會(huì)調(diào)用每個(gè)子View的dispatchTouchEvent()方法,dispatchTouchEvent里一般又會(huì)調(diào)用onTouchEvent());
如果沒有子View消費(fèi)這次事件(子View的dispatchTouchEvent()返回都是false),會(huì)調(diào)用mParentView的super.dispatchTouchEvent(ev),即View中的dispatchTouchEvent(ev),然后調(diào)用mParentView的onTouchEvent()方法,再調(diào)用ViewDragHelper的processTouchEvent(MotionEvent ev)方法。此時(shí)(ACTION_DOWN事件發(fā)生時(shí))mParentView的onTouchEvent()要返回true,onTouchEvent()才能繼續(xù)接受到接下來的ACTION_MOVE、ACTION_UP等事件,否則無法完成拖動(dòng)(除了ACTION_DOWN外的其他事件發(fā)生時(shí)返回true或false都不會(huì)影響接下來的事件接受),因?yàn)橥蟿?dòng)的相關(guān)代碼是寫在processTouchEvent()里的ACTION_MOVE部分的。要注意的是返回true后mParentView的onInterceptTouchEvent()就不會(huì)收到后續(xù)的ACTION_MOVE、ACTION_UP等事件了。

如果有子View消費(fèi)了本次ACTION_DOWN事件,mParentView的onTouchEvent()就收不到ACTION_DOWN事件了,也就是ViewDragHelper的processTouchEvent(MotionEvent ev)收不到ACTION_DOWN事件了。不過只要該View沒有調(diào)用過requestDisallowInterceptTouchEvent(true),mParentView的onInterceptTouchEvent()的ACTION_MOVE部分還是會(huì)執(zhí)行的,如果在此時(shí)返回了true攔截了ACTION_MOVE事件,processTouchEvent()里的ACTION_MOVE部分也就會(huì)正常執(zhí)行,拖動(dòng)也就沒問題了。onInterceptTouchEvent()的ACTION_MOVE部分具體做了怎樣的處理,稍后再來解析。

接下來對(duì)這兩種情況逐一解析。

假設(shè)沒有子View消費(fèi)這次事件,根據(jù)剛才的分析最終就會(huì)調(diào)用processTouchEvent(MotionEvent ev)的ACTION_DOWN部分
跟shouldInterceptTouchEvent()里ACTION_DOWN那部分基本一致,唯一區(qū)別就是這里沒有約束條件直接調(diào)用了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)方法,把當(dāng)前觸摸到的View和觸摸手指編號(hào)傳遞了過去,在tryCaptureView()中決定是否需要拖動(dòng)當(dāng)前觸摸到的View,如果要拖動(dòng)當(dāng)前觸摸到的View就在tryCaptureView()中返回true,讓ViewDragHelper把當(dāng)前觸摸的View捕獲下來,接著就調(diào)用了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)中將要拖動(dòng)的View和觸摸的手指編號(hào)記錄下來,并調(diào)用Callback的onViewCaptured(childView, activePointerId)通知外部有子View被捕獲到了,再調(diào)用setDragState()設(shè)置當(dāng)前的狀態(tài)為STATE_DRAGGING,看setDragState()源碼:
void setDragState(int state) { if (mDragState != state) { mDragState = state; mCallback.onViewDragStateChanged(state); if (mDragState == STATE_IDLE) { mCapturedView = null; } }}
狀態(tài)改變后會(huì)調(diào)用Callback的onViewDragStateChanged()通知狀態(tài)的變化。
假設(shè)ACTION_DOWN發(fā)生后在mParentView的onTouchEvent()返回了true,接下來就會(huì)執(zhí)行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; }
如果一直沒松手,這部分代碼會(huì)一直調(diào)用。這里先判斷mDragState是否為STATE_DRAGGING,而唯一調(diào)用setDragState(STATE_DRAGGING)的地方就是tryCaptureViewForDrag()了,剛才在ACTION_DOWN里調(diào)用過tryCaptureViewForDrag(),現(xiàn)在又要分兩種情況。如果剛才在ACTION_DOWN里捕獲到要拖動(dòng)的View,那么就執(zhí)行if部分的代碼,這個(gè)稍后解析,先考慮沒有捕獲到的情況。沒有捕獲到的話,mDragState依然是STATE_IDLE,然后會(huì)執(zhí)行else部分的代碼。這里主要就是檢查有沒有哪個(gè)手指觸摸到了要拖動(dòng)的View上,觸摸上了就嘗試捕獲它,然后讓mDragState變?yōu)镾TATE_DRAGGING,之后就會(huì)執(zhí)行if部分的代碼了,這個(gè)稍后解析,先考慮沒有捕獲到的情況。沒有捕獲到的話,mDragState依然是STATE_IDLE,然后會(huì)執(zhí)行else部分的代碼。這里主要就是檢查有沒有哪個(gè)手指觸摸到了要拖動(dòng)的View上,觸摸上了就嘗試捕獲它,然后讓mDragState變?yōu)镾TATE_DRAGGING,之后就會(huì)執(zhí)行if部分的代碼了。這里還有兩個(gè)方法涉及到了Callback里的方法,需要來解析一下,分別是reportNewEdgeDrags()和checkTouchSlop(),先看reportNewEdgeDrags():
reportNewEdgeDrags()對(duì)四個(gè)邊緣都做了一次檢查,檢查是否在某些邊緣產(chǎn)生拖動(dòng)了,如果有拖動(dòng),就將有拖動(dòng)的邊緣記錄在mEdgeDragsInProgress中,再調(diào)用Callback的onEdgeDragStarted(int edgeFlags, int pointerId)通知某個(gè)邊緣開始產(chǎn)生拖動(dòng)了。雖然reportNewEdgeDrags()會(huì)被調(diào)用很多次(因?yàn)閜rocessTouchEvent()的ACTION_MOVE部分會(huì)執(zhí)行很多次),但mCallback.onEdgeDragStarted(dragsStarted, pointerId)只會(huì)調(diào)用一次,具體的要看checkNewEdgeDrag()這個(gè)方法:
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(邊緣)開始產(chǎn)生拖動(dòng)了。
  • 方法的兩個(gè)參數(shù)delta和odelta需要解釋一下,odelta里的o應(yīng)該代表opposite,這是什么意思呢,以reportNewEdgeDrags()里調(diào)用checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)為例,我們要監(jiān)測(cè)左邊緣的觸摸情況,所以主要監(jiān)測(cè)的是x軸方向上的變化,這里delta為dx,odelta為dy,也就是說delta是指我們主要監(jiān)測(cè)的方向上的變化,odelta是另外一個(gè)方向上的變化,后面要判斷假另外一個(gè)方向上的變化是否要遠(yuǎn)大于主要方向上的變化,所以需要另外一個(gè)方向上的距離變化的值。
  • mInitialEdgesTouched是在ACTION_DOWN部分的saveInitialMotion()里生成的,ACTION_DOWN發(fā)生時(shí)觸摸到的邊緣會(huì)被記錄在mInitialEdgesTouched中。如果ACTION_DOWN發(fā)生時(shí)沒有觸摸到邊緣,或者觸摸到的邊緣不是指定的edge,就直接返回false了。
  • mTrackingEdges是由setEdgeTrackingEnabled(int edgeFlags)設(shè)置的,當(dāng)我們想要追蹤監(jiān)聽邊緣觸摸時(shí)才需要調(diào)用setEdgeTrackingEnabled(int edgeFlags),如果我們沒有調(diào)用過它,這里就直接返回false了。
  • mEdgeDragsLocked它在這個(gè)方法里被引用了多次,它在整個(gè)ViewDragHelper里唯一被賦值的地方就是這里的第12行,所以默認(rèn)值是0,第6行mEdgeDragsLocked[pointerId] & edge) == edge執(zhí)行的結(jié)果是false。我們?cè)偬?1到14行看看,absDelta < absODelta * 0.5f的意思是檢查在次要方向上移動(dòng)的距離是否遠(yuǎn)超過主要方向上移動(dòng)的距離,如果是再調(diào)用Callback的onEdgeLock(edge)檢查是否需要鎖定某個(gè)邊緣,如果鎖定了某個(gè)邊緣,那個(gè)邊緣就算觸摸到了也不會(huì)被記錄在mEdgeDragsInProgress里了,也不會(huì)收到Callback的onEdgeDragStarted()通知了。并且將鎖定的邊緣記錄在mEdgeDragsLocked變量里,再次調(diào)用本方法時(shí)就會(huì)在第6行進(jìn)行判斷了,第6行里如果檢測(cè)到給定的edge被鎖定,就直接返回false了。
  • 回到第7行的(mEdgeDragsInProgress[pointerId] & edge) == edge,mEdgeDragsInProgress是保存已發(fā)生過拖動(dòng)事件的邊緣的,如果給定的edge已經(jīng)保存過了,那就沒必要再檢測(cè)其他東西了,直接返回false了。
  • 第8行(absDelta <= mTouchSlop && absODelta <= mTouchSlop)很簡(jiǎn)單了,就是檢查本次移動(dòng)的距離是不是太小了,太小就不處理了。
  • 最后一句返回的時(shí)候再次檢查給定的edge有沒有記錄過,確保了每個(gè)邊緣只會(huì)調(diào)用一次reportNewEdgeDrags的mCallback.onEdgeDragStarted(dragsStarted, pointerId)

checkTouchSlop()方法主要就是檢查手指移動(dòng)的距離有沒有超過觸發(fā)處理移動(dòng)事件的最短距離(mTouchSlop)了,注意dx和dy指的是當(dāng)前觸摸點(diǎn)到ACTION_DOWN觸摸到的點(diǎn)的距離。這里先檢查Callback的getViewHorizontalDragRange(child)和getViewVerticalDragRange(child)是否大于0,如果想讓某個(gè)View在某個(gè)方向上滑動(dòng),就要在那個(gè)方向?qū)?yīng)的方法里返回大于0的數(shù)。否則在processTouchEvent()的ACTION_MOVE部分就不會(huì)調(diào)用tryCaptureViewForDrag()來捕獲當(dāng)前觸摸到的View了,拖動(dòng)也就沒辦法進(jìn)行了。

回到processTouchEvent()的ACTION_MOVE部分,假設(shè)現(xiàn)在我們的手指已經(jīng)滑動(dòng)到可以被捕獲到的View上了,也都正常的實(shí)現(xiàn)了Callback中的相關(guān)方法,讓tryCaptureViewForDrag()正常的捕獲到觸摸到的View了,下一次ACTION_MOVE時(shí)就執(zhí)行if部分的代碼了,也就是開始不停的調(diào)用dragTo()對(duì)mCaptureView進(jìn)行真正拖動(dòng)了,看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); }}
參數(shù)dx和dy是前后兩次ACTION_MOVE移動(dòng)的距離,left和top分別為mCapturedView.getLeft() + dx, mCapturedView.getTop() + dy,也就是期望的移動(dòng)后的坐標(biāo),對(duì)View的getLeft()等方法不理解的請(qǐng)參閱Android View坐標(biāo)getLeft, getRight, getTop, getBottom。
這里通過調(diào)用offsetLeftAndRight()和offsetTopAndBottom()來完成對(duì)mCapturedView移動(dòng),這兩個(gè)是View中定義的方法,看它們的源碼就知道內(nèi)部是通過改變View的mLeft、mRight、mTop、mBottom,即改變View在父容器中的坐標(biāo)位置,達(dá)到移動(dòng)View的效果,所以如果調(diào)用mCapturedView的layout(int l, int t, int r, int b)方法也可以實(shí)現(xiàn)移動(dòng)View的效果。
具體要移動(dòng)到哪里,由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()來決定的,如果不想在水平方向上移動(dòng),在clampViewPositionHorizontal(View child, int left, int dx)里直接返回child.getLeft()就可以了,這樣clampedX - oldLeft的值為0,這里調(diào)用mCapturedView.offsetLeftAndRight(clampedX - oldLeft)就不會(huì)起作用了。垂直方向上同理。
最后會(huì)調(diào)用Callback的onViewPositionChanged(mCapturedView, clampedX, clampedY,clampedDx, clampedDy)通知捕獲到的View位置改變了,并把最終的坐標(biāo)(clampedX、clampedY)和最終的移動(dòng)距離(clampedDx、 clampedDy)傳遞過去。
ACTION_MOVE部分就算告一段落了,接下來應(yīng)該是用戶松手觸發(fā)ACTION_UP,或者是達(dá)到某個(gè)條件導(dǎo)致后續(xù)的ACTION_MOVE被mParentView的上層View給攔截了而收到ACTION_CANCEL,一起來看這兩個(gè)部分:
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; } }}
這兩個(gè)部分都是重置所有的狀態(tài)記錄,并通知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()里也調(diào)用了dispatchViewReleased(),只不過傳遞了速率給它,這個(gè)速率就是由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); }}
這里調(diào)用Callback的onViewReleased(mCapturedView, xvel, yvel)通知外部捕獲到的View被釋放了,而在onViewReleased()前后有個(gè)mReleaseInProgress值得注意,注釋里說唯一可以調(diào)用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()里了。

首先這兩個(gè)方法是干什么的呢。在現(xiàn)實(shí)生活中保齡球的打法是,先做扔的動(dòng)作讓球的速度達(dá)到最大,然后突然松手,由于慣性,保齡球就以最后松手前的速度為初速度拋出去了,直至自然停止,或者撞到邊界停止,這種效果叫fling。
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)就是對(duì)捕獲到的View做出這種fling的效果,用戶在屏幕上滑動(dòng)松手之前也會(huì)有一個(gè)滑動(dòng)的速率。fling也引出來的一個(gè)問題,就是不知道View最終會(huì)滾動(dòng)到哪個(gè)位置,最后位置是在啟動(dòng)fling時(shí)根據(jù)最后滑動(dòng)的速度來計(jì)算的(flingCapturedView的四個(gè)參數(shù)int minLeft, int minTop, int maxLeft, int maxTop可以限定最終位置的范圍),假如想要讓View滾動(dòng)到指定位置應(yīng)該怎么辦,答案就是使用settleCapturedViewAt(int finalLeft, int finalTop)。

為什么唯一可以調(diào)用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);}
這兩個(gè)方法里一開始都會(huì)判斷mReleaseInProgress為false,如果為false就會(huì)拋一個(gè)IllegalStateException異常,而mReleaseInProgress唯一為true的時(shí)候就是在dispatchViewReleased()里調(diào)用onViewReleased()的時(shí)候。
Scroller的用法請(qǐng)參閱Android中滑屏實(shí)現(xiàn)----手把手教你如何實(shí)現(xiàn)觸摸滑屏以及Scroller類詳解 ,或者自行解讀Scroller源碼,代碼量不多。
ViewDragHelper還有一個(gè)移動(dòng)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的限制,所以可以在任何地方調(diào)用,效果和settleCapturedViewAt()類似,因?yàn)樗鼈冏罱K都調(diào)用了forceSettleCapturedViewAt()來啟動(dòng)自動(dòng)滾動(dòng),區(qū)別在于settleCapturedViewAt()會(huì)以最后松手前的滑動(dòng)速率為初速度將View滾動(dòng)到最終位置,而smoothSlideViewTo()滾動(dòng)的初速度是0。forceSettleCapturedViewAt()里有地方調(diào)用了Callback里的方法,所以再來看看這個(gè)方法:
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;}
可以看到自動(dòng)滑動(dòng)是靠Scroll類完成,在這里生成了調(diào)用mScroller.startScroll()需要的參數(shù)。再來看看計(jì)算滾動(dòng)時(shí)間的方法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()方法確保參數(shù)中給定的速率在正常范圍之內(nèi)。最終的滾動(dòng)時(shí)間還要經(jīng)過computeAxisDuration()算出來,通過它的參數(shù)可以看到最終的滾動(dòng)時(shí)間是由dx、xvel、mCallback.getViewHorizontalDragRange()共同影響的??碿omputeAxisDuration():
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,就通過距離除以速率來算出時(shí)間;如果velocity為0,就通過要滑動(dòng)的距離(delta)除以總的移動(dòng)范圍(motionRange,就是Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)來算出時(shí)間。最后還會(huì)對(duì)計(jì)算出的時(shí)間做過濾,最終時(shí)間反正是不會(huì)超過MAX_SETTLE_DURATION的,源碼里的取值是600毫秒,所以不用擔(dān)心在Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回錯(cuò)誤的數(shù)而導(dǎo)致自動(dòng)滾動(dòng)時(shí)間過長(zhǎng)了。

在調(diào)用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()時(shí),還需要實(shí)現(xiàn)mParentView的computeScroll()

至此,整個(gè)觸摸流程和ViewDragHelper的重要的方法都過了一遍。之前在討論shouldInterceptTouchEvent()的ACTION_DOWN部分執(zhí)行完后應(yīng)該再執(zhí)行什么的時(shí)候,還有一種情況沒有展開詳解,就是有子View消費(fèi)了本次ACTION_DOWN事件的情況,現(xiàn)在來看看這種情況。
假設(shè)現(xiàn)在shouldInterceptTouchEvent()的ACTION_DOWN部分執(zhí)行完了,也有子View消費(fèi)了這次的ACTION_DOWN事件,那么接下來就會(huì)調(diào)用mParentView的onInterceptTouchEvent()的ACTION_MOVE部分,不明白為什么的請(qǐng)參閱Andriod 從源碼的角度詳解View,ViewGroup的Touch事件的分發(fā)機(jī)制,接著調(diào)用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;}
如果有多個(gè)手指觸摸到屏幕上了,對(duì)每個(gè)觸摸點(diǎn)都檢查一下,看當(dāng)前觸摸的地方是否需要捕獲某個(gè)View。這里先用findTopChildUnder(int x, int y)尋找觸摸點(diǎn)處的子View,再用checkTouchSlop(View child, float dx, float dy)檢查當(dāng)前觸摸點(diǎn)到ACTION_DOWN觸摸點(diǎn)的距離是否達(dá)到了mTouchSlop,達(dá)到了才會(huì)去捕獲View。
接著看19~41行if (pastSlop){...}部分,這里檢查在某個(gè)方向上是否可以進(jìn)行拖動(dòng),檢查過程涉及到getView[Horizontal|Vertical]DragRange和clampViewPosition[Horizontal|Vertical]四個(gè)方法。如果getView[Horizontal|Vertical]DragRange返回都是0,就會(huì)認(rèn)作是不會(huì)產(chǎn)生拖動(dòng)。clampViewPosition[Horizontal|Vertical]返回的是被捕獲的View的最終位置,如果和原來的位置相同,說明我們沒有期望它移動(dòng),也就會(huì)認(rèn)作是不會(huì)產(chǎn)生拖動(dòng)的。不會(huì)產(chǎn)生拖動(dòng)就會(huì)在39行直接break,不會(huì)執(zhí)行后續(xù)的代碼,而后續(xù)代碼里有調(diào)用tryCaptureViewForDrag(),所以不會(huì)產(chǎn)生拖動(dòng)也就不會(huì)去捕獲View了,拖動(dòng)也不會(huì)進(jìn)行了。
如果檢查到可以在某個(gè)方向上進(jìn)行拖動(dòng),就會(huì)調(diào)用后面的tryCaptureViewForDrag()捕獲子View,如果捕獲成功,mDragState就會(huì)變成STATE_DRAGGING,shouldInterceptTouchEvent()返回true,mParentView的onInterceptTouchEvent()返回true,后續(xù)的移動(dòng)事件就會(huì)在mParentView的onTouchEvent()執(zhí)行了,最后執(zhí)行的就是mParentView的processTouchEvent()的ACTION_MOVE部分,拖動(dòng)正常進(jìn)行。

回頭再看之前在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;}
現(xiàn)在應(yīng)該明白這部分代碼會(huì)在什么情況下執(zhí)行了。當(dāng)我們松手后捕獲的View處于自動(dòng)滾動(dòng)的過程中時(shí),用戶再次觸摸屏幕,就會(huì)執(zhí)行這里的tryCaptureViewForDrag()嘗試捕獲View,如果捕獲成功,mDragState就變?yōu)镾TATE_DRAGGING了,shouldInterceptTouchEvent()就返回true了,然后就是mParentView的onInterceptTouchEvent()返回true,接著執(zhí)行mParentView的onTouchEvent(),再執(zhí)行processTouchEvent()的ACTION_DOWN部分。此時(shí)(ACTION_DOWN事件發(fā)生時(shí))mParentView的onTouchEvent()要返回true,onTouchEvent()才能繼續(xù)接受到接下來的ACTION_MOVE、ACTION_UP等事件,否則無法完成拖動(dòng)。

至此整個(gè)事件傳遞流程和ViewDragHelper的重要方法基本都解析完了,shouldInterceptTouchEvent()和processTouchEvent()的ACTION_POINTER_DOWN、ACTION_POINTER_UP部分就留給讀者自己解析了。

參考:
Android ViewDragHelper源碼解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容