在前一篇文章從PhotoView看Android手勢監(jiān)聽實(shí)踐中膏秫,介紹了PhotoView這一控件的手勢控制的分析右遭,其中有三個主要行為的觸發(fā)做盅,Drag,F(xiàn)ling窘哈,Scale吹榴,而在PhotoView的實(shí)現(xiàn)中除了Scale采取的是一個ScaleGestureDetector這樣的一個高級類,前面兩種行為都是依賴原生的手勢來判斷滚婉,十分的麻煩图筹,代碼量也很大, 那么這兩個有沒有比較簡單實(shí)用的類呢让腹?
結(jié)論自然是肯定的远剩,這篇文章要介紹的就是這么一個閃亮的存在,ViewDragHelper骇窍。先看一下官方對這個類的一個定義瓜晤。
ViewDragHelper是一個在自定義ViewGroup中十分實(shí)用的類,它提供了一系列有用的操作和狀態(tài)追蹤來幫助用戶實(shí)現(xiàn)在一個ViewGroup內(nèi)拖動View或者復(fù)位 腹纳。
總體設(shè)計(jì)
ViewDragHelper 只有一個類痢掠,但是內(nèi)部還有一個抽象類CallBack。
CallBack中有一系列方法嘲恍,用來設(shè)置許多屬性足画,可拖動的范圍,邊緣檢測佃牛,哪個View觸發(fā)拖動等等淹辞。這個CallBack是在初始化一個ViewDragHelper 時的必要參數(shù)。
除了CallBack之外俘侠,ViewDragHelper 依然是通過 shouldInterceptTouchEvent和 processTouchEvent 以及設(shè)置的屬性來設(shè)置狀態(tài)判斷拖動象缀,不過這些被封裝后就不需要我們自己寫了,省時省力爷速,ViewDragHelper 內(nèi)部實(shí)際上是一個小型狀態(tài)機(jī)攻冷,在IDLE,DRAGGING遍希,SETTLING三種狀態(tài)之間切換。
流程圖
這個圖是我們在使用一個ViewDragHelper 所需要做的事情里烦,ViewDragHelper使用一個靜態(tài)的方式來創(chuàng)建一個對象
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;
}
第一個參數(shù)就是ParentView的引用凿蒜,第二個參數(shù)是一個觸發(fā)的靈敏程度,默認(rèn)為1.0胁黑,第三個就是圖中的自定義的CallBack废封。
在CallBack中,我們需要根據(jù)自己的需要實(shí)現(xiàn)對應(yīng)的方法丧蘸,總體來說主要是上圖中的幾個方法:
tryCaptureView: 在這個方法中漂洋,我們會去聲明我們想要產(chǎn)生Drag的View,這個方法是有返回值的,只有在返回true的情況下刽漂,才有權(quán)限去真正的產(chǎn)生Drag的行為演训,我們直接看這個方法在源碼中的調(diào)用
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;
}
toCapture
也就是我們現(xiàn)在手指所在的View,mCapturedView
就是ViewDragHelper 中當(dāng)前已經(jīng)有Drag狀態(tài)的View贝咙,實(shí)際上即使已經(jīng)產(chǎn)生了拖動样悟,這個方法依然會不斷的觸發(fā),在手指Id和View都相同的情況下庭猩,就直接return true窟她,如果是第一次,這里的mCallback.tryCaptureView(toCapture, pointerId)
的返回值決定了是否會走到條件語句之內(nèi)蔼水,因此需要在實(shí)現(xiàn)的時候如果想要觸發(fā)Drag震糖,這個方法一定要返回true。
onEdgeDragStarted:如果我們設(shè)置了可以在邊緣觸摸滑動趴腋,那么可以在這個方法中實(shí)現(xiàn)一個側(cè)滑的效果吊说,通過手動調(diào)用ViewDragHelper的 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 = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
這個方法可以擺脫前面 tryCaptureView 需要返回true的一個限制,即使返回false于样,在這里依然能夠?qū)鬟M(jìn)來的childView的狀態(tài)置為STATE_DRAGGING疏叨。
clampViewPositionVertical: 這個方法還有一個對應(yīng)方法,這兩個方法主要是用來指定DragView的活動范圍
clampViewPositionVertical(View child, int top, int dy)
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(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);
...
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);
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
在ACTION_MOVE的時候穿剖,根據(jù)移動的距離delta蚤蔓,調(diào)用了dragTo的方法,在這里由我們實(shí)現(xiàn)的clampViewPositionVertical
方法根據(jù)一系列參數(shù)糊余,返回了一個最后的X秀又,Y坐標(biāo),通過ViewCompat的兩個方法來實(shí)現(xiàn)View的位置變換贬芥,從上面的變換可以看出我們需要返回的是View最終能到達(dá)的地方吐辙。
onViewReleased: 這個就是在手指抬起的時候或者超出邊界了會觸發(fā),如果想實(shí)現(xiàn)一個側(cè)滑菜單蘸劈,那么在這里可以根據(jù)給予的速度的參數(shù)來決定是否去打開或者關(guān)閉菜單昏苏。
除了CallBack之外,還有一個重要的點(diǎn)威沫,那就是ViewDragHelper 怎么與MotionEvent連接起來贤惯,我們在創(chuàng)建ViewDragHelper 實(shí)例的時候需要傳入一個ParentView,這是一個ViewGroup棒掠,我們需要drag的view就是這個父控件的子View孵构,所以我們需要在onInterceptTouchEvent的時候采取ViewDragHelper 的shouldInterceptTouchEvent
方法
return mDragState == STATE_DRAGGING;
這個方法的返回是一個判斷語句,判斷是否是Drag狀態(tài)烟很,那么肯定有一個設(shè)置狀態(tài)的地方
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
在down和Pointer_down的時候去判斷能不能設(shè)置這個狀態(tài),不過前面就說了颈墅,對于邊緣檢測型蜡镶,攔不攔無所謂,直接可以繞過tryCaptureView那一關(guān)恤筛,對于直接Drag的還是需要的官还,不過事件可能被子View截獲了。
除了這個之外叹俏,我們還需要實(shí)現(xiàn)一個onTouchEvent妻枕,ViewDragHelper 也提供了一個對應(yīng)的方法 processTouchEvent
,這個主要就是用來drag view用的粘驰,這里最關(guān)鍵的就是onTouchEvent這個方法的返回值屡谐,具體情況具體分析,如果返回true蝌数,后續(xù)的所有事件就都由這個父控件接送了愕掏,那么自然drag行為也就可以觸發(fā)了。如果不返回true顶伞,那么除了down事件外饵撑,沒有別的事件可以接收了,除非邊緣是一個有點(diǎn)擊事件的子view唆貌。
側(cè)滑實(shí)現(xiàn)
分析了那么多滑潘,還是模仿一個側(cè)滑的實(shí)現(xiàn),效果十分的簡單
如果不使用ViewDragHelper锨咙,那么這個需要多長的代碼不清楚语卤,但是使用ViewDragHelper,這個效果不需要100行酪刀。先放代碼
public class NavigationView extends LinearLayout {
private static final String TAG = "NavigationView";
private static final int RIGHT = 100;
private static final int MIN_VELOCITY = 300;
private static float density;
private ViewDragHelper mDragHelper;
private View mContent;
private View mMenu;
public NavigationView(Context context) {
this(context, null);
}
public NavigationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(HORIZONTAL);
mDragHelper = ViewDragHelper.create(this, new CustomCallBack());
mDragHelper.setEdgeTrackingEnabled(EDGE_LEFT);
density = getResources().getDisplayMetrics().density;
}
private class CustomCallBack extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mMenu;
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mDragHelper.captureChildView(mMenu,pointerId);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
int newLeft = Math.max(-child.getWidth(),Math.min(left,0));
return newLeft;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (xvel > MIN_VELOCITY || releasedChild.getLeft() >-releasedChild.getWidth() * 0.5) {
mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
}else {
mDragHelper.settleCapturedViewAt(-releasedChild.getWidth(), releasedChild.getTop());
}
invalidate();
}
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if(count >= 2){
//簡單寫了 直接寫死
mMenu = getChildAt(1);
mContent = getChildAt(0);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//如果menu的寬度是match_parent或者超過限制 那么就需要重新設(shè)置
int width = (int) (density * RIGHT);
if (mMenu.getMeasuredWidth() + width > getWidth()){
int menuWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() -width,MeasureSpec.EXACTLY);
mMenu.measure(menuWidthSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mMenu != null){
mMenu.layout(-mMenu.getMeasuredWidth(),t,0,mMenu.getMeasuredHeight());
}
if (mContent != null){
mContent.layout(0,0,mContent.getMeasuredWidth(),mContent.getMeasuredHeight());
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean event = mDragHelper.shouldInterceptTouchEvent(ev);
return event;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG,"onTouchEvent" + event.toString());
mDragHelper.processTouchEvent(event);
return true;
}
}
這里盡量寫的簡單粹舵,但是核心的東西不會少,兩個View骂倘,一個是側(cè)滑里面的menu眼滤,一個是外面的主content。這里直接繼承了LinearLayout 历涝,measure時如果寬度過大诅需,也會做一個限制,然后layout到屏幕外面去荧库。
根據(jù)前面的方法的分析诱担,這里的邏輯就一目了然了,設(shè)置一個左邊邊緣檢測电爹,在 onEdgeDragStarted
上面去drag我們的menu菜單,除此之外料睛,在 onViewReleased
的時候根據(jù)速度和當(dāng)前menu的位置判斷后去設(shè)置最終滑動的位置丐箩,這里是一個Scroller摇邦,所有務(wù)必實(shí)現(xiàn)一個 computeScroll
。
寫的比較的簡潔屎勘,其中還有很多可以完善的地方施籍,比如添加開閉按鈕,判斷更準(zhǔn)確一點(diǎn)概漱,不過這些都是后續(xù)的小細(xì)節(jié)丑慎,這里為的是簡單但不失主體。
整個源碼在github上: https://github.com/sheepm/ViewDragHelper_Sample