Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 為何說Android ViewDragHelper是神器 (二)

為何說Android ViewDragHelper是神器 (二)

編輯:關於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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved