Android ViewDragHelper源碼解析

在自定義ViewGroup的過程中,如果涉及到View的拖動滑動,ViewDragHelper的使用應該是少不了的,它提供了一系列用於用戶拖動子View的輔助方法和相關的狀態記錄,像Navigation Drawer的邊緣滑動、QQ5.x的側滑菜單、知乎裏的頁面滑動返回都可以由它實現,所以有必要完全掌握它的使用。

要想完全掌握ViewDragHelper的使用和原理,最好的辦法就是讀懂它的源碼,所以就有了這篇分析,以便在印象模糊之時可以再次快速回顧ViewDragHelper的原理、用法、注意事項等。

基本用法

  1. 在自定義ViewGroup的構造方法裏調用ViewDragHelper的靜態工廠方法create()創建ViewDragHelper實例
  2. 實現ViewDragHelper.Callback
    最重要的幾個方法是tryCaptureView()、clampViewPositionVertical()、clampViewPositionHorizontal()、getViewHorizontalDragRange()、getViewVerticalDragRange()
    • tryCaptureView()裏會傳遞當前觸摸區域下的子View實例作爲參數,如果需要對當前觸摸的子View進行拖拽移動就返回true,否則返回false。
    • clampViewPositionVertical()決定了要拖拽的子View在垂直方向上應該移動到的位置,該方法會傳遞三個參數:要拖拽的子View實例、期望的移動後位置子View的top值、移動的距離。返回值爲子View在最終位置時的top值,一般直接返回第二個參數即可。
    • clampViewPositionHorizontal()與clampViewPositionVertical()同理,只不過是發生在水平方向上,最終返回的是View的left值。
    • getViewVerticalDragRange()要返回一個大於0的數,纔會在在垂直方向上對觸摸到的View進行拖動。
    • getViewHorizontalDragRange()與getViewVerticalDragRange()同理,只不過是發生在水平方向上。
  3. 在onInterceptTouchEvent()方法裏調用並返回ViewDragHelper的shouldInterceptTouchEvent()方法
  4. 在onTouchEvent()方法裏調用ViewDragHelper()的processTouchEvent()方法。ACTION_DOWN事件發生時,如果當前觸摸點下要拖動的子View沒有消費事件,此時應該在onTouchEvent()返回true,否則將收不到後續事件,不會產生拖動。
  5. 上面幾個步驟已經實現了子View拖動的效果,如果還想要實現fling效果(滑動時鬆手後以一定速率繼續自動滑動下去並逐漸停止,類似於扔東西)或者鬆手後自動滑動到指定位置,需要實現自定義ViewGroup的computeScroll()方法,方法實現如下:

    @Override
    public void computeScroll() {
        if (mDragHelper.continueSettling(true)) {
            postInvalidate();
        }
    }
    並在ViewDragHelper.Callback的onViewReleased()方法裏調用settleCapturedViewAt()、flingCapturedView(),或在任意地方調用smoothSlideViewTo()方法。
  6. 如果要實現邊緣拖動的效果,需要調用ViewDragHelper的setEdgeTrackingEnabled()方法,註冊想要監聽的邊緣。然後實現ViewDragHelper.Callback裏的onEdgeDragStarted()方法,在此手動調用captureChildView()傳遞要拖動的子View。

具體的使用Demo請見最後面公佈的幾個案例。

源碼詳解

ViewDragHelper的完整源碼可在GitHubGrepCode上在線查看。在最後的總結部分,我畫了簡單的流程圖,梳理了整個觸摸事件傳遞過重中相關方法的調用,有需要的就先去總結部分看看。

預備知識

  1. 瞭解View的座標系統,Android View座標getLeft, getRight, getTop, getBottom
  2. 瞭解MotionEvent中關於多點觸控的機制,android觸控,先了解MotionEvent(一)
  3. 瞭解Scroller類原理,Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解
  4. 瞭解Touch事件的分發機制,Andriod 從源碼的角度詳解View,ViewGroup的Touch事件的分發機制

ViewDragHelper實例的創建

ViewDragHelper重載了兩個create()靜態方法,先看兩個參數的create()方法:

/**
 * Factory method to create a new ViewDragHelper.
 *
 * @param forParent Parent view to monitor
 * @param cb Callback to provide information and receive events
 * @return a new ViewDragHelper instance
 */
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
    return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

create()的兩個參數很好理解,第一個是我們自定義的ViewGroup,第二個是控制子View拖拽需要的回調對象。create()直接調用了ViewDragHelper構造方法,我們再來看看這個構造方法。

/**
 * Apps should use ViewDragHelper.create() to get a new instance.
 * This will allow VDH to use internal compatibility implementations for different
 * platform versions.
 *
 * @param context Context to initialize config-dependent params from
 * @param forParent Parent view to monitor
 */
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
    if (forParent == null) {
        throw new IllegalArgumentException("Parent view may not be null");
    }
    if (cb == null) {
        throw new IllegalArgumentException("Callback may not be null");
    }

    mParentView = forParent;
    mCallback = cb;

    final ViewConfiguration vc = ViewConfiguration.get(context);
    final float density = context.getResources().getDisplayMetrics().density;
    mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

    mTouchSlop = vc.getScaledTouchSlop();
    mMaxVelocity = vc.getScaledMaximumFlingVelocity();
    mMinVelocity = vc.getScaledMinimumFlingVelocity();
    mScroller = ScrollerCompat.create(context, sInterpolator);
}

