Android -- ViewDragHelper


ViewDragHelper

SlidingPaneLayout和DrawerLayout,現在這倆個類被廣泛的運用,其實研究他們的源碼你會發現這兩個類都運用了ViewDragHelper來處理拖動。

ViewDragHelper並不是第一個用於分析手勢處理的類,gesturedetector也是,但是在和拖動相關的手勢分析方面gesturedetector只能說是勉為其難。

好的特點

  • ViewDragHelper.Callback是連接ViewDragHelper與view之間的橋梁(這個view一般是指擁子view的容器即parentView)

  • ViewDragHelper可以檢測到是否觸及到邊緣

  • ViewDragHelper並不是直接作用於要被拖動的View,而是使其控制的視圖容器中的子View可以被拖動,如果要指定某個子view的行為,需要在Callback中想辦法;

  • ViewDragHelper的本質其實是分析onInterceptTouchEvent和onTouchEvent的MotionEvent參數,然后根據分析的結果去改變一個容器中被拖動子View的位置( 通過offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在觸摸的時候判斷當前拖動的是哪個子View;

雖然ViewDragHelper的實例方法 ViewDragHelper.create(ViewGroup forParent, Callback cb) 可以指定一個被ViewDragHelper處理拖動事件的對象 ,但ViewDragHelper類的設計決定了其適用於被包含在一個自定義ViewGroup之中,而不是對任意一個布局上的視圖容器使用ViewDragHelper。

使用

public class VDHLayout extends LinearLayout{
    private ViewDragHelper mDragger;

    public VDHLayout(Context context, AttributeSet attrs){
        super(context, attrs);
        //第二個參數就是滑動靈敏度的意思
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback(){
            //這個地方實際上函數返回值為true就代表可以滑動 為false 則不能滑動
            @Override
            public boolean tryCaptureView(View child, int pointerId){
                return true;
            }

            //這個地方實際上left就代表 你將要移動到的位置的坐標。返回值就是最終確定的移動的位置。
            // 我們要讓view滑動的范圍在我們的layout之內
             //實際上就是判斷如果這個坐標在layout之內 那我們就返回這個坐標值。
            //如果這個坐標在layout的邊界處 那我們就只能返回邊界的坐標給他。不能讓他超出這個范圍
            //除此之外就是如果你的layout設置了padding的話,也可以讓子view的活動范圍在padding之內的.
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx){
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy){
                return top;
            }
        });
    }

   @Override
    public boolean onInterceptTouchEvent(MotionEvent event){
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        mDragger.processTouchEvent(event);
        return true;
    }
}

觸摸相關

onInterceptTouchEvent中通過使用mDragger.shouldInterceptTouchEvent(event)來決定我們是否應該攔截當前的事件。onTouchEvent中通過mDragger.processTouchEvent(event)處理事件。

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
  final int leftBound = getPaddingLeft();
  final int rightBound = getWidth() - mDragView.getWidth();
  final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
  return newLeft;
}

@Override
public int clampViewPositionVertical(View child, int top, int dy) {
  final int topBound = getPaddingTop();
  final int bottomBound = getHeight() - mDragView.getHeight();
  final int newTop = Math.min(Math.max(top, topBound), bottomBound);
  return newTop;
}

Code

public class VDHLayout extends LinearLayout{
    private ViewDragHelper mDragger;

    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;

    private Point mAutoBackOriginPos = new Point();

    public VDHLayout(Context context, AttributeSet attrs){
        super(context, attrs);
        mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback(){
            @Override
            public boolean tryCaptureView(View child, int pointerId){
                //mEdgeTrackerView禁止直接移動
                return child == mDragView || child == mAutoBackView;
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx){
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy){
                return top;
            }


            //手指釋放的時候回調
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel){
                //mAutoBackView手指釋放時可以自動回去
                if (releasedChild == mAutoBackView){
                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                    invalidate();
                }
            }

            //在邊界拖動時回調
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId){
                mDragger.captureChildView(mEdgeTrackerView, pointerId);
            }
        });
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event){
        return mDragger.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        mDragger.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll(){
        if(mDragger.continueSettling(true)){
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b){
        super.onLayout(changed, l, t, r, b);

        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }

    @Override
    protected void onFinishInflate(){
        super.onFinishInflate();

        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }
}

第一個View基本沒做任何修改,就是演示簡單的移動 。

第二個View,實現的是除了移動后,松手自動返回到原本的位置。(注意你拖動的越快,返回的越快)。我們在onLayout之后保存了最開啟的位置信息,最主要還是重寫了Callback中的onViewReleased,我們在onViewReleased中判斷如果是mAutoBackView則調用settleCapturedViewAt回到初始的位置。大家可以看到緊隨其后的代碼是invalidate();因為其內部使用的是mScroller.startScroll,所以別忘了需要invalidate()以及結合computeScroll方法一起。

第三個View,實現的是邊界移動時對View進行捕獲。我們在onEdgeDragStarted回調方法中,主動通過captureChildView對其進行捕獲,該方法可以繞過tryCaptureView,所以我們的tryCaptureView雖然並為返回true,但卻不影響。注意如果需要使用邊界檢測需要添加上mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

Click事件

將View全部加上clickable=true,意思就是子View可以消耗事件。再次運行,你會發現本來可以拖動的View不動了。原因是什么呢?主要是因為,如果子View不消耗事件,那么整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent,在onTouchEvent的DOWN的時候就確定了captureView。如果消耗事件,那么就會先走onInterceptTouchEvent方法,判斷是否可以捕獲,而在判斷的過程中會去判斷另外兩個回調的方法:getViewHorizontalDragRangegetViewVerticalDragRange,只有這兩個方法返回大於0的值才能正常的捕獲。

所以,記得重寫下面這兩個方法:

@Override
public int getViewHorizontalDragRange(View child){
     return getMeasuredWidth()-child.getMeasuredWidth();
}

@Override
public int getViewVerticalDragRange(View child){
     return getMeasuredHeight()-child.getMeasuredHeight();
}

ViewDragHelper.Callback

ViewDragHelper中攔截和處理事件時,需要會回調CallBack中的很多方法來決定一些事,比如:哪些子View可以移動、對個移動的View的邊界的控制等等。

  • onViewDragStateChanged

    當ViewDragHelper狀態發生變化時回調(IDLE,DRAGGING,SETTING[自動滾動時])

  • onViewPositionChanged

    當captureview的位置發生改變時回調

  • onViewCaptured

    當captureview被捕獲時回調

  • onViewReleased

    當capture view被釋放的時候

  • onViewCaptured

    當captureview被捕獲時回調

  • onEdgeTouched

    當觸摸到邊界時回調。

  • onEdgeLock

    true的時候會鎖住當前的邊界,false則unLock。

  • getOrderedChildIndex

    改變同一個坐標(x,y)去尋找captureView位置的方法。(具體在:findTopChildUnder方法中)

回調順序

houldInterceptTouchEvent:

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

源碼分析

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構造方法, 我們再來看看這個構造方法。

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行,首先是關於多點觸控.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后接下來應該是會調用哪個方法呢,接下來會在mParentView的所有子View中尋找響應這個Touch事件的View(會調用每個子View 的dispatchTouchEvent()方法,dispatchTouchEvent里一般又會調用onTouchEvent()).

1.如果沒有子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等事件了。

2.如果有子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);
    }
}

首先這兩個方法是干什么的呢。在現實生活中保齡球的打法是,先做扔的動作讓球的速度達到最大,然后突然松手,由於慣性,保齡球就以最后松手前的速度為初速度拋出去了,直至自然停止,或者撞到邊界停止,這種效果叫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()的時候。

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(1f, (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);
}

如果給定的速率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);
        }
    }

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

假設現在shouldInterceptTouchEvent()的ACTION_DOWN部分執行完了,也有子View消費了這次的ACTION_DOWN事件,那么接下來就會調用mParentView的onInterceptTouchEvent()的ACTION_MOVE部分,接着調用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等事件,否則無法完成拖動。

總結

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

  • 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()被調用的地方有

1.tryCaptureViewForDrag()成功捕獲到子View時

1.1 shouldInterceptTouchEvent()的ACTION_DOWN部分捕獲到

1.2 shouldInterceptTouchEvent()的ACTION_MOVE部分捕獲到

1.3 processTouchEvent()的ACTION_MOVE部分捕獲到

2.調用settleCapturedViewAt()、smoothSlideViewTo()、flingCapturedView()時

3.拖動View松手時(processTouchEvent()的ACTION_UP、ACTION_CANCEL)

4.自動滾動停止時(continueSettling()里檢測到滾動結束時)

5.外部調用abort()時

  • void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 正在被拖動的View或者自動滾動的View的位置改變時會調用此方法。

1.在dragTo()里被調用(正在被拖動時)

2.在continueSettling()里被調用(自動滾動時)

3.外部調用abort()時被調用

  • void onViewCaptured(View capturedChild, int activePointerId) tryCaptureViewForDrag()成功捕獲到子View時會調用此方法。

1.在shouldInterceptTouchEvent()的ACTION_DOWN里成功捕獲

2.在shouldInterceptTouchEvent()的ACTION_MOVE里成功捕獲

3.在processTouchEvent()的ACTION_MOVE里成功捕獲

4.手動調用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

我是天王蓋地虎的分割線

demo:http://pan.baidu.com/s/1dD1Qx01#path=%2FAndroid_cnblogs

ViewDragHelper.zip

參考:http://souly.cn/技術博文/2015/09/23/viewDragHelper解析/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM