自定義ViewGroup最常添加的功能就是子View的拖動,若是你的事件分發及處理的基本功很是紮實,那麼徹底能夠本身實現這個功能。然而幸運的是,系統提供了一個工具類ViewDragHelper
,它提供了這個功能實現的框架,這樣就大大提升了開發的效率。php
本文不只僅告訴你這個工具類該怎麼使用,並且也會分析它的設計原理。只有掌握原理了,才能在實際中作到以不變應萬變。java
本文須要你對事件的分發和處理有基本的認識,若是你還沒掌握,能夠參考我以前寫的三篇文章android
若是你對事件分發和處理的流程不熟悉,你可能從本文中只學到如何使用ViewDragHelper類,可是並不會掌握它的精華。app
既然ViewDragHelper
是一個工具框架類,那麼對事件的處理確定也是作好了封裝。假設有一個自定義ViewGroup類,名字叫作VDHLayout
。咱們來看下如何使用ViewDragHelper
類實現事件的處理。框架
public class VDHLayout extends ViewGroup {
ViewDragHelper mViewDragHelper;
public VDHLayout(Context context) {
this(context, null);
}
public VDHLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 建立ViewDragHelper對象,回調參數用來控制子View的拖動
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return false;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
// 簡單點,只操做第一個子View
View first = getChildAt(0);
first.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + first.getMeasuredWidth(),
getPaddingTop() + first.getMeasuredHeight());
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 利用ViewDragHelper來判斷是否須要截斷
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 利用ViewDragHelper來處理子View的拖拽
mViewDragHelper.processTouchEvent(event);
return true;
}
}
複製代碼
VDHLayout
繼承自ViewGroup
,爲了簡單起見,只對它的第一個子View進行佈局,這就是在onLayout()
中的操做。ide
事件處理的代碼是在onInterceptTouchEvent()
和onTouchEvent()
方法中實現的,從代碼中能夠看到,分別用ViewDragHelper.shouldInterceptTouchEvent()
和ViewDragHelper.processTouchEvent()
來處理事件。工具
如今,咱們已經成功地用ViewDragHelper
實現了事件的處理,那麼子View的拖動是在哪裏控制的呢?這個實際上是在建立ViewDragHelper
對象的時候,用傳入的回調參數控制的。從代碼中能夠看到,咱們只實現了回調中的一個方法tryCaptureView()
,這個方法也是必需要實現的。源碼分析
根據事件分發和處理的原理可知,VDHLayout
的子View是否能處理ACTION_DOWN
事件,關乎着VDHLayout
的事件分發和處理的邏輯。ViewDragHelper
的回調固然也是受這個的影響的,所以我將分兩部分來說解如何實現回調。佈局
首先咱們來看下子View不處理事件的狀況。post
根據View事件分發和處理的原理可知,若是一個View不設置任何監聽事件,而且不可點擊,也不可長按,那麼這個View就不處理任何事件。
理論上講的有點抽象,舉個例子,例如 在XML佈局中給VDHLayout
添加一個ImageView
控件
<?xml version="1.0" encoding="utf-8"?>
<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<ImageView android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />
</com.bxll.vdhdemo.VDHLayout>
複製代碼
這個ImageView
沒有任何監聽事件,默認不可點擊也不可長按的,所以它就是一個不處理事件的子View。
如今以這個佈局爲例進行分析,當手指點擊ImageView
的時候,因爲子View,也就是ImageView
,不處理事件,因此ACTION_DOWN
事件必定會先通過VDHLayout.onInterceptTouchEvent()
,再通過VDHLayout.onTouchEvent()
。
根據事件處理的經驗,真正的處理邏輯其實都在VDHLayout.onTouchEvent()
中,它的實現以下
public boolean onTouchEvent(MotionEvent event) {
// 利用ViewDragHelper來處理子View的拖拽
mViewDragHelper.processTouchEvent(event);
return true;
}
複製代碼
因爲
VDHLayout
要經過觸摸事件控制子View拖動,所以在onTouchEvent()
中必需要返回true
。
能夠看到,是用ViewDragHelper.processTouchEvent()
來實現VDHLayout.onTouchEvent()
的,如今來看看ViewDragHelper.processTouchEvent()
是如何處理ACTION_DOWN
事件的
public void processTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
// 1. 找到事件做用於哪一個子View
final View toCapture = findTopChildUnder((int) x, (int) y);
// 保存座標值
saveInitialMotion(x, y, pointerId);
// 2. 嘗試捕獲這個用於拖動的子View
tryCaptureViewForDrag(toCapture, pointerId);
// 邊緣觸摸回調
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// ...
}
}
複製代碼
首先經過findTopChildUnder()
方法找到手指按下的那個子View
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
// getOrderedChildIndex()回調決定了獲取哪一個子View
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,然而咱們能夠發現,回調方法getOrderedChildIndex()
決定了究竟是哪一個子View被找到。從這裏能夠看出,手指操做的並不必定都是最上面的子View。
找到了ACTION_DOWN
做用的子View後,就經過tryCaptureViewForDrag()
來嘗試捕獲這個子View
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
// 經過回調判斷這個子View是否能被捕獲
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
複製代碼
首先經過tryCaptureView()
回調方法判斷子View是否可以被捕獲,被捕獲的子View才能被用來拖動。
若是可以被捕獲,那麼就調用captureChildView()
通知子View被捕獲
public void captureChildView(@NonNull View childView, int activePointerId) {
// mCapturedView表明被用來拖動的目標
mCapturedView = childView;
mActivePointerId = activePointerId;
// 回調通知View被捕獲
mCallback.onViewCaptured(childView, activePointerId);
// 設置爲拖動狀態
setDragState(STATE_DRAGGING);
}
複製代碼
captureChildView()
是經過onViewCaptured()
進行回調,通知子View已經被捕獲。
如今,來總結下ViewDragHelper.processTouchEvent()
對ACTION_DOWN
事件的處理中,回調作了哪些事事情(只列舉主要的回調)
getOrderedChildIndex()
回調,判斷ACTION_DOWN
做用於哪一個子View。tryCaptureView()
回調,判斷子View是否能被捕獲。onViewCaptured()
回調,通知哪一個子View被捕獲。ACTION_DOWN
處理完了,如今咱們來看看ACTION_MOVE
事件如何處理的。
因爲子View不處理事件,ACTION_MOVE
事件交由VDHLayout.onTouchEvent()
處理,也就是交給了ViewDragHelper.processTouchEvent()
處理。
public void processTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// 判斷手指是否有效
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(index);
// 獲取x,y軸上拖動的距離差
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
// 對於目標View執行拖動
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// ...
}
break;
}
// ...
}
}
複製代碼
ViewDragHelper.processTouchEvent()
對ACTION_MOVE
的處理中,首先計算在x,y軸上移動的距離差,而後經過dragTo()
方法拖動剛剛捕獲的子View。
咱們注意下dragTo()
第一個參數和第二個參數,它指的是目標View(被捕獲的子View)理論上要移動到的座標點。
private void dragTo(int left, int top, int dx, int dy) {
// clampedX, clampedY表示目標View要拖動到的終點座標
int clampedX = left;
int clampedY = top;
// 獲取目標View的起始座標
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
// 若是拖動的距離大於0,經過回調獲取目標View最終要拖動到的x座標值
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
// 目標View在水平方向移動
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
// 若是拖動的距離大於0,經過回調獲取目標View最終要拖動到的x座標值
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;
// 回調通知,目標View實際移動到(clampedX, clampedY),以及x,y軸實際移動的距離差爲clampedDx, clampedDy
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
複製代碼
x,y方向上,只要任意一個方向上手指拖動的距離大於0, 那麼就經過clampViewPositionHorizontal()
/clampViewPositionVertical()
回調方法,計算目標View實際須要拖動到的終點座標。
經過回調計算出來終點座標後,就把目標View移動到這個計算出來的座標點上。
最後,只要x,y方向上拖動距離大於0,那麼就經過onViewPositionChanged()
回調方法,通知目標View實際拖動到哪一個座標,以及實際拖動的距離差。
如今咱們明白了,ViewDragHelper.processTouchEvent()
處理ACTION_MOVE
,實際上就是處理目標View的拖動,它用到了以下回調
clampViewPositionHorizontal()
和clampViewPositionVertical()
回調,用來計算目標View拖動的實際座標。onViewPositionChanged()
回調,通知目標View實際被拖動到哪一個座標,以及在x,y軸上拖動的實際距離差。有了前面的理論基礎,如今咱們來實現下回調,讓不處理事件的子View可以被拖動,並且只容許在水平方向上被拖動。
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
// 爲簡單起見,全部的View均可以被拖動
return true;
}
/** * 控制目標View在x方向的移動。 */
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// 不容許垂直方向移動
return 0;
}
/** * 控制目標View在y方向的移動。 */
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
// 水平方向移動不能超出父View範圍
return Math.min(Math.max(0, left), getWidth() - child.getWidth());
}
});
複製代碼
因爲咱們不容許垂直方向的拖動,所以clampViewPositionHorizontal()
要返回0,clampViewPositionVertical()
的返回值要控制在VDHLayout
範圍內滑動。效果以下
在前面的分析中還有其它的一些回調,能夠根據實際項目要求進行復寫實現。
如今來分析子View可以處理事件的狀況。讓子View能處理事件最簡單的方式是設置它能夠點擊,例如
<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<ImageView android:clickable="true" android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />
</com.bxll.vdhdemo.VDHLayout>
複製代碼
當利用這個佈局再次運行程序的時候,你會發現原來能夠拖動的ImageView
不能被拖動了。這是由於事件的處理邏輯改變了,從而ViewDragHelper
的實現邏輯也改變了。
因爲子View能處理事件,所以對於ACTION_DOWN
事件,就只會通過VDHLayout.onInterceptTouchEvent()
方法,而並不會通過VDHLayout.onTouchEvent()
方法。從前面的代碼實現可知,VDHLayout.onInterceptTouchEvent()
是由ViewDragHelper.shouldInterceptTouchEvent()
實現。然而ViewDragHelper.shouldInterceptTouchEvent()
方法對於ACTION_DOWN
只是簡單一些簡單處理,並不會截斷事件。
所以咱們須要分析ACTION_MOVE
是如何被ViewDragHelper.shouldInterceptTouchEvent()
截斷的。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
// ...
final int pointerCount = ev.getPointerCount();
// 只考慮單手指操做
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
// 1. 判斷是否達到拖動的標準
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
// 一個不截斷的狀況:若是拖動標準,卻沒有實際的拖動距離,那就不截斷事件
if (pastSlop) {
//獲取新,舊座標值
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
// 經過回調獲取x,y方向的拖動範圍
final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
// 沒有實際的拖動距離就不截斷事件
if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
&& (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
break;
}
}
// 報告邊緣動
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
// 2. 若是達到拖動的臨界距離,那麼就嘗試捕獲子View
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
}
// 若是成功捕獲子View,那麼狀態就會被設置爲STATE_DRAGGING,也就表明截斷事件
return mDragState == STATE_DRAGGING;
}
複製代碼
ViewDragHelper.shouldInterceptTouchEvent()
考慮了多手指的狀況,爲了簡化分析,只考慮單手指的狀況。
第一步,判斷是否達到拖動的條件,有兩個條件
checkTouchSlop()
返回true根據事件處理的經驗,若是要截斷
ACTION_MOVE
事件,必需要有條件地截斷。
checkTouchSlop()
方法用來判斷是否達到的拖動的臨界距離
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
// 經過回調方法判斷x,y方向是否容許拖動
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
// 若是x或y方向容許拖動,根據拖動的距離計算是否達到拖動的臨界值
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;
}
// 若是x和y方向都不容許拖動,那就永遠不可能達到拖動臨界值
return false;
}
複製代碼
首先經過getViewHorizontalDragRange()
和getViewVerticalDragRange()
獲取x,y方向拖動範圍,只要這個範圍大於0,就表明能夠在x,y方向上拖動。而後根據哪一個方向能夠拖動,就相應的計算拖動的距離是否達到了臨界距離。
如今回到shouldInterceptTouchEvent()
方法的第二步,當達到了拖動條件後,就調用tryCaptureViewForDrag()
嘗試捕獲目標View,這個方法在前面已經分析過,它會首先回調tryCaptureView()
肯定目標View是否能被拖動,若是能拖動,再回調onViewCaptured()
通知目標View已經捕獲,最後設置狀態爲STATE_DRAGGING
。
當狀態設置爲了STATE_DRAGGING
後,那麼ViewDragHelper.shouldInterceptTouchEvent()
返回值就是true
,也就是說VDHLayout.onInterceptTouchEvent()
截斷了ACTION_MOVE
事件。
VDHLayout.onInterceptTouchEvent()
截斷了ACTION_MOVE
事件後,後續的ACTION_MOVE
事件就交給了VDHLayout.onTouchEvent()
方法,也就是交給了ViewDragHelper.processTouchEvent()
處理。這個方法以前分析過,就是處理目標View的拖動。
那麼如今咱們來總結下ViewDragHelper.shouldInterceptTouchEvent()
在處理ACTION_MOVE
截斷的時候,用到哪些關鍵回調
getViewHorizontalDragRange()
和getViewVerticalDragRange()
方法判斷x,y方向上是否能夠拖動。返回值大於0表示能夠拖動。通過剛纔的分析,咱們知道,對於一個能處理事件的子View,若是想讓它能被拖動,必須複寫getViewHorizontalDragRange()
或getViewVerticalDragRange()
回調,用於告訴ViewDragHelper
,在相應的方向上容許被拖動。
那麼如今,咱們就來解決子View(能處理事件)不能拖動的問題,咱們仍然只讓子View在水平方向上被拖動
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
// 爲簡單起見,全部的View均可以被拖動
return true;
}
/** * 控制目標View在x方向的移動。 */
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// 不容許垂直方向移動
return 0;
}
/** * 控制目標View在y方向的移動。 */
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
// 水平方向移動不能超出父View範圍
return Math.min(Math.max(0, left), getWidth() - child.getWidth());
}
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
// 因爲只容許目標View在VDHLayout中水平拖動,所以水平拖動範圍就是VDHLayout的寬度減去目標View寬度
return getWidth() - child.getWidth();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
// 因爲不容許垂直方向拖動,所以拖動範圍也就是0
return 0;
}
});
}
複製代碼
因爲咱們只容許水平方向拖動,所以getViewVerticalDragRange()
返回的垂直方向的拖動範圍就是0,getViewHorizontalDragRange()
返回的水平方向的拖動範圍就是getWidth() - child.getWidth()
。
ViewDragHelper
有一個邊緣觸摸功能,這個邊緣觸摸的功能比較簡單,所以我並不打算從源碼進行分析,而只是從API角度進行說明。
要向觸發邊緣滑動功能,首先要調用ViewDragHelper.setEdgeTrackingEnabled(int edgeFlags)
方法,設置哪一個邊緣容許跟蹤。參數有以下幾個可用值
public static final int EDGE_LEFT = 1 << 0;
public static final int EDGE_RIGHT = 1 << 1;
public static final int EDGE_TOP = 1 << 2;
public static final int EDGE_BOTTOM = 1 << 3;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
複製代碼
邊緣觸摸的回調有以下幾個
/** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. */
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/** * Called when the given edge may become locked. This can happen if an edge drag * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} * was called. This method should return true to lock this edge or false to leave it * unlocked. The default behavior is to leave edges unlocked. */
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/** * Called when the user has started a deliberate drag away from one * of the subscribed edges in the parent view while no child view is currently captured. */
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
複製代碼
註釋已經很清楚的解釋了這幾個回調的時機,我獻醜來翻譯下
onEdgeTouched()
: 當沒有子View被捕獲,而且容許邊緣觸摸,當用戶觸摸邊緣時回調。onEdgeLock()
: 用來鎖定鎖定哪一個邊緣。這個回調是在onEdgeTouched()
以後,開始拖動以前調用的。onEdgeDragStarted()
: 當沒有子View被捕獲,而且容許邊緣觸摸,當用戶已經開始拖動的時候回調。系統控件DrawerLayout
就是利用ViewDragHelper
的邊緣滑動功能實現的。因爲篇幅緣由,我就不用例子來展現邊緣觸摸的功能如何使用了。
ViewDragHelper
還有一個View定義的功能,利用OverScroller
實現。有以下幾個方法
/** * 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. */
public boolean settleCapturedViewAt(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. */
public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {}
/** * 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. */
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {}
複製代碼
從註釋能夠能夠看出,這個三個方法都須要在下一幀刷新的時候調用continueSettling()
,這個就與OverScroller
的用法是一致的。
如今,來利用settleCapturedViewAt()
方法實現一個功能,讓拖動的View被釋放後,回到原點。
當拖動的View被釋放後,會回調onViewReleased()
方法
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (mViewDragHelper.settleCapturedViewAt(0, 0)) {
invalidate();
}
}
複製代碼
因爲利用的是OverScroller
來實現的,所以必須調用進行重繪。重繪的時候,會調用控件的computeScroll()
方法,在這裏調用剛纔說講的continueSettling()
方法
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
複製代碼
continueSettling()
也是對OverScroller
邏輯的封裝,若是返回true
就表明這個定位操做還在進行中,所以還須要繼續調用重繪操做。
想了解其中的原理,你必定要熟悉
OverScroller
的原理。
如此一來就能夠實現以下效果
不少絢麗的視圖拖動操做,每每都是用ViewDragHelper
實現的,這個工具類簡直是一個集大成之做,咱們須要徹底掌握它,這樣咱們才能遊刃有餘地在自定義ViewGroup中實現各類牛逼的View拖動效果。