這個構造函數是私有的,也是僅有的構造函數,所以外部只能通過create()工廠方法來創建ViewDragHelper實例了。這裏要求了我們傳遞的自定義ViewGroup和回調對象不能爲空,否則會直接拋出異常中斷程序。在這裏也初始化了一些觸摸滑動需要的參考值和輔助類。

  • mParentView和mCallback分別保存傳遞過來的對應參數
  • ViewConfiguration類裏定義了View相關的一系列時間、大小、距離等常量
  • mEdgeSize表示邊緣觸摸的範圍。例如mEdgeSize爲20dp並且用戶註冊監聽了左側邊緣觸摸時,觸摸點的x座標小於mParentView.getLeft() + mEdgeSize時(即觸摸點在容器左邊界往右20dp內)就算做是左側的邊緣觸摸,詳見ViewDragHelper的getEdgesTouched()方法。
  • mTouchSlop是一個很小的距離值,只有在前後兩次觸摸點的距離超過mTouchSlop的值時,我們才把這兩次觸摸算作是“滑動”,我們只在此時進行滑動處理,否則任何微小的距離的變化我們都要處理的話會顯得太頻繁,如果處理過程又比較複雜耗時就會使界面產生卡頓。
  • mMaxVelocity、mMinVelocity是fling時的最大、最小速率,單位是像素每秒。
  • mScroller是View滾動的輔助類,該類的詳細解析參見下面幾篇文章

再看三個參數的create()方法:

/**
 * Factory method to create a new ViewDragHelper.
 *
 * @param forParent Parent view to monitor
 * @param sensitivity Multiplier for how sensitive the helper should be about detecting
 *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
 * @param cb Callback to provide information and receive events
 * @return a new ViewDragHelper instance
 */
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;
}

第二個參數sensitivity是用來調節mTouchSlop的值。sensitivity越大,mTouchSlop越小,對滑動的檢測就越敏感。例如sensitivity爲1時,前後觸摸點距離超過20dp才進行滑動處理,現在sensitivity爲2的話,前後觸摸點距離超過10dp就進行處理了。

對Touch事件的處理

當mParentView(自定義ViewGroup)被觸摸時,首先會調用mParentView的onInterceptTouchEvent(MotionEvent ev),接着就調用shouldInterceptTouchEvent(MotionEvent ev) ,所以先來看看這個方法的ACTION_DOWN部分:

/**
 * Check if this event as provided to the parent view's onInterceptTouchEvent should
 * cause the parent to intercept the touch event stream.
 *
 * @param ev MotionEvent provided to onInterceptTouchEvent
 * @return true if the parent view should return true from onInterceptTouchEvent
 */
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    final int actionIndex = MotionEventCompat.getActionIndex(ev);

    if (action == MotionEvent.ACTION_DOWN) {
        // Reset things for a new event stream, just in case we didn't get
        // the whole previous stream.
        cancel();
    }

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int pointerId = MotionEventCompat.getPointerId(ev, 0);
            saveInitialMotion(x, y, pointerId);

            final View toCapture = findTopChildUnder((int) x, (int) y);

            // Catch a settling view if possible.
            if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
                tryCaptureViewForDrag(toCapture, pointerId);
            }

            final int edgesTouched = mInitialEdgesTouched[pointerId];
            if ((edgesTouched & mTrackingEdges) != 0) {
                mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
            }
            break;
        }

        // 其他case暫且省略
    }

    return mDragState == STATE_DRAGGING;
}

看9~21行,首先是關於多點觸控(MotionEvent的actionIndex、ACTION_POINTER_DOWN 等概念),不明白的請參閱android觸控,先了解MotionEvent(一)

mVelocityTracker記錄下觸摸的各個點信息,稍後可以用來計算本次滑動的速率,每次發生ACTION_DOWN事件都會調用cancel(),而在cancel()方法裏mVelocityTracker又被清空了,所以mVelocityTracker 記錄下的是本次ACTION_DOWN事件直至ACTION_UP事件發生後(下次ACTION_DOWN事件發生前)的所有觸摸點的信息。

再來看24~42行case MotionEvent.ACTION_DOWN部分,先是調用saveInitialMotion(x, y, pointerId)保存手勢的初始信息,即ACTION_DOWN發生時的觸摸點座標(x、y)、觸摸手指編號(pointerId),如果觸摸到了mParentView的邊緣還會記錄觸摸的是哪個邊緣。接着調用findTopChildUnder((int) x, (int) y);來獲取當前觸摸點下最頂層的子View,看findTopChildUnder 的源碼:

/**
 * Find the topmost child under the given point within the parent view's coordinate system.
 * The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
 *
 * @param x X position to test in the parent's coordinate system
 * @param y Y position to test in the parent's coordinate system
 * @return The topmost child view under (x, y) or null if none found.
 */
public View findTopChildUnder(int x, int y) {
    final int childCount = mParentView.getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        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;
}

代碼很簡單,註釋裏也說明的很清楚了。如果在同一個位置有兩個子View重疊,想要讓下層的子View被選中,那麼就要實現Callback裏的getOrderedChildIndex(int index)方法來改變查找子View的順序;例如topView(上層View)的index是4,bottomView(下層View)的index是3,按照正常的遍歷查找方式(getOrderedChildIndex()默認直接返回index),會選擇到topView,要想讓bottomView被選中就得這麼寫:

public int getOrderedChildIndex(int index) {
    int indexTop = mParentView.indexOfChild(topView);
    int indexBottom = mParentView.indexOfChild(bottomView);
    if (index == indexTop) {
        return indexBottom;
    }
    return index;
}

32~35行,這裏還看到了一個mDragState成員變量,它共有三種取值:

  1. STATE_IDLE:所有的View處於靜止空閒狀態
  2. STATE_DRAGGING:某個View正在被用戶拖動(用戶正在與設備交互)
  3. STATE_SETTLING:某個View正在安置狀態中(用戶並沒有交互操作),就是自動滾動的過程中
    mCapturedView默認爲null,所以一開始不會執行這裏的代碼,mDragState處於STATE_SETTLING狀態時纔會執行tryCaptureViewForDrag(),執行的情況到後面再分析,這裏先跳過。

