在自定義ViewGroup的過程當中,若是涉及到View的拖動滑動,ViewDragHelper的使用應該是少不了的,它提供了一系列用於用戶拖動子View的輔助方法和相關的狀態記錄,像Navigation Drawer的邊緣滑動、QQ5.x的側滑菜單、知乎裏的頁面滑動返回均可以由它實現,因此有必要徹底掌握它的使用。html
要想徹底掌握ViewDragHelper的使用和原理,最好的辦法就是讀懂它的源碼,因此就有了這篇分析,以便在印象模糊之時能夠再次快速回顧ViewDragHelper的原理、用法、注意事項等。java
上面幾個步驟已經實現了子View拖動的效果,若是還想要實現fling效果(滑動時鬆手後以必定速率繼續自動滑動下去並逐漸中止,相似於扔東西)或者鬆手後自動滑動到指定位置,須要實現自定義ViewGroup的computeScroll()方法,方法實現以下:android
@Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { postInvalidate(); } }並在ViewDragHelper.Callback的onViewReleased()方法裏調用settleCapturedViewAt()、flingCapturedView(),或在任意地方調用smoothSlideViewTo()方法。
若是要實現邊緣拖動的效果,須要調用ViewDragHelper的setEdgeTrackingEnabled()方法,註冊想要監聽的邊緣。而後實現ViewDragHelper.Callback裏的onEdgeDragStarted()方法,在此手動調用captureChildView()傳遞要拖動的子View。git
具體的使用Demo請見最後面公佈的幾個案例。github
ViewDragHelper的完整源碼可在GitHub或GrepCode上在線查看。在最後的總結部分,我畫了簡單的流程圖,梳理了整個觸摸事件傳遞太重中相關方法的調用,有須要的就先去總結部分看看。app
ViewDragHelper重載了兩個create()靜態方法,先看兩個參數的create()方法:ide
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); }
create()的兩個參數很好理解,第一個是咱們自定義的ViewGroup,第二個是控制子View拖拽須要的回調對象。create()直接調用了ViewDragHelper構造方法,咱們再來看看這個構造方法。函數
/** * Apps should use ViewDragHelper.create() to get a new instance. * This will allow VDH to use internal compatibility implementations for different * platform versions. * * @param context Context to initialize config-dependent params from * @param forParent Parent view to monitor */ 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; final ViewConfiguration vc = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); mTouchSlop = vc.getScaledTouchSlop(); mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMinVelocity = vc.getScaledMinimumFlingVelocity(); mScroller = ScrollerCompat.create(context, sInterpolator); }
這個構造函數是私有的,也是僅有的構造函數,因此外部只能經過create()工廠方法來建立ViewDragHelper實例了。這裏要求了咱們傳遞的自定義ViewGroup和回調對象不能爲空,不然會直接拋出異常中斷程序。在這裏也初始化了一些觸摸滑動須要的參考值和輔助類。post
再看三個參數的create()方法:this
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ 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; }
第二個參數sensitivity是用來調節mTouchSlop的值。sensitivity越大,mTouchSlop越小,對滑動的檢測就越敏感。例如sensitivity爲1時,先後觸摸點距離超過20dp才進行滑動處理,如今sensitivity爲2的話,先後觸摸點距離超過10dp就進行處理了。
當mParentView(自定義ViewGroup)被觸摸時,首先會調用mParentView的onInterceptTouchEvent(MotionEvent ev),接着就調用shouldInterceptTouchEvent(MotionEvent ev) ,因此先來看看這個方法的ACTION_DOWN部分:
/** * Check if this event as provided to the parent view's onInterceptTouchEvent should * cause the parent to intercept the touch event stream. * * @param ev MotionEvent provided to onInterceptTouchEvent * @return true if the parent view should return true from onInterceptTouchEvent */ 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 = 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); // Catch a settling view if possible. 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暫且省略 } return mDragState == STATE_DRAGGING; }
看9~21行,首先是關於多點觸控(MotionEvent的actionIndex、ACTION_POINTER_DOWN 等概念),不明白的請參閱android觸控,先了解MotionEvent(一)。
mVelocityTracker記錄下觸摸的各個點信息,稍後能夠用來計算本次滑動的速率,每次發生ACTION_DOWN事件都會調用cancel(),而在cancel()方法裏mVelocityTracker又被清空了,因此mVelocityTracker 記錄下的是本次ACTION_DOWN事件直至ACTION_UP事件發生後(下次ACTION_DOWN事件發生前)的全部觸摸點的信息。
再來看24~42行case MotionEvent.ACTION_DOWN部分,先是調用saveInitialMotion(x, y, pointerId)保存手勢的初始信息,即ACTION_DOWN發生時的觸摸點座標(x、y)、觸摸手指編號(pointerId),若是觸摸到了mParentView的邊緣還會記錄觸摸的是哪一個邊緣。接着調用findTopChildUnder((int) x, (int) y);來獲取當前觸摸點下最頂層的子View,看findTopChildUnder 的源碼:
/** * Find the topmost child under the given point within the parent view's coordinate system. * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. * * @param x X position to test in the parent's coordinate system * @param y Y position to test in the parent's coordinate system * @return The topmost child view under (x, y) or null if none found. */ 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; }
代碼很簡單,註釋裏也說明的很清楚了。若是在同一個位置有兩個子View重疊,想要讓下層的子View被選中,那麼就要實現Callback裏的getOrderedChildIndex(int index)方法來改變查找子View的順序;例如topView(上層View)的index是4,bottomView(下層View)的index是3,按照正常的遍歷查找方式(getOrderedChildIndex()默認直接返回index),會選擇到topView,要想讓bottomView被選中就得這麼寫:
public int getOrderedChildIndex(int index) { int indexTop = mParentView.indexOfChild(topView); int indexBottom = mParentView.indexOfChild(bottomView); if (index == indexTop) { return indexBottom; } return index; }
32~35行,這裏還看到了一個mDragState成員變量,它共有三種取值:
37~40行調用了Callback.onEdgeTouched向外部通知mParentView的某些邊緣被觸摸到了,mInitialEdgesTouched是在剛纔調用過的saveInitialMotion方法裏進行賦值的。
ACTION_DOWN 部分處理完了,跳過switch語句塊,剩下的代碼就只有return mDragState == STATE_DRAGGING;。在ACTION_DOWN部分沒有對mDragState進行賦值,其默認值爲STATE_IDLE,因此此處返回false。
那麼返回false後接下來應該是會調用哪一個方法呢,根據Andriod 從源碼的角度詳解View,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部分:
/** * Process a touch event received by the parent view. This method will dispatch callback events * as needed before returning. The parent view's onTouchEvent implementation should call this. * * @param ev The touch event received by the parent view */ public void processTouchEvent(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 = 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); final View toCapture = findTopChildUnder((int) x, (int) y); saveInitialMotion(x, y, pointerId); // Since the parent is already directly processing this touch event, // there is no reason to delay for a slop before dragging. // Start immediately if possible. tryCaptureViewForDrag(toCapture, pointerId); final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } // 其餘case暫且省略 } }
這段代碼跟shouldInterceptTouchEvent()裏ACTION_DOWN那部分基本一致,惟一區別就是這裏沒有約束條件直接調用了tryCaptureViewForDrag()方法,如今來看看這個方法:
/** * Attempt to capture the view with the given pointer ID. The callback will be involved. * This will put us into the "dragging" state. If we've already captured this view with * this pointer this method will immediately return true without consulting the callback. * * @param toCapture View to capture * @param pointerId Pointer to capture with * @return true if capture was successful */ 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)方法:
/** * Capture a specific child view for dragging within the parent. The callback will be notified * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to * capture this view. * * @param childView Child view to capture * @param activePointerId ID of the pointer that is dragging the captured child view */ 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; } // 省略其餘case... } }
要注意的是,若是一直沒鬆手,這部分代碼會一直調用。這裏先判斷mDragState是否爲STATE_DRAGGING,而惟一調用setDragState(STATE_DRAGGING)的地方就是tryCaptureViewForDrag()了,剛纔在ACTION_DOWN裏調用過tryCaptureViewForDrag(),如今又要分兩種狀況。
若是剛纔在ACTION_DOWN裏捕獲到要拖動的View,那麼就執行if部分的代碼,這個稍後解析,先考慮沒有捕獲到的狀況。沒有捕獲到的話,mDragState依然是STATE_IDLE,而後會執行else部分的代碼。這裏主要就是檢查有沒有哪一個手指觸摸到了要拖動的View上,觸摸上了就嘗試捕獲它,而後讓mDragState變爲STATE_DRAGGING,以後就會執行if部分的代碼了。這裏還有兩個方法涉及到了Callback裏的方法,須要來解析一下,分別是reportNewEdgeDrags()和checkTouchSlop(),先看reportNewEdgeDrags():
private void reportNewEdgeDrags(float dx, float dy, int pointerId) { int dragsStarted = 0; if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { dragsStarted |= EDGE_LEFT; } if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { dragsStarted |= EDGE_TOP; } if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { dragsStarted |= EDGE_RIGHT; } if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { dragsStarted |= EDGE_BOTTOM; } if (dragsStarted != 0) { mEdgeDragsInProgress[pointerId] |= dragsStarted; mCallback.onEdgeDragStarted(dragsStarted, pointerId); } }
這裏對四個邊緣都作了一次檢查,檢查是否在某些邊緣產生拖動了,若是有拖動,就將有拖動的邊緣記錄在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; }
再來看checkTouchSlop()方法:
/** * Check if we've crossed a reasonable touch slop for the given child view. * If the child cannot be dragged along the horizontal or vertical axis, motion * along that axis will not count toward the slop check. * * @param child Child to check * @param dx Motion since initial position along X axis * @param dy Motion since initial position along Y axis * @return true if the touch slop has been crossed */ 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)了,注意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():
/** * Like all callback events this must happen on the UI thread, but release * involves some extra semantics. During a release (mReleaseInProgress) * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} * or {@link #flingCapturedView(int, int, int, int)}. */ 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()呢?看看它們的源碼
/** * Settle the captured view at the given (left, top) position. * The appropriate velocity from prior motion will be taken into account. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. * * @param finalLeft Settled left edge position for the captured view * @param finalTop Settled top edge position for the captured view * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ 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)); } /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. * * @param minLeft Minimum X position for the view's left edge * @param minTop Minimum Y position for the view's top edge * @param maxLeft Maximum X position for the view's left edge * @param maxTop Maximum Y position for the view's top edge */ 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),看下它的源碼:
/** * Animate the view <code>child</code> to the given (left, top) position. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. * * <p>This operation does not count as a capture event, though {@link #getCapturedView()} * will still report the sliding view while the slide is in progress.</p> * * @param child Child view to capture and animate * @param finalLeft Final left position of child * @param finalTop Final top position of child * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ 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裏的方法,因此再來看看這個方法:
/** * Settle the captured view at the given (left, top) position. * * @param finalLeft Target left position for the captured view * @param finalTop Target top position for the captured view * @param xvel Horizontal velocity * @param yvel Vertical velocity * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ 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(1f, (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); }
6~10行沒看明白,直接看14~19行,若是給定的速率velocity不爲0,就經過距離除以速率來算出時間;若是velocity爲0,就經過要滑動的距離(delta)除以總的移動範圍(motionRange,就是Callback裏getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)來算出時間。最後還會對計算出的時間作過濾,最終時間反正是不會超過MAX_SETTLE_DURATION的,源碼裏的取值是600毫秒,因此不用擔憂在Callback裏getViewHorizontalDragRange()、getViewVerticalDragRange()返回錯誤的數而致使自動滾動時間過長了。
在調用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()時,還須要實現mParentView的computeScroll():
@Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } }
這屬於Scroll類用法的範疇,不明白的請參閱Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解 的「知識點二: 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部分就留給讀者本身解析了。
對於整個觸摸事件傳遞過程,我畫了簡要的流程圖,方便往後快速回顧。
多點觸摸狀況我就沒研究了,在這裏忽略~
三個開啓自動滾動的方法:
Callback的各個方法總結:
在這裏列舉一部分對ViewDragHelper的應用案例,你們本身剖析它們的源碼來實踐鞏固。
轉載請註明出處http://www.cnblogs.com/lqstayreal/p/4500219.html