我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉發更多有用的知識給大家.謝謝關注_,說不定什么時候會有福利哈.
1.簡介
在上一篇文章SwipeBackLayout源代碼分析中,我們了解了ViewDragHelper
是可以幫助我們處理各種拖拽事件的類.使用好ViewDragHelper
能幫助我們做出各種酷炫的交互,今天我們就來分析一下ViewDragHelper
的使用與實現:
2.使用方法
我們這里就以翔總的這篇文章中的例子來介紹一下ViewDragHelper
的使用.另外,本文中的demo可以在
這里找到
首先我們創建一個DragLayout
類并繼承自LinearLayout
,然后我們準備在DragLayout
放置三個View
第一個用來被我們拖動然后停止在松手的位置,第二個可以被我們拖動,松手的時候滑動到指定位置,第三個只可以通過觸摸邊緣來進行拖動,
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
//child為當前觸摸區域下的View,如果返回true,就可以拖拽.
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();
}
//left為child即將移動到的水平位置的值,但是返回值會最終決定移動到的值
//這里直接返回了left
@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
都定義了哪些方法:
public static abstract class Callback {
//當View的拖拽狀態改變時回調,state為STATE_IDLE,STATE_DRAGGING,STATE_SETTLING的一種
//STATE_IDLE: 當前未被拖拽
//STATE_DRAGGING:正在被拖拽
//STATE_SETTLING: 被拖拽后需要被安放到一個位置中的狀態
public void onViewDragStateChanged(int state) {}
//當View被拖拽位置發生改變時回調
//changedView :被拖拽的View
//left : 被拖拽后View的left邊緣坐標
//top : 被拖拽后View的top邊緣坐標
//dx : 拖動的x偏移量
//dy : 拖動的y偏移量
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
//當一個View被捕獲到準備開始拖動時回調,
//capturedChild : 捕獲的View
//activePointerId : 對應的PointerId
public void onViewCaptured(View capturedChild, int activePointerId) {}
//當被捕獲拖拽的View被釋放是回調
//releasedChild : 被釋放的View
//xvel : 釋放View的x方向上的加速度
//yvel : 釋放View的y方向上的加速度
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
//如果parentView訂閱了邊緣觸摸,則如果有邊緣觸摸就回調的接口
//edgeFlags : 當前觸摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
//pointerId : 用來描述邊緣觸摸操作的id
public void onEdgeTouched(int edgeFlags, int pointerId) {}
//是否鎖定該邊緣的觸摸,默認返回false,返回true表示鎖定
public boolean onEdgeLock(int edgeFlags) {
return false;
}
//邊緣觸摸開始時回調
//edgeFlags : 當前觸摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
//pointerId : 用來描述邊緣觸摸操作的id
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
//在尋找當前觸摸點下的子View時會調用此方法,尋找到的View會提供給tryCaptureViewForDrag()來嘗試捕獲。
//如果需要改變子View的遍歷查詢順序可改寫此方法,例如讓下層的View優先于上層的View被選中。
public int getOrderedChildIndex(int index) {
return index;
}
//獲取被拖拽View child 的水平拖拽范圍,返回0表示無法被水平拖拽
public int getViewHorizontalDragRange(View child) {
return 0;
}
//獲取被拖拽View child 的垂直拖拽范圍,返回0表示無法被水平拖拽
public int getViewVerticalDragRange(View child) {
return 0;
}
//嘗試捕獲被拖拽的View
public abstract boolean tryCaptureView(View child, int pointerId);
//決定拖拽View在水平方向上應該移動到的位置
//child : 被拖拽的View
//left : 期望移動到位置的View的left值
//dx : 移動的水平距離
//返回值 : 直接決定View在水平方向的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
//決定拖拽View在垂直方向上應該移動到的位置
//child : 被拖拽的View
//top : 期望移動到位置的View的top值
//dy : 移動的垂直距離
//返回值 : 直接決定View在垂直方向的位置
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
想必注釋已經很清楚了,正是這些回調方法,再結合ViewDragHelper
中的各種方法,來幫助我們實現各種各樣的拖拽的效果。
2.shouldInterceptTouchEvent()方法的實現
在這里我們假設大家都清楚了Android
的事件分發機制,如果不清楚請看這里,要想處理觸摸事件,我們需要在onInterceptTouchEvent(MotionEvent ev)
方法里判斷是否需要攔截這次觸摸事件,如果此方法返回true
則觸摸事件將會交給onTouchEvent(MotionEvent event)
處理,這樣我們就能處理觸摸事件了,所以我們在上面的使用方法里會這樣寫:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
這樣就將是否攔截觸摸事件,以及處理觸摸事件委托給ViewDragHelper
來處理了,所以我們先來看看ViewDragHelper
中shouldInterceptTouchEvent();
方法的實現:
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
//獲取action
final int action = MotionEventCompat.getActionMasked(ev);
//獲取action對應的index
final int actionIndex = MotionEventCompat.getActionIndex(ev);
//如果是按下的action則重置一些信息,包括各種事件點的數組
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
//初始化mVelocityTracker
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
//根據action來做相應的處理
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
//獲取這個事件對應的pointerId,一般情況下只有一個手指觸摸時為0
//兩個手指觸摸時第二個手指觸摸返回的pointerId為1,以此類推
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
//保存點的數據
//TODO (1)
saveInitialMotion(x, y, pointerId);
//獲取當前觸摸點下最頂層的子View
//TODO (2)
final View toCapture = findTopChildUnder((int) x, (int) y);
//如果toCapture是已經捕獲的View,而且正在處于被釋放狀態
//那么就重新捕獲
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
//如果觸摸了邊緣,回調callback的onEdgeTouched()方法
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);
//因為同一時間ViewDragHelper只能操控一個View,所以當有新的手指觸摸時
//只討論當無觸摸發生時,回調邊緣觸摸的callback
//或者正在處于釋放狀態時重新捕獲View
if (mDragState == STATE_IDLE) {
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (mDragState == STATE_SETTLING) {
// Catch a settling view if possible.
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;
// 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];
//獲取當前觸摸點下最頂層的子View
final View toCapture = findTopChildUnder((int) x, (int) y);
//如果找到了最頂層View,并且產生了拖動(checkTouchSlop()返回true)
//TODO (3)
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
//根據callback的四個方法getView[Horizontal|Vertical]DragRange和
//clampViewPosition[Horizontal|Vertical]來檢查是否可以拖動
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;
}
//如果產生了拖動則調用tryCaptureViewForDrag()
//TODO (4)
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;
}
}
//如果mDragState等于正在拖拽則返回true
return mDragState == STATE_DRAGGING;
}
上面就是整個shouldInterceptTouchEvent()
的實現,上面的注釋也足夠清楚了,我們這里就先不分析某一種觸摸事件,大家可以看到我上面留了幾個TODO,下文會一起分析,這里我假設大家都已經對觸摸事件分發處理都有充分的理解了,我們下面就直接看ViewDragHelper
里processTouchEvent()
方法的實現.
3.processTouchEvent()方法的實現
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]);
//拖拽至指定位置
//TODO (5)
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
//如果還不是拖拽狀態,就檢測是否經過了一個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 MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
//如果當前點正在被拖拽,則再剩余還在觸摸的點鐘尋找是否正在View上
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
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) {
// This one's going away, skip.
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) {
// We didn't find another pointer still touching the view, release it.
//如果沒找到則釋放View
//TODO (6)
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP: {
//如果是拖拽狀態的釋放則調用
//releaseViewForPointerUp()
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()方法
private void saveInitialMotion(float x, float y, int pointerId) {
//確保各個數組的大小足夠存放數據
ensureMotionHistorySizeForId(pointerId);
//保存x坐標
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
//保存y坐標
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
//保存是否觸摸到邊緣
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
//保存當前id是否在觸摸,用于后續驗證
mPointersDown |= 1 << pointerId;
}
5.findTopChildUnder()方法
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()方法
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()方法
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
//如果已經捕獲該View 直接返回true
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
//根據mCallback.tryCaptureView()方法來最終決定是否可以捕獲View
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
//如果可以則調用captureChildView(),并返回true
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
可以看到如果可以捕獲View
則調用了captureChildView()
方法:
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
mCapturedView = childView;
mActivePointerId = activePointerId;
//回調callback
mCallback.onViewCaptured(childView, activePointerId);
//設定mDragState的狀態為STATE_DRAGGING
setDragState(STATE_DRAGGING);
}
如果程序執行到這里,就證明View
已經處于拖拽狀態了,后續的觸摸操作,將直接根據mDragState
為STATE_DRAGGING
的狀態處理.
8.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) {
//回調callback來決定View最終被拖拽的x方向上的偏移量
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
//移動View
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
//回調callback來決定View最終被拖拽的y方向上的偏移量
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
//移動View
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
//回調callback
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
因為dragTo()
方法是在processTouchEvent()
中的MotionEvent.ACTION_MOVE case
被調用所以當程序運行到這里時View
就會不斷的被拖動了。如果一旦手指釋放則最終會調用releaseViewForPointerUp()
方法
8.releaseViewForPointerUp()方法的實現
private void releaseViewForPointerUp() {
//計算出當前x和y方向上的加速度
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()
:
private void dispatchViewReleased(float xvel, float yvel) {
//設定當前正處于釋放階段
mReleaseInProgress = true;
//回調callback的onViewReleased()方法
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
//設定狀態
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
//如果onViewReleased()中沒有調用任何方法,則狀態設定為STATE_IDLE
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
后如果項目里有多處使用,可以包裝成一個提供給我們自己項目的模塊使用,防止出現更多的多余代碼.