37~40行調用了Callback.onEdgeTouched向外部通知mParentView的某些邊緣被觸摸到了,mInitialEdgesTouched是在剛纔調用過的saveInitialMotion方法裏進行賦值的。

ACTION_DOWN 部分處理完了,跳過switch語句塊,剩下的代碼就只有return mDragState == STATE_DRAGGING;。在ACTION_DOWN部分沒有對mDragState進行賦值,其默認值爲STATE_IDLE,所以此處返回false。

那麼返回false後接下來應該是會調用哪個方法呢,根據Andriod 從源碼的角度詳解View,ViewGroup的Touch事件的分發機制裏的解析,接下來會在mParentView的所有子View中尋找響應這個Touch事件的View(會調用每個子View的dispatchTouchEvent()方法,dispatchTouchEvent裏一般又會調用onTouchEvent());

  • 如果沒有子View消費這次事件(子View的dispatchTouchEvent()返回都是false),會調用mParentView的super.dispatchTouchEvent(ev),即View中的dispatchTouchEvent(ev),然後調用mParentView的onTouchEvent()方法,再調用ViewDragHelper的processTouchEvent(MotionEvent ev)方法。此時(ACTION_DOWN事件發生時)mParentView的onTouchEvent()要返回true,onTouchEvent()才能繼續接受到接下來的ACTION_MOVE、ACTION_UP等事件,否則無法完成拖動(除了ACTION_DOWN外的其他事件發生時返回true或false都不會影響接下來的事件接受),因爲拖動的相關代碼是寫在processTouchEvent()裏的ACTION_MOVE部分的。要注意的是返回true後mParentView的onInterceptTouchEvent()就不會收到後續的ACTION_MOVE、ACTION_UP等事件了。

  • 如果有子View消費了本次ACTION_DOWN事件,mParentView的onTouchEvent()就收不到ACTION_DOWN事件了,也就是ViewDragHelper的processTouchEvent(MotionEvent ev)收不到ACTION_DOWN事件了。不過只要該View沒有調用過requestDisallowInterceptTouchEvent(true),mParentView的onInterceptTouchEvent()的ACTION_MOVE部分還是會執行的,如果在此時返回了true攔截了ACTION_MOVE事件,processTouchEvent()裏的ACTION_MOVE部分也就會正常執行,拖動也就沒問題了。onInterceptTouchEvent()的ACTION_MOVE部分具體做了怎樣的處理,稍後再來解析。

接下來對這兩種情況逐一解析。

假設沒有子View消費這次事件,根據剛纔的分析最終就會調用processTouchEvent(MotionEvent ev)的ACTION_DOWN部分:

/**
 * Process a touch event received by the parent view. This method will dispatch callback events
 * as needed before returning. The parent view's onTouchEvent implementation should call this.
 *
 * @param ev The touch event received by the parent view
 */
public void processTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    final int actionIndex = MotionEventCompat.getActionIndex(ev);

    if (action == MotionEvent.ACTION_DOWN) {
        // Reset things for a new event stream, just in case we didn't get
        // the whole previous stream.
        cancel();
    }

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int pointerId = MotionEventCompat.getPointerId(ev, 0);
            final View toCapture = findTopChildUnder((int) x, (int) y);

            saveInitialMotion(x, y, pointerId);

            // Since the parent is already directly processing this touch event,
            // there is no reason to delay for a slop before dragging.
            // Start immediately if possible.
            tryCaptureViewForDrag(toCapture, pointerId);

            final int edgesTouched = mInitialEdgesTouched[pointerId];
            if ((edgesTouched & mTrackingEdges) != 0) {
                mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
            }
            break;
        }
        // 其他case暫且省略
    }
}

這段代碼跟shouldInterceptTouchEvent()裏ACTION_DOWN那部分基本一致,唯一區別就是這裏沒有約束條件直接調用了tryCaptureViewForDrag()方法,現在來看看這個方法:

/**
 * Attempt to capture the view with the given pointer ID. The callback will be involved.
 * This will put us into the "dragging" state. If we've already captured this view with
 * this pointer this method will immediately return true without consulting the callback.
 *
 * @param toCapture View to capture
 * @param pointerId Pointer to capture with
 * @return true if capture was successful
 */
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;
}

這裏調用了Callback的tryCaptureView(View child, int pointerId)方法,把當前觸摸到的View和觸摸手指編號傳遞了過去,在tryCaptureView()中決定是否需要拖動當前觸摸到的View,如果要拖動當前觸摸到的View就在tryCaptureView()中返回true,讓ViewDragHelper把當前觸摸的View捕獲下來,接着就調用了captureChildView(toCapture, pointerId)方法:

/**
 * 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(toCapture, pointerId)中將要拖動的View和觸摸的手指編號記錄下來,並調用Callback的onViewCaptured(childView, activePointerId)通知外部有子View被捕獲到了,再調用setDragState()設置當前的狀態爲STATE_DRAGGING,看setDragState()源碼:

void setDragState(int state) {
    if (mDragState != state) {
        mDragState = state;
        mCallback.onViewDragStateChanged(state);
        if (mDragState == STATE_IDLE) {
            mCapturedView = null;
        }
    }
}

狀態改變後會調用Callback的onViewDragStateChanged()通知狀態的變化。

假設ACTION_DOWN發生後在mParentView的onTouchEvent()返回了true,接下來就會執行ACTION_MOVE部分:

public void processTouchEvent(MotionEvent ev) {

    switch (action) {
        // 省略其他case...
        
        case MotionEvent.ACTION_MOVE: {
            if (mDragState == STATE_DRAGGING) {
                final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                final float x = MotionEventCompat.getX(ev, index);
                final float y = MotionEventCompat.getY(ev, 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 = MotionEventCompat.getPointerCount(ev);
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = MotionEventCompat.getPointerId(ev, i);
                    final float x = MotionEventCompat.getX(ev, i);
                    final float y = MotionEventCompat.getY(ev, 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;
        }

        // 省略其他case...
    }
}

要注意的是,如果一直沒鬆手,這部分代碼會一直調用。這裏先判斷mDragState是否爲STATE_DRAGGING,而唯一調用setDragState(STATE_DRAGGING)的地方就是tryCaptureViewForDrag()了,剛纔在ACTION_DOWN裏調用過tryCaptureViewForDrag(),現在又要分兩種情況。
如果剛纔在ACTION_DOWN裏捕獲到要拖動的View,那麼就執行if部分的代碼,這個稍後解析,先考慮沒有捕獲到的情況。沒有捕獲到的話,mDragState依然是STATE_IDLE,然後會執行else部分的代碼。這裏主要就是檢查有沒有哪個手指觸摸到了要拖動的View上,觸摸上了就嘗試捕獲它,然後讓mDragState變爲STATE_DRAGGING,之後就會執行if部分的代碼了。這裏還有兩個方法涉及到了Callback裏的方法,需要來解析一下,分別是reportNewEdgeDrags()和checkTouchSlop(),先看reportNewEdgeDrags():

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);
    }
}

這裏對四個邊緣都做了一次檢查,檢查是否在某些邊緣產生拖動了,如果有拖動,就將有拖動的邊緣記錄在mEdgeDragsInProgress中,再調用Callback的onEdgeDragStarted(int edgeFlags, int pointerId)通知某個邊緣開始產生拖動了。雖然reportNewEdgeDrags()會被調用很多次(因爲processTouchEvent()的ACTION_MOVE部分會執行很多次),但mCallback.onEdgeDragStarted(dragsStarted, pointerId)只會調用一次,具體的要看checkNewEdgeDrag()這個方法:

private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
    final float absDelta = Math.abs(delta);
    final float absODelta = Math.abs(odelta);

    if ((mInitialEdgesTouched[pointerId] & edge) != edge  || (mTrackingEdges & edge) == 0 ||
            (mEdgeDragsLocked[pointerId] & edge) == edge ||
            (mEdgeDragsInProgress[pointerId] & edge) == edge ||
            (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
        return false;
    }
    if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
        mEdgeDragsLocked[pointerId] |= edge;
        return false;
    }
    return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
  • checkNewEdgeDrag()返回true表示在指定的edge(邊緣)開始產生拖動了。
  • 方法的兩個參數delta和odelta需要解釋一下,odelta裏的o應該代表opposite,這是什麼意思呢,以reportNewEdgeDrags()裏調用checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)爲例,我們要監測左邊緣的觸摸情況,所以主要監測的是x軸方向上的變化,這裏delta爲dx,odelta爲dy,也就是說delta是指我們主要監測的方向上的變化,odelta是另外一個方向上的變化,後面要判斷假另外一個方向上的變化是否要遠大於主要方向上的變化,所以需要另外一個方向上的距離變化的值。
  • mInitialEdgesTouched是在ACTION_DOWN部分的saveInitialMotion()裏生成的,ACTION_DOWN發生時觸摸到的邊緣會被記錄在mInitialEdgesTouched中。如果ACTION_DOWN發生時沒有觸摸到邊緣,或者觸摸到的邊緣不是指定的edge,就直接返回false了。
  • mTrackingEdges是由setEdgeTrackingEnabled(int edgeFlags)設置的,當我們想要追蹤監聽邊緣觸摸時才需要調用setEdgeTrackingEnabled(int edgeFlags),如果我們沒有調用過它,這裏就直接返回false了。
  • mEdgeDragsLocked它在這個方法裏被引用了多次,它在整個ViewDragHelper裏唯一被賦值的地方就是這裏的第12行,所以默認值是0,第6行mEdgeDragsLocked[pointerId] & edge) == edge執行的結果是false。我們再跳到11到14行看看,absDelta < absODelta * 0.5f的意思是檢查在次要方向上移動的距離是否遠超過主要方向上移動的距離,如果是再調用Callback的onEdgeLock(edge)檢查是否需要鎖定某個邊緣,如果鎖定了某個邊緣,那個邊緣就算觸摸到了也不會被記錄在mEdgeDragsInProgress裏了,也不會收到Callback的onEdgeDragStarted()通知了。並且將鎖定的邊緣記錄在mEdgeDragsLocked變量裏,再次調用本方法時就會在第6行進行判斷了,第6行裏如果檢測到給定的edge被鎖定,就直接返回false了。
  • 回到第7行的(mEdgeDragsInProgress[pointerId] & edge) == edge,mEdgeDragsInProgress是保存已發生過拖動事件的邊緣的,如果給定的edge已經保存過了,那就沒必要再檢測其他東西了,直接返回false了。
  • 第8行(absDelta <= mTouchSlop && absODelta <= mTouchSlop)很簡單了,就是檢查本次移動的距離是不是太小了,太小就不處理了。
  • 最後一句返回的時候再次檢查給定的edge有沒有記錄過,確保了每個邊緣只會調用一次reportNewEdgeDrags的mCallback.onEdgeDragStarted(dragsStarted, pointerId)

再來看checkTouchSlop()方法:

/**
 * Check if we've crossed a reasonable touch slop for the given child view.
 * If the child cannot be dragged along the horizontal or vertical axis, motion
 * along that axis will not count toward the slop check.
 *
 * @param child Child to check
 * @param dx Motion since initial position along X axis
 * @param dy Motion since initial position along Y axis
 * @return true if the touch slop has been crossed
 */
private boolean checkTouchSlop(View child, float dx, float dy) {
    if (child == null) {
        return false;
    }
    final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
    final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

    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;
    }
    return false;
}

這個方法主要就是檢查手指移動的距離有沒有超過觸發處理移動事件的最短距離(mTouchSlop)了,注意dx和dy指的是當前觸摸點到ACTION_DOWN觸摸到的點的距離。這裏先檢查Callback的getViewHorizontalDragRange(child)和getViewVerticalDragRange(child)是否大於0,如果想讓某個View在某個方向上滑動,就要在那個方向對應的方法裏返回大於0的數。否則在processTouchEvent()的ACTION_MOVE部分就不會調用tryCaptureViewForDrag()來捕獲當前觸摸到的View了,拖動也就沒辦法進行了。

回到processTouchEvent()的ACTION_MOVE部分,假設現在我們的手指已經滑動到可以被捕獲到的View上了,也都正常的實現了Callback中的相關方法,讓tryCaptureViewForDrag()正常的捕獲到觸摸到的View了,下一次ACTION_MOVE時就執行if部分的代碼了,也就是開始不停的調用dragTo()對mCaptureView進行真正拖動了,看dragTo()方法:

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);
        mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
    }
    if (dy != 0) {
        clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
        mCapturedView.offsetTopAndBottom(clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
        final int clampedDx = clampedX - oldLeft;
        final int clampedDy = clampedY - oldTop;
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                clampedDx, clampedDy);
    }
}

參數dx和dy是前後兩次ACTION_MOVE移動的距離,left和top分別爲mCapturedView.getLeft() + dx, mCapturedView.getTop() + dy,也就是期望的移動後的座標,對View的getLeft()等方法不理解的請參閱Android View座標getLeft, getRight, getTop, getBottom

這裏通過調用offsetLeftAndRight()和offsetTopAndBottom()來完成對mCapturedView移動,這兩個是View中定義的方法,看它們的源碼就知道內部是通過改變View的mLeft、mRight、mTop、mBottom,即改變View在父容器中的座標位置,達到移動View的效果,所以如果調用mCapturedView的layout(int l, int t, int r, int b)方法也可以實現移動View的效果。

具體要移動到哪裏,由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()來決定的,如果不想在水平方向上移動,在clampViewPositionHorizontal(View child, int left, int dx)裏直接返回child.getLeft()就可以了,這樣clampedX - oldLeft的值爲0,這裏調用mCapturedView.offsetLeftAndRight(clampedX - oldLeft)就不會起作用了。垂直方向上同理。

最後會調用Callback的onViewPositionChanged(mCapturedView, clampedX, clampedY,clampedDx, clampedDy)通知捕獲到的View位置改變了,並把最終的座標(clampedX、clampedY)和最終的移動距離(clampedDx、 clampedDy)傳遞過去。

ACTION_MOVE部分就算告一段落了,接下來應該是用戶鬆手觸發ACTION_UP,或者是達到某個條件導致後續的ACTION_MOVE被mParentView的上層View給攔截了而收到ACTION_CANCEL,一起來看這兩個部分:

public void processTouchEvent(MotionEvent ev) {
    // 省略

    switch (action) {
        // 省略其他case

        case MotionEvent.ACTION_UP: {
            if (mDragState == STATE_DRAGGING) {
                releaseViewForPointerUp();
            }
            cancel();
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            if (mDragState == STATE_DRAGGING) {
                dispatchViewReleased(0, 0);
            }
            cancel();
            break;
        }
    }
}

這兩個部分都是重置所有的狀態記錄,並通知View被放開了,再看下releaseViewForPointerUp()和dispatchViewReleased()的源碼:

private void releaseViewForPointerUp() {
    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
    final float xvel = clampMag(
            VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
            mMinVelocity, mMaxVelocity);
    final float yvel = clampMag(
            VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
            mMinVelocity, mMaxVelocity);
    dispatchViewReleased(xvel, yvel);
}

releaseViewForPointerUp()裏也調用了dispatchViewReleased(),只不過傳遞了速率給它,這個速率就是由processTouchEvent()的mVelocityTracker追蹤算出來的。再看dispatchViewReleased():

/**
 * Like all callback events this must happen on the UI thread, but release
 * involves some extra semantics. During a release (mReleaseInProgress)
 * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
 * or {@link #flingCapturedView(int, int, int, int)}.
 */
private void dispatchViewReleased(float xvel, float yvel) {
    mReleaseInProgress = true;
    mCallback.onViewReleased(mCapturedView, xvel, yvel);
    mReleaseInProgress = false;

    if (mDragState == STATE_DRAGGING) {
        // onViewReleased didn't call a method that would have changed this. Go idle.
        setDragState(STATE_IDLE);
    }
}

這裏調用Callback的onViewReleased(mCapturedView, xvel, yvel)通知外部捕獲到的View被釋放了,而在onViewReleased()前後有個mReleaseInProgress值得注意,註釋裏說唯一可以調用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()裏了。

首先這兩個方法是幹什麼的呢。在現實生活中保齡球的打法是,先做扔的動作讓球的速度達到最大,然後突然鬆手,由於慣性,保齡球就以最後鬆手前的速度爲初速度拋出去了,直至自然停止,或者撞到邊界停止,這種效果叫fling。
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)就是對捕獲到的View做出這種fling的效果,用戶在屏幕上滑動鬆手之前也會有一個滑動的速率。fling也引出來的一個問題,就是不知道View最終會滾動到哪個位置,最後位置是在啓動fling時根據最後滑動的速度來計算的(flingCapturedView的四個參數int minLeft, int minTop, int maxLeft, int maxTop可以限定最終位置的範圍),假如想要讓View滾動到指定位置應該怎麼辦,答案就是使用settleCapturedViewAt(int finalLeft, int finalTop)。

