ViewDragHelper 簡單實現類:
public class VGHLayout extends LinearLayout {
private ViewDragHelper mHelper;
private View mFreeView;
private View mEdgeView;
private View mReleaseView;
private Point autoBackPoint = new Point();
public VGHLayout(Context context) {
this(context, null);
}
public VGHLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VGHLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 定義構造函數
* this-指定當前使用的容器
* 1.0f-敏感度,越大越敏感
* Callback-拖拽方法回掉
*
* ps:helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
*/
mHelper = ViewDragHelper.create(this, 1.0f, new DemoDragHelperCallback());
mHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
class DemoDragHelperCallback extends ViewDragHelper.Callback {
/**
* 如果返回true,則表示可以捕獲該view,你可以根據傳入的第一個view參數
* 決定哪些可以捕獲
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child != mEdgeView;
}
/**
* 可以在該方法中,對child移動的邊界進行控制
* left和top就是當前view即將移動的位置,需要做相關的邊界控制
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - getPaddingRight() - child.getWidth();
return Math.min(Math.max(left, leftBound), rightBound);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - getPaddingBottom() - child.getHeight();
return Math.min(Math.max(top, topBound), bottomBound);
}
/**
* 我們把我們的TextView全部加上clickable=true,意思就是子View可以消耗事件。再次運行,
* 你會發現本來可以拖動的View不動了,主要是因為,如果子View不消耗事件,
* 那么整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent,在onTouchEvent的DOWN的時候就確定了captureView。
* 如果消耗事件,那么就會先走onInterceptTouchEvent方法,判斷是否可以捕獲,
* 而在判斷的過程中會去判斷另外兩個回調的方法:getViewHorizontalDragRange和getViewVerticalDragRange,
* 只有這兩個方法返回大于0的值才能正常的捕獲,方法的返回值為當前view的移動范圍,如果只是移動一個方向
* 則只重寫其中的方法即可
*/
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
/**
* 手指釋放時回掉的方法
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == mReleaseView) {
/**
* 使用下沉的回掉效果
*/
/**
* smoothSlideViewTo,將子控件平滑移動到指定位置。它調用了forceSettleCapturedViewAt,在forceSettleCapturedViewAt中會調用Scroller.startScroll方法。
* settleCapturedViewAt,將子控件移動到指定位置。與smoothSlideViewTo相似,它也調用了forceSettleCapturedViewAt方法。與smoothSlideViewTo不同的是,它以手指離開時的速度為初速度,將子控件移動到指定位置。
* captureChildView方法,將指定的子控件移動到指定位置。與上面兩個方法不同的是,它直接移動到指定位置,不會有時間上的等待,也就是說不會有那種平滑的感覺。
* lingCapturedView方法,與settleCapturedViewAt類似,都使用了手指離開時的速度作為計算的當前位置的依據。
*/
mHelper.settleCapturedViewAt(autoBackPoint.x, autoBackPoint.y);
/**
* 重新進行繪制
*/
invalidate();
}
}
/**
* 在邊界拖動時候進行回掉
*/
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mHelper.captureChildView(mEdgeView, pointerId);
}
/**
* 當ViewDragHelper狀態發生變化時回調(IDLE,DRAGGING,SETTING[自動滾動時])
*/
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
/**
* 當captureview的位置發生改變時回調
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 當captureview被捕獲時回調
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
/**
* true的時候會鎖住當前的邊界,false則unLock。
*/
@Override
public boolean onEdgeLock(int edgeFlags) {
return super.onEdgeLock(edgeFlags);
}
/**
* 當觸摸到邊界時回調。
*/
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
}
/**
* 改變同一個坐標(x,y)去尋找captureView位置的方法。(具體在:findTopChildUnder方法中)
*/
@Override
public int getOrderedChildIndex(int index) {
return super.getOrderedChildIndex(index);
}
}
/**
* 將touch事件的攔截和處理交由DragHelper進行處理
*/
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return mHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mHelper.processTouchEvent(event);
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
autoBackPoint.x = mReleaseView.getLeft();
autoBackPoint.y = mReleaseView.getTop();
}
/**
* 填充完畢后調用此方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFreeView = getChildAt(0);
mEdgeView = getChildAt(1);
mReleaseView = getChildAt(2);
}
/**
* 重寫computeScroll方法,實現fling或者松手后的滑動效果
* computeScroll也不是來讓ViewGroup滑動的,真正讓ViewGroup滑動的是scrollTo,scrollBy.
* computeScroll的作用是計算ViewGroup如何滑動
* 調用startScroll()是不會有滾動效果的,只有在computeScroll()獲取滾動情況,做出滾動的響應
* computeScroll在父控件執行drawChild時,會調用這個方法
* 并在ViewDragHelper.Callback的onViewReleased()方法里調用
* settleCapturedViewAt()、flingCapturedView(),或在任意地方調用smoothSlideViewTo()方法。
*/
@Override
public void computeScroll() {
/**
* continueSettling,當ViewDragHelper的狀態是STATE_SETTLING(自動滾動)時,
* 該方法會將子控件自動進行移動(使用Scroller實現測量空前當前應該處于的位置,
* 然后調用View.offsetLeftAndRight和View.offsetTopAndBottom方法進行移動)。
* 在自定義控件中的computeScroll方法中調用。computeScroll方法用于處理自動移動的狀況,
* 通常是在MONTIONEVENT.ACTION_UP的時候,調用Scroller.startScroll方法。
*/
/**
* 解析:
* continueSettling方法調用的是View.offsetLeftAndRight和offsetTopAndBottom方法來實現滑動效果。
* ViewDragHelper中另一個方法dragTo,同樣使用的是offsetLeftAndRight實現滑動,
* 而dragTo只在processTouchEvent的ACTION_MOVE中調用。
*/
if (mHelper.continueSettling(true)) {
invalidate();
}
}
/**
* 方法正常流程的回掉順序:
* shouldInterceptTouchEvent:
*DOWN:
* getOrderedChildIndex(findTopChildUnder)
* ->onEdgeTouched
*
* MOVE:
* getOrderedChildIndex(findTopChildUnder)
* ->getViewHorizontalDragRange &
* getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
* ->clampViewPositionHorizontal&
* clampViewPositionVertical
* ->onEdgeDragStarted
* ->tryCaptureView
* ->onViewCaptured
* ->onViewDragStateChanged
*
* processTouchEvent:
*
* DOWN:
* getOrderedChildIndex(findTopChildUnder)
* ->tryCaptureView
* ->onViewCaptured
* ->onViewDragStateChanged
* ->onEdgeTouched
* MOVE:
* ->STATE==DRAGGING:dragTo
* ->STATE!=DRAGGING:
* onEdgeDragStarted
* ->getOrderedChildIndex(findTopChildUnder)
* ->getViewHorizontalDragRange&
* getViewVerticalDragRange(checkTouchSlop)
* ->tryCaptureView
* ->onViewCaptured
* ->onViewDragStateChanged
* 從上面也可以解釋,我們在之前TextView(clickable=false)的情況下,
* 沒有編寫getViewHorizontalDragRange方法時,是可以移動的。
* 因為直接進入processTouchEvent的DOWN,然后就onViewCaptured、onViewDragStateChanged(進入DRAGGING狀態),
* 接下來MOVE就直接dragTo了。
* 而當子View消耗事件的時候,就需要走shouldInterceptTouchEvent,MOVE的時候經過一系列的判斷(getViewHorizontalDragRange,clampViewPositionVertical等),
* 才能夠去tryCaptureView。
*/
}
效果圖:
效果如圖