Android -- ViewDragHelper

ViewDragHelper

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

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

好的特色

  • ViewDragHelper.Callback是鏈接ViewDragHelper與view之間的橋樑(這個view通常是指擁子view的容器即parentView)android

  • ViewDragHelper能夠檢測到是否觸及到邊緣app

  • ViewDragHelper並非直接做用於要被拖動的View,而是使其控制的視圖容器中的子View能夠被拖動,若是要指定某個子view的行爲,須要在Callback中想辦法;ide

  • 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基本沒作任何修改,就是演示簡單的移動 。post

第二個View,實現的是除了移動後,鬆手自動返回到本來的位置。(注意你拖動的越快,返回的越快)。咱們在onLayout以後保存了最開啓的位置信息,最主要仍是重寫了Callback中的onViewReleased,咱們在onViewReleased中判斷若是是mAutoBackView則調用settleCapturedViewAt回到初始的位置。你們能夠看到緊隨其後的代碼是invalidate();由於其內部使用的是mScroller.startScroll,因此別忘了須要invalidate()以及結合computeScroll方法一塊兒。this

第三個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=%252FAndroid_cnblogs

ViewDragHelper.zip

參考:http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2015/09/23/viewDragHelper%E8%A7%A3%E6%9E%90/

相關文章
相關標籤/搜索