爲什麼唯一可以調用settleCapturedViewAt()和flingCapturedView()的地方是Callback的onViewReleased()呢?看看它們的源碼

/**
 * 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.
 *
 * @param finalLeft Settled left edge position for the captured view
 * @param finalTop Settled top edge position for the captured view
 * @return true if animation should continue through {@link #continueSettling(boolean)} calls
 */
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
    if (!mReleaseInProgress) {
        throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
                "Callback#onViewReleased");
    }

    return forceSettleCapturedViewAt(finalLeft, finalTop,
            (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
            (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}

/**
 * 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.
 *
 * @param minLeft Minimum X position for the view's left edge
 * @param minTop Minimum Y position for the view's top edge
 * @param maxLeft Maximum X position for the view's left edge
 * @param maxTop Maximum Y position for the view's top edge
 */
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
    if (!mReleaseInProgress) {
        throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
                "Callback#onViewReleased");
    }

    mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
            (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
            (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
            minLeft, maxLeft, minTop, maxTop);

    setDragState(STATE_SETTLING);
}

這兩個方法裏一開始都會判斷mReleaseInProgress爲false,如果爲false就會拋一個IllegalStateException異常,而mReleaseInProgress唯一爲true的時候就是在dispatchViewReleased()裏調用onViewReleased()的時候。

Scroller的用法請參閱Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解 ,或者自行解讀Scroller源碼,代碼量不多。

ViewDragHelper還有一個移動View的方法是smoothSlideViewTo(View child, 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.
 *
 * <p>This operation does not count as a capture event, though {@link #getCapturedView()}
 * will still report the sliding view while the slide is in progress.</p>
 *
 * @param child Child view to capture and animate
 * @param finalLeft Final left position of child
 * @param finalTop Final top position of child
 * @return true if animation should continue through {@link #continueSettling(boolean)} calls
 */
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
    mCapturedView = child;
    mActivePointerId = INVALID_POINTER;

    boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
    if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
        // If we're in an IDLE state to begin with and aren't moving anywhere, we
        // end up having a non-null capturedView with an IDLE dragState
        mCapturedView = null;
    }

    return continueSliding;
}

可以看到它不受mReleaseInProgress的限制,所以可以在任何地方調用,效果和settleCapturedViewAt()類似,因爲它們最終都調用了forceSettleCapturedViewAt()來啓動自動滾動,區別在於settleCapturedViewAt()會以最後鬆手前的滑動速率爲初速度將View滾動到最終位置,而smoothSlideViewTo()滾動的初速度是0。forceSettleCapturedViewAt()裏有地方調用了Callback裏的方法,所以再來看看這個方法:

/**
 * Settle the captured view at the given (left, top) position.
 *
 * @param finalLeft Target left position for the captured view
 * @param finalTop Target top position for the captured view
 * @param xvel Horizontal velocity
 * @param yvel Vertical velocity
 * @return true if animation should continue through {@link #continueSettling(boolean)} calls
 */
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;
}

可以看到自動滑動是靠Scroll類完成,在這裏生成了調用mScroller.startScroll()需要的參數。再來看看計算滾動時間的方法computeSettleDuration():

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
    xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
    yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
    final int absDx = Math.abs(dx);
    final int absDy = Math.abs(dy);
    final int absXVel = Math.abs(xvel);
    final int absYVel = Math.abs(yvel);
    final int addedVel = absXVel + absYVel;
    final int addedDistance = absDx + absDy;

    final float xweight = xvel != 0 ? (float) absXVel / addedVel :
            (float) absDx / addedDistance;
    final float yweight = yvel != 0 ? (float) absYVel / addedVel :
            (float) absDy / addedDistance;

    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));

    return (int) (xduration * xweight + yduration * yweight);
}

clampMag()方法確保參數中給定的速率在正常範圍之內。最終的滾動時間還要經過computeAxisDuration()算出來,通過它的參數可以看到最終的滾動時間是由dx、xvel、mCallback.getViewHorizontalDragRange()共同影響的。看computeAxisDuration():

private int computeAxisDuration(int delta, int velocity, int motionRange) {
    if (delta == 0) {
        return 0;
    }

    final int width = mParentView.getWidth();
    final int halfWidth = width / 2;
    final float distanceRatio = Math.min(f, (float) Math.abs(delta) / width);
    final float distance = halfWidth + halfWidth *
            distanceInfluenceForSnapDuration(distanceRatio);

    int duration;
    velocity = Math.abs(velocity);
    if (velocity > 0) {
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        final float range = (float) Math.abs(delta) / motionRange;
        duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
    }
    return Math.min(duration, MAX_SETTLE_DURATION);
}

6~10行沒看明白,直接看14~19行,如果給定的速率velocity不爲0,就通過距離除以速率來算出時間;如果velocity爲0,就通過要滑動的距離(delta)除以總的移動範圍(motionRange,就是Callback裏getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)來算出時間。最後還會對計算出的時間做過濾,最終時間反正是不會超過MAX_SETTLE_DURATION的,源碼裏的取值是600毫秒,所以不用擔心在Callback裏getViewHorizontalDragRange()、getViewVerticalDragRange()返回錯誤的數而導致自動滾動時間過長了。

在調用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()時,還需要實現mParentView的computeScroll():

@Override
public void computeScroll() {
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

這屬於Scroll類用法的範疇,不明白的請參閱Android中滑屏實現----手把手教你如何實現觸摸滑屏以及Scroller類詳解 的“知識點二: computeScroll()方法介紹”。

至此,整個觸摸流程和ViewDragHelper的重要的方法都過了一遍。之前在討論shouldInterceptTouchEvent()的ACTION_DOWN部分執行完後應該再執行什麼的時候,還有一種情況沒有展開詳解,就是有子View消費了本次ACTION_DOWN事件的情況,現在來看看這種情況。

假設現在shouldInterceptTouchEvent()的ACTION_DOWN部分執行完了,也有子View消費了這次的ACTION_DOWN事件,那麼接下來就會調用mParentView的onInterceptTouchEvent()的ACTION_MOVE部分,不明白爲什麼的請參閱Andriod 從源碼的角度詳解View,ViewGroup的Touch事件的分發機制,接着調用ViewDragHelper的shouldInterceptTouchEvent()的ACTION_MOVE部分:

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    // 省略...
    
    switch (action) {
        // 省略其他case...

        case MotionEvent.ACTION_MOVE: {
            // First to cross a touch slop over a draggable view wins. Also report edge drags.
            final int pointerCount = MotionEventCompat.getPointerCount(ev);
            for (int i = 0; i < pointerCount; i++) {
                final int pointerId = MotionEventCompat.getPointerId(ev, i);
                final float x = MotionEventCompat.getX(ev, i);
                final float y = MotionEventCompat.getY(ev, 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;
        }

        // 省略其他case...
    }

    return mDragState == STATE_DRAGGING;
}

如果有多個手指觸摸到屏幕上了,對每個觸摸點都檢查一下,看當前觸摸的地方是否需要捕獲某個View。這裏先用findTopChildUnder(int x, int y)尋找觸摸點處的子View,再用checkTouchSlop(View child, float dx, float dy)檢查當前觸摸點到ACTION_DOWN觸摸點的距離是否達到了mTouchSlop,達到了纔會去捕獲View。
接着看19~41行if (pastSlop){...}部分,這裏檢查在某個方向上是否可以進行拖動,檢查過程涉及到getView[Horizontal|Vertical]DragRange和clampViewPosition[Horizontal|Vertical]四個方法。如果getView[Horizontal|Vertical]DragRange返回都是0,就會認作是不會產生拖動。clampViewPosition[Horizontal|Vertical]返回的是被捕獲的View的最終位置,如果和原來的位置相同,說明我們沒有期望它移動,也就會認作是不會產生拖動的。不會產生拖動就會在39行直接break,不會執行後續的代碼,而後續代碼裏有調用tryCaptureViewForDrag(),所以不會產生拖動也就不會去捕獲View了,拖動也不會進行了。
如果檢查到可以在某個方向上進行拖動,就會調用後面的tryCaptureViewForDrag()捕獲子View,如果捕獲成功,mDragState就會變成STATE_DRAGGING,shouldInterceptTouchEvent()返回true,mParentView的onInterceptTouchEvent()返回true,後續的移動事件就會在mParentView的onTouchEvent()執行了,最後執行的就是mParentView的processTouchEvent()的ACTION_MOVE部分,拖動正常進行。

回頭再看之前在shouldInterceptTouchEvent()的ACTION_DOWN部分留下的坑:

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    // 省略其他部分...
    
    switch (action) {
        // 省略其他case...

        case MotionEvent.ACTION_DOWN: {
            // 省略其他部分...
            
            // Catch a settling view if possible.
            if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
                tryCaptureViewForDrag(toCapture, pointerId);
            }
            
            // 省略其他部分...
        }

        // 省略其他case...
    }

    return mDragState == STATE_DRAGGING;
}

現在應該明白這部分代碼會在什麼情況下執行了。當我們鬆手後捕獲的View處於自動滾動的過程中時,用戶再次觸摸屏幕,就會執行這裏的tryCaptureViewForDrag()嘗試捕獲View,如果捕獲成功,mDragState就變爲STATE_DRAGGING了,shouldInterceptTouchEvent()就返回true了,然後就是mParentView的onInterceptTouchEvent()返回true,接着執行mParentView的onTouchEvent(),再執行processTouchEvent()的ACTION_DOWN部分。此時(ACTION_DOWN事件發生時)mParentView的onTouchEvent()要返回true,onTouchEvent()才能繼續接受到接下來的ACTION_MOVE、ACTION_UP等事件,否則無法完成拖動。

至此整個事件傳遞流程和ViewDragHelper的重要方法基本都解析完了,shouldInterceptTouchEvent()和processTouchEvent()的ACTION_POINTER_DOWN、ACTION_POINTER_UP部分就留給讀者自己解析了。

總結

對於整個觸摸事件傳遞過程,我畫了簡要的流程圖,方便日後快速回顧。

多點觸摸情況我就沒研究了,在這裏忽略~

三個開啓自動滾動的方法:

  • settleCapturedViewAt(int finalLeft, int finalTop)
    以鬆手前的滑動速度爲初速動,讓捕獲到的View自動滾動到指定位置。只能在Callback的onViewReleased()中調用。
  • flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
    以鬆手前的滑動速度爲初速動,讓捕獲到的View在指定範圍內fling。只能在Callback的onViewReleased()中調用。
  • smoothSlideViewTo(View child, int finalLeft, int finalTop)
    指定某個View自動滾動到指定的位置,初速度爲0,可在任何地方調用。

Callback的各個方法總結:

  • void onViewDragStateChanged(int state)
    拖動狀態改變時會調用此方法,狀態state有STATE_IDLE、STATE_DRAGGING、STATE_SETTLING三種取值。
    它在setDragState()裏被調用,而setDragState()被調用的地方有
    • tryCaptureViewForDrag()成功捕獲到子View時
      • shouldInterceptTouchEvent()的ACTION_DOWN部分捕獲到
      • shouldInterceptTouchEvent()的ACTION_MOVE部分捕獲到
      • processTouchEvent()的ACTION_MOVE部分捕獲到
    • 調用settleCapturedViewAt()、smoothSlideViewTo()、flingCapturedView()時
    • 拖動View鬆手時(processTouchEvent()的ACTION_UP、ACTION_CANCEL)
    • 自動滾動停止時(continueSettling()裏檢測到滾動結束時)
    • 外部調用abort()時
  • void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
    正在被拖動的View或者自動滾動的View的位置改變時會調用此方法。
    • 在dragTo()裏被調用(正在被拖動時)
    • 在continueSettling()裏被調用(自動滾動時)
    • 外部調用abort()時被調用
  • void onViewCaptured(View capturedChild, int activePointerId)
    tryCaptureViewForDrag()成功捕獲到子View時會調用此方法。
    • 在shouldInterceptTouchEvent()的ACTION_DOWN裏成功捕獲
    • 在shouldInterceptTouchEvent()的ACTION_MOVE裏成功捕獲
    • 在processTouchEvent()的ACTION_MOVE裏成功捕獲
    • 手動調用captureChildView()
  • void onViewReleased(View releasedChild, float xvel, float yvel)
    拖動View鬆手時(processTouchEvent()的ACTION_UP)或被父View攔截事件時(processTouchEvent()的ACTION_CANCEL)會調用此方法。
  • void onEdgeTouched(int edgeFlags, int pointerId)
    ACTION_DOWN或ACTION_POINTER_DOWN事件發生時如果觸摸到監聽的邊緣會調用此方法。edgeFlags的取值爲EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM的組合。
  • boolean onEdgeLock(int edgeFlags)
    返回true表示鎖定edgeFlags對應的邊緣,鎖定後的那些邊緣就不會在onEdgeDragStarted()被通知了,默認返回false不鎖定給定的邊緣,edgeFlags的取值爲EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM其中之一。
  • void onEdgeDragStarted(int edgeFlags, int pointerId)
    ACTION_MOVE事件發生時,檢測到開始在某些邊緣有拖動的手勢,也沒有鎖定邊緣,會調用此方法。edgeFlags取值爲EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM的組合。可在此手動調用captureChildView()觸發從邊緣拖動子View的效果。
  • int getOrderedChildIndex(int index)
    在尋找當前觸摸點下的子View時會調用此方法,尋找到的View會提供給tryCaptureViewForDrag()來嘗試捕獲。如果需要改變子View的遍歷查詢順序可改寫此方法,例如讓下層的View優先於上層的View被選中。
  • int getViewHorizontalDragRange(View child)、int getViewVerticalDragRange(View child)
    返回給定的child在相應的方向上可以被拖動的最遠距離,默認返回0。ACTION_DOWN發生時,若觸摸點處的child消費了事件,並且想要在某個方向上可以被拖動,就要在對應方法裏返回大於0的數。
    被調用的地方有三處:
    • 在checkTouchSlop()中被調用,返回值大於0纔會去檢查mTouchSlop。在ACTION_MOVE裏調用tryCaptureViewForDrag()之前會調用checkTouchSlop()。如果checkTouchSlop()失敗,就不會去捕獲View了。
    • 如果ACTION_DOWN發生時,觸摸點處有子View消費事件,在shouldInterceptTouchEvent()的ACTION_MOVE裏會被調用。如果兩個方向上的range都是0(兩個方法都返回0),就不會去捕獲View了。
    • 在調用smoothSlideViewTo()時被調用,用於計算自動滾動要滾動多長時間,這個時間計算出來後,如果超過最大值,最終時間就取最大值,所以不用擔心在getView[Horizontal|Vertical]DragRange裏返回了不合適的數導致計算的時間有問題,只要返回大於0的數就行了。
  • boolean tryCaptureView(View child, int pointerId)
    在tryCaptureViewForDrag()中被調用,返回true表示捕獲給定的child。tryCaptureViewForDrag()被調用的地方有
    • shouldInterceptTouchEvent()的ACTION_DOWN裏
    • shouldInterceptTouchEvent()的ACTION_MOVE裏
    • processTouchEvent()的ACTION_MOVE裏
  • int clampViewPositionHorizontal(View child, int left, int dx)、int clampViewPositionVertical(View child, int top, int dy)
    child在某方向上被拖動時會調用對應方法,返回值是child移動過後的座標位置,clampViewPositionHorizontal()返回child移動過後的left值,clampViewPositionVertical()返回child移動過後的top值。
    兩個方法被調用的地方有兩處:
    • 在dragTo()中被調用,dragTo()在processTouchEvent()的ACTION_MOVE裏被調用。用來獲取被拖動的View要移動到的位置。
    • 如果ACTION_DOWN發生時,觸摸點處有子View消費事件,在shouldInterceptTouchEvent()的ACTION_MOVE裏會被調用。如果兩個方向上返回的還是原來的left和top值,就不會去捕獲View了。

案例參考

在這裏列舉一部分對ViewDragHelper的應用案例,大家自己剖析它們的源碼來實踐鞏固。

  1. YoutubeLayout,這是最簡單的Demo
  2. QQ5.x側滑菜單ResideLayout
  3. SwipeBackLayoutSwipeBack
  4. SlidingUpPanel
  5. DrawerLayout

其他關於ViewDragHelper的分析文章

轉載請註明出處http://www.cnblogs.com/lqstayreal/p/4500219.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章