編輯:關於Android編程
前言: 通過上一篇的為何說Android ViewDragHelper是神器 (一)中我們簡單了解了ViewDragHelper的用法,然後實現了一個“view隨手指滑動而滑動”的效果,代碼很簡單,但是VDH中處理的邏輯卻很多很多,不得不說VDH真的是神器,要我們自己寫的話得寫一段時間了,接下來我們繼續往下研究研究VDH,加油吧!騷年(^__^) !!!
以下demo內容大致參考鴻陽博客中的Android ViewDragHelper解析 一文,陽神一直是我崇拜的一個偶像(^__^) 。
ViewDragHelper還能做以下的一些操作:
邊界檢測、加速度檢測(eg:DrawerLayout邊界觸發拉出)
移動到某個指定的位置(eg:點擊Button,展開/關閉Drawerlayout)
回調Drag Release(eg:DrawerLayout部分,手指抬起,自動展開/收縮)
那麼我們接下來對我們最基本的例子進行改造,包含上述的幾個操作。
我們再創建兩個view,id叫autobackview(拖動後手指一抬起返回初始位置),edgeview(滑動邊緣開始滑動的view):
ids.xml:
text_layout.xml:
我們改改DragView:
package com.cisetech.demo; import android.content.Context; import android.graphics.Point; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; /** * author:yinqingy * date:2016-11-06 13:49 * blog:http://blog.csdn.net/vv_bug * desc: */ public class DragView extends LinearLayout{ private ViewDragHelper mDragger; private View mEdgeView,mAutoBackView; private Point mAutoBackOriginPos=new Point(); public DragView(Context context) { super(context); init(); } public DragView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public DragView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mDragger=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { //當id為edgeview的時候,不允許其滑動 return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } @Override public int clampViewPositionVertical(View child, int top, int dy) { return top; } /** * 當手指在邊緣拖動的時候回調此方法 * edgeFlags分為left、top、right、bottom */ @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { //當在邊緣滑動的時候 mDragger.captureChildView(mEdgeView, pointerId); } //手指釋放的時候回調 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { //mAutoBackView手指釋放時可以自動回去 if (releasedChild.getId()==R.id.autobackview) { mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y); invalidate(); } } }); //一定要加上這句代碼,不然就checkNewEdgeDrag就不會進入判斷了 mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mDragger.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { mDragger.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); //獲取autoBackView的初始位置 mAutoBackOriginPos.x = mAutoBackView.getLeft(); mAutoBackOriginPos.y = mAutoBackView.getTop(); } @Override protected void onFinishInflate() { super.onFinishInflate(); mEdgeView=findViewById(R.id.edgeview); mAutoBackView=findViewById(R.id.autobackview); } @Override public void computeScroll() { if(mDragger.continueSettling(true)) invalidate(); } }
我們來分析下代碼:
代碼中都有注釋,我就不一一解釋了,先解釋下邊緣滑動的代碼:
首先:
@Override public boolean tryCaptureView(View child, int pointerId) { //當id為edgeview的時候,不允許其滑動 return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview; }
不允許edgeview直接滑動,所以返回的是false,
然後:
/** * 當手指在邊緣拖動的時候回調此方法 * edgeFlags分為left、top、right、bottom */ @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { //當在邊緣滑動的時候 mDragger.captureChildView(mEdgeView, pointerId); }
在當手指觸碰到ViewGroup的邊緣的時候,調用了mDragger.captureChildView方法,
最後:
//一定要加上這句代碼,不然就checkNewEdgeDrag就不會進入判斷了 mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
很少的代碼,我們的view就可以實現邊緣滑動了(不了解邊緣滑動的可以想象下側滑菜單(^__^) 嘻嘻……),那麼我們進入到VDH源碼中看看為什麼可以邊緣滑動?
首先看看onEdgeDragStarted在哪調用的?
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); } }
我們可以看到是在一個叫reportNewEdgeDrags的方法中調用的,那麼reportNewEdgeDrags又是在哪調用的呢?
在VDH中的processTouchEvent方法中我們看到:
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); } else { // Check to see if any pointer is now over a draggable view. 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]; 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; }
當mDragState != STATE_DRAGGING的時候會調用reportNewEdgeDrags方法,在VDH中只有當mDragState ==STATE_DRAGGING的時候才能對view進行拖動,那麼我們看看mDragState 在什麼地方被置成STATE_DRAGGING標記的?
/** * 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中,我們很清晰的看到setDragState(STATE_DRAGGING);這麼一段代碼,當mDragg為STATE_DRAGGING狀態的時候,當進入到processTouchEvent方法的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); }
調了dragTo方法就可以拖動了,dragTo在前一篇博客中有提及,我就不再說明了,到此,邊界拖動的代碼已經解析完畢了。
接下來看看松手自動返回的代碼:
} //手指釋放的時候回調 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { //mAutoBackView手指釋放時可以自動回去 if (releasedChild.getId()==R.id.autobackview) { mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y); invalidate(); } } });
在手指松開的時候會調用onViewReleased方法,然後我們調用了VDH的settleCapturedViewAt方法,我們看看settleCapturedViewAt內部:
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; }
其內部主要調用了forceSettleCapturedViewAt方法,說到底還是調用了mScroller.startScroll(startLeft, startTop, dx, dy, duration);方法,Scroller的用法不懂的自己去腦補啊,還是很重要的一個組件的(^__^) 嘻嘻……既然有Scroller,我們就要重寫View的computeScroll方法,所以我們在DragView中有重寫:
@Override public void computeScroll() { if(mDragger.continueSettling(true)) invalidate(); } }
其實其continueSettling的內部想必知道Scroller的童鞋應該猜得出干了什麼:
boolean keepGoing = mScroller.computeScrollOffset();
到此手指松開回到原來位置的代碼也分析完畢了。
細心的童鞋可以發現,我們做測試用的View都是TextView,因為TextView本身就不具備可點擊性,如果換成本身具有可點擊性的Button,那麼還會有一樣的效果嗎?
我們試試:
當我們換成Button後,我們運行發現,只有邊界移動的view可以移動,其它兩個view不管怎麼滑動都沒效果哦,為什麼呢?
主要是因為,如果子View不消耗事件,那麼整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent,在onTouchEvent的DOWN的時候就確定了captureView。如果消耗事件,那麼就會先走onInterceptTouchEvent方法,判斷是否可以捕獲,而在判斷的過程中會去判斷另外兩個回調的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有這兩個方法返回大於0的值才能正常的捕獲。
所以,如果你用Button測試,或者給TextView添加了clickable = true ,都記得重寫下面這兩個方法:
@Override public int getViewHorizontalDragRange(View child){ return getMeasuredWidth()-child.getMeasuredWidth(); } @Override public int getViewVerticalDragRange(View child){ return getMeasuredHeight()-child.getMeasuredHeight(); }
方法的返回值應當是該childView橫向或者縱向的移動的范圍,當前如果只需要一個方向移動,可以只復寫一個。
這個時候你肯定又會問“為什麼重寫這兩個方法就可以了呢?”
(涉及到事件分發的知識,不懂的童鞋還是得腦補一下哈(^__^) 嘻嘻……)我們來看看原因:
在VDH中的shouldInterceptTouchEvent方法中我們看到這麼一段代碼:
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 = 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); 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; }
代碼有點長,我們看重點,我們看到這麼一段代碼:
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; }
當pastSlop為true的時候,才會去跑:
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { break; }
當跑了tryCaptureViewForDrag的時候就會去走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); }
而這個時候setDragState(STATE_DRAGGING);會給mDragState設置成STATE_DRAGGING,當設置成了STATE_DRAGGING,在shouldInterceptTouchEvent的最後會返回true:
return mDragState == STATE_DRAGGING;
當shouldInterceptTouchEvent返回true以後,我們自定義的ViewGroup中的onInterceptTouchEvent也就返回true了,因此直接攔截了子View的事件,所以接下來才會進ViewGoup的onTouchEvent方法,所以才可以滑動。
有點復雜的感覺額,但是如果很清晰的掌握了事件分發流程,還是很好理解的。
細心的童鞋會發現,我們的View拖動的邊界沒有限制,以至於都拖到ViewGroup外面去了,好吧,我就直接貼代碼了。
左右的邊界:
左邊為getPaddingLeft(),右邊為getWidth() - mDragView.getWidth() - getPaddingRight():
public int clampViewPositionHorizontal(View child, int left, int dx) { final int leftBound = getPaddingLeft(); final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight(); final int newLeft = Math.min(Math.max(left, leftBound), rightBound); return newLeft; }
上下的的邊界: 上邊為getPaddingTop(),下邊為 getHeight() - child.getHeight() - getPaddingBottom():
@Override public int clampViewPositionVertical(View child, int top, int dy) { final int topBound = getPaddingTop(); final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); final int newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; }
好吧!到此,VDH的基本用法就介紹到這裡了,接下來會進入到VDH的實戰部分,有興趣的童鞋可以跟我一起進入VDH實戰部分哦!!!
最後附上demo的git鏈接:
https://github.com/913453448/SwipeBackLayout
我們再用支付寶支付的時候,會從底部彈上來一個對話框,讓我們選擇支付方式等等,今天我們就來慢慢實現這個功能效果圖實現主界面很簡單,就是一個按鈕,點擊後跳到支付詳情的Frag
一、能做什麼你只需要傳url,JavaBean就可以在回調方法裡面得到想要的結果,你會發現你的代碼裡面沒有了子線程、沒有了handle,鏈式的變成使得代碼更加清晰。1.1
Android 7的系統版本新增的很多的新功能,比如說任務處理功能,允許用戶雙擊“最近”按鈕去快速切換到自己上一次使用的應用程序中。同時,“最近”菜單中還有一個“清除全部
Afw流程演示Device OwnerL平台恢復出廠設置連接翻牆Wifi直到出現如下界面點擊”Set up work device”,輸入賬號和激