Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android應用ViewDragHelper詳解及部分源碼淺析

Android應用ViewDragHelper詳解及部分源碼淺析

編輯:關於Android編程

 

1 背景

很久沒有更新博客了,忙裡偷閒產出一篇。寫這片文章主要是去年項目中的一個需求,當時三下五除二的將其實現了,但是源碼的閱讀卻一直扔在那遲遲沒有時間理會,現在揀起來看看吧,否則心裡一直不踏實。

關於啥是ViewDragHelper,這裡不再解釋,官方下面這個解釋已經很牛逼了,如下:

/**
 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
 * of useful operations and state tracking for allowing a user to drag and reposition
 * views within their parent ViewGroup.
 */

下面我們先從一個例子說起,然後再進行源碼淺析。duang、duang、duang!!!

2 基礎實例

在開始源碼淺析之前有必要先通過一個實例來認識下他的功效,下面呈上的效果圖就是他的功效之一:

這裡寫圖片描述

Android-Blog-Source/tree/master/ViewDragHelper-Demo">該效果實例源碼點我下載

這個效果圖其實和去年那個項目中的需求十分接近了,為了保密期間才修改為此;當時的需求是這樣的:

能夠上下平滑收縮; 能夠上下手勢收縮; 能夠上下跟隨手指手勢收縮一定比例; 上下兩層裡下層是一個ScrollView,上層是一個ListView的業務邏輯交互;

當時拿到這個需求時我的內心是崩潰的(哈哈,當然不會崩潰,網絡用語而已);起初構思時我心裡萌生了三套方案,是用屬性動畫來做呢,還是滑動來做啊,還是用ViewDragHelper來做啊,記得當時思考了一會決定用ViewDragHelper來做,因為他實現起來比較簡單,其他的比較麻煩。哈哈,下面給出ViewDragHelper的實現代碼:

/**
 * 垂直DrawerLayout
 * 實現步驟:
 * 1.使用靜態方法來構ViewDragHelper,需要傳入一個ViewDragHelper.Callback對象.
 * 2.重寫onInterceptTouchEvent和onTouchEvent回調ViewDragHelper中對應方法.
 * 3.在ViewDragHelper.Callback中對視圖做操作.
 * 4.使用ViewDragHelper.smoothSlideViewTo()方法平滑滾動.
 * 5.自定義一些交互邏輯的自由實現.
 */
public class VerticalDrawerLayout extends ViewGroup {
    private ViewDragHelper mTopViewDragHelper;

    private View mContentView;
    private View mDrawerView;

    private int mCurTop = 0;

    private boolean mIsOpen = true;

    public VerticalDrawerLayout(Context context) {
        super(context);
        init();
    }

    public VerticalDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public VerticalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //Step1:使用靜態方法構造ViewDragHelper,其中需要傳入一個ViewDragHelper.Callback回調對象.
        mTopViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack());
        mTopViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP);
    }

    //Step2:定義一個ViewDragHelper.Callback回調實現類
    private class ViewDragHelperCallBack extends ViewDragHelper.Callback {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //返回ture則表示可以捕獲該view,手指摸上一瞬間調運
            return child == mDrawerView;
        }

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            //setEdgeTrackingEnabled設置的邊界滑動時觸發
            //captureChildView是為了讓tryCaptureView返回false依舊生效
            mTopViewDragHelper.captureChildView(mDrawerView, pointerId);
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //手指觸摸移動時實時回調, left表示要到的x位置
            return super.clampViewPositionHorizontal(child, left, dx);
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            //手指觸摸移動時實時回調, top表示要到的y位置
            //保證手指挪動時只能向上,向下最大到0
            return Math.max(Math.min(top, 0), -mDrawerView.getHeight());
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            //手指釋放時回調
            float movePrecent = (releasedChild.getHeight() + releasedChild.getTop()) / (float) releasedChild.getHeight();
            int finalTop = (xvel >= 0 && movePrecent > 0.5f) ? 0 : -releasedChild.getHeight();
            mTopViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), finalTop);
            invalidate();
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            //mDrawerView完全挪出屏幕則防止過度繪制
            mDrawerView.setVisibility((changedView.getHeight()+top == 0)? View.GONE : View.VISIBLE);
            mCurTop = top;
            requestLayout();
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            if (mDrawerView == null) return 0;
            return (mDrawerView == child) ? mDrawerView.getHeight() : 0;
        }

        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            if (state == ViewDragHelper.STATE_IDLE) {
                mIsOpen = (mDrawerView.getTop() == 0);
            }
        }
    }

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

    public void closeDrawer() {
        if (mIsOpen) {
            mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), -mDrawerView.getHeight());
            invalidate();
        }
    }

    public void openDrawer() {
        if (!mIsOpen) {
            mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), 0);
            invalidate();
        }
    }

    public boolean isDrawerOpened() {
        return mIsOpen;
    }

    //Step3:重寫onInterceptTouchEvent回調ViewDragHelper中對應的方法.
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mTopViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    //Step3:重寫onTouchEvent回調ViewDragHelper中對應的方法.
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTopViewDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(measureWidth, measureHeight);

        mContentView = getChildAt(0);
        mDrawerView = getChildAt(1);

        MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                measureWidth- (params.leftMargin + params.rightMargin), MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                measureHeight - (params.topMargin + params.bottomMargin), MeasureSpec.EXACTLY);
        mContentView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        mDrawerView.measure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();
            mContentView.layout(params.leftMargin, params.topMargin,
                    mContentView.getMeasuredWidth() + params.leftMargin,
                    mContentView.getMeasuredHeight() + params.topMargin);

            params = (MarginLayoutParams) mDrawerView.getLayoutParams();
            mDrawerView.layout(params.leftMargin, mCurTop + params.topMargin,
                    mDrawerView.getMeasuredWidth() + params.leftMargin,
                    mCurTop + mDrawerView.getMeasuredHeight() + params.topMargin);
        }
    }
}

怎麼樣,簡單吧。效果也有了,代碼也有了,ViewDragHelper也體驗了,接下來就該苦逼的看源碼了。

3 ViewDragHelper局部源碼淺析

上面的例子中我們可以知道,使用ViewDragHelper的第一步就是通過他提供的靜態工廠方法create獲取實例,因為ViewDragHelper的構造方法是私有的。既然這樣那我們先看下這些靜態工廠方法,如下:

public class ViewDragHelper {
    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;
    }

    public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
        return new ViewDragHelper(forParent.getContext(), forParent, cb);
    }
}

可以看見,三個參數的create方法實質調運的還是兩個參數的create。其中forParent一般是我們自定義的ViewGroup,cb是控制子View相關狀態的回調抽象類實現對象,sensitivity是用來調節mTouchSlop的,至於mTouchSlop是啥以及sensitivity的作用下面會有解釋。接著可以發現兩個參數的create實質是調運了ViewDragHelper的構造函數,那我們就來分析一下這個構造函數,如下源碼:

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
    ......
    //對參數進行賦值
    mParentView = forParent;
    mCallback = cb;
    //通過ViewConfiguration等將dp轉px得到mEdgeSize
    final ViewConfiguration vc = ViewConfiguration.get(context);
    final float density = context.getResources().getDisplayMetrics().density;
    mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
    //通過ViewConfiguration獲取TouchSlop,默認為8
    mTouchSlop = vc.getScaledTouchSlop();
    //獲得允許執行一個fling手勢動作的最大速度值
    mMaxVelocity = vc.getScaledMaximumFlingVelocity();
    //獲得允許執行一個fling手勢動作的最小速度值
    mMinVelocity = vc.getScaledMinimumFlingVelocity();
    //通過兼容包的ScrollerCompat實例化Scroller,動畫插值器為sInterpolator
    mScroller = ScrollerCompat.create(context, sInterpolator);
}

可以看見,構造函數其實沒有做啥特別的事情,主要就是一些參數的實例化,最主要的就是實例化了一個Scroller的內部成員,而且還在ViewDragHelper中重寫了插值器,如下:

private static final Interpolator sInterpolator = new Interpolator() {
    public float getInterpolation(float t) {
        t -= 1.0f;
        return t * t * t * t * t + 1.0f;
    }
};

關於動畫插值器這裡不再說了,之前博文有講過。我們還是把視線回到上面實例部分,可以看見,在獲取ViewDragHelper實例之後我們接著重寫了ViewGroup的onInterceptTouchEvent和onTouchEvent方法,在其中觸發了ViewDragHelper的shouldInterceptTouchEvent和processTouchEvent方法。所以下面我們就來分析這兩個方法,首先我們看下shouldInterceptTouchEvent方法,如下:

//這玩意返回值的作用在前面博客中有分析,我們先來看下ACTION_DOWN事件
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    final int actionIndex = MotionEventCompat.getActionIndex(ev);

    if (action == MotionEvent.ACTION_DOWN) {
        //每次ACTION_DOWN都會調用cancel(),該方法中mVelocityTracker被清空,故mVelocityTracker記錄的是本次ACTION_DOWN到ACTION_UP的觸摸信息
        cancel();
    }
    //獲取VelocityTracker實例,記錄下各個觸摸點信息用來計算本次滑動速率等
    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);
        //Step 1
        saveInitialMotion(x, y, pointerId);
        //Step 2
        final View toCapture = findTopChildUnder((int) x, (int) y);

        //Step 3
        if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
            tryCaptureViewForDrag(toCapture, pointerId);
        }
        //Step 4
        final int edgesTouched = mInitialEdgesTouched[pointerId];
        if ((edgesTouched & mTrackingEdges) != 0) {
            mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
        }
        break;
        }
        //暫時忽略
        ......
    }
    //Step 5
    return mDragState == STATE_DRAGGING;
}

可以看見,上面代碼我們只列出了shouldInterceptTouchEvent關於ACTION_DOWN部分的代碼,在注釋裡我將他分為了5步來敘述。我們先來看當ACTION_DOWN觸發時Step 1的代碼,他通過saveInitialMotion(x, y, pointerId)保存了事件的初始信息,如下是saveInitialMotion方法代碼:

    private void saveInitialMotion(float x, float y, int pointerId) {
        ensureMotionHistorySizeForId(pointerId);
        mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
        mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
    //getEdgesTouched就是通過mEdgeSize去判斷觸摸邊沿方向是否OK
        mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
        mPointersDown |= 1 << pointerId;
    }

接著目光再回到shouldInterceptTouchEvent方法的Step 2,可以發現他嘗試通過findTopChildUnder()方法來獲取當前觸摸點下最頂層的子View,我們可以看下這個方法的源碼,如下:

public View findTopChildUnder(int x, int y) {
    //獲取mParentView中子View個數
    final int childCount = mParentView.getChildCount();
    //倒序遍歷整個子View,因為最上面的子View最後插入
    for (int i = childCount - 1; i >= 0; i--) {
        //遍歷拿到最靠上且獲得觸摸焦點的那個子View
        final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
        //判斷當前DOWN的觸摸點是否在該子View范圍,也就是說是不是摸上了該子View
        if (x >= child.getLeft() && x < child.getRight() &&
            y >= child.getTop() && y < child.getBottom()) {
        return child;
        }
    }
    return null;
}

這個方法你不覺的奇怪麼?怎麼遍歷時getChildAt傳入的不是index,而是mCallback.getOrderedChildIndex(i)啊,我勒個去,mCallback不就是我們創建ViewDragHelper實例時傳入的CallBack對象麼?我們還是跳過去看下getOrderedChildIndex()方法吧,如下:

public static abstract class Callback {
    ......
    public int getOrderedChildIndex(int index) {
        return index;
    }
    ......
}

貓了個咪的,這玩意默認啥也沒干啊,看樣子是某些情況下給我們用來重寫的,啥情況?原來在上面findTopChildUnder()方法中返回當前被觸摸的View時會有一種坑爹的情況出現,那就是如果在mParentView的同一個位置有多個子View是重疊的,此時又想讓重疊的View中下面指定的那個被選中(默認for循環是倒序額)時getOrderedChildIndex()方法的默認實現就搞不定了,所以就需要我們自己去實現Callback裡的getOrderedChildIndex()方法來改變查找子View的順序。譬如:

public int getOrderedChildIndex(int index) {
    //實現重疊View時讓下面的View獲得選中
    int topIndex = mParentView.indexOfChild(your_top_view);
    int BottomSelectedIndex = mParentView.indexOfChild(blow_your_top_view_selected);
    return ((index == topIndex) ? indexBottom : index);
}

好了,扯遠了,我們還是把目光回到shouldInterceptTouchEvent方法的Step 3,可以發現這裡有一個判斷,因為第一次觸摸屏幕mCapturedView默認為null,所以一開始不會執行這個判斷裡的代碼,同時因為mDragState第一次也不處於STATE_SETTLING狀態,所以不執行,等執行時再分析。我們繼續往下看Step 4,可以發現這裡首先拿了saveInitialMotion方法賦值的結果,然後判斷設置的邊沿方向進行Callback的onEdgeTouched()方法回調。到這裡boolean shouldInterceptTouchEvent()方法的第一次觸摸按下ACTION_DOWN所干的事就完了,接著Step 5時直接return了mDragState == STATE_DRAGGING;,因為上面說了,在ACTION_DOWN時mDragState還是STATE_IDLE狀態,所以這裡返回了false。

至此第一次手指觸摸mParentView上時shouldInterceptTouchEvent的ACTION_DOWN流程就結束了,接著我們就是依據這個返回值的情況進行分析(具體參見之前博文關於Android觸摸事件傳遞的分析)。這裡返回false就表示mParentView沒有攔截這次事件,所以接下來會在mParentView中觸發每個子View的boolean dispatchTouchEvent()方法,這時依據Android觸摸事件處理機制又分為了兩大類情況來處理,一類就是子View消費了這個ACTION_DOWN,一類是沒有消費的情況,而這些情況下又分很多中情況,譬如子View消費了本次ACTION_DOWN,mParentView的onTouchEvent()就不會收到ACTION_DOWN了(即ViewDragHelper的processTouchEvent()方法也就收不到ACTION_DOWN了);這時候又有很多情況,譬如當前子View如果調運了requestDisallowInterceptTouchEvent(true),則ACTION_MOVE等來時mParentView的onInterceptTouchEvent()方法就不會被回調(即ViewDragHelper的相關方法也就沒意義了);當前子View沒有調用requestDisallowInterceptTouchEvent(true),則ACTION_MOVE等來時mParentView的onInterceptTouchEvent()方法還會被執行,此時若onInterceptTouchEvent()方法返回true,則mParentView的onTouchEvent()就會被調運(即ViewDragHelper的processTouchEvent()會被執行)。

額額額,觸摸事件傳遞本來就很復雜,這裡情況又很多,所以我們還是不分情況來說了,淺析源碼我們牽一條主線走就行,其它的用到時再分析即可。

所以我們來看下子View沒有消費這次ACTION_DOWN事件(即子View的dispatchTouchEvent()方法返回false)的流程。此時mParentView的dispatchTouchEvent()方法會調用自己的super.dispatchTouchEvent()方法(即View的dispatchTouchEvent()),然後super會調用mParentView的onTouchEvent()方法(即調用ViewDragHelper的processTouchEvent()方法)。這時onTouchEvent()方法需要返回true(只用在ACTION_DOWN時返回true,否則onTouchEvent()方法無法接收接下來的ACTION_MOVE等事件),當onTouchEvent()返回true以後ACTION_MOVE、ACTION_UP等事件再來時就不會再執行mParentView的onInterceptTouchEvent()了。

那就牽著主線走吧,我們可以看下processTouchEvent()方法的ACTION_DOWN部份源碼,如下:

public void processTouchEvent(MotionEvent ev) {
    //和shouldInterceptTouchEvent相似,省略   
    ......

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
        //和shouldInterceptTouchEvent相似,省略解釋
        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);

        //Step 1 重點!!!!
        tryCaptureViewForDrag(toCapture, pointerId);

        //和shouldInterceptTouchEvent相似,省略解釋
        final int edgesTouched = mInitialEdgesTouched[pointerId];
        if ((edgesTouched & mTrackingEdges) != 0) {
            mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
        }
        break;
        }
    //省略其他ACTION
    ......
    }
}

可以看見,該方法裡最核心的東西估計就是tryCaptureViewForDrag()方法了,下面我們看下這個方法,如下:

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
    //正在拽就不管了
    if (toCapture == mCapturedView && mActivePointerId == pointerId) {
        // Already done!
        return true;
    }
    //調用了Callback的tryCaptureView()方法,傳遞觸摸到的View和觸摸點編號
    //Callback的tryCaptureView()決定是否能夠拖動當前觸摸的View
    if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
        mActivePointerId = pointerId;
        //重點
        captureChildView(toCapture, pointerId);
        return true;
    }
    return false;
}

可以看見,通過Callback的tryCaptureView()重寫設置是否可以挪動該View,若可以挪動(返回true)則又調運了captureChildView()方法,繼續看下captureChildView()方法源碼:

public void captureChildView(View childView, int activePointerId) {
    ......
    //暫存被捕獲的這個View的相關信息及觸摸信息
    mCapturedView = childView;
    mActivePointerId = activePointerId;
    //通過Callback的onViewCaptured()方法回調當前View被捕獲了
    mCallback.onViewCaptured(childView, activePointerId);
    //設置當前被捕獲的子View狀態為STATE_DRAGGING
    //裡面會通過mCallback.onViewDragStateChanged(state)回調告知狀態
    setDragState(STATE_DRAGGING);
}

到此一次mParentView自己消費事件,子View無攔截ACTION_DOWN的事件處理就徹底結束了。接著就是主流程的ACTION_MOVE事件了,這玩意由於mParentView的onTouchEvent消費了事件且沒進行攔截ACTION_DOWN,所以一旦觸發時就直接走進了processTouchEvent()方法裡,下面是ACTION_MOVE代碼:

public void processTouchEvent(MotionEvent ev) {
    ......

    switch (action) {
        ......
        case MotionEvent.ACTION_MOVE: {
        //分兩種情況,依賴上一個ACTION_DOWN事件
        if (mDragState == STATE_DRAGGING) {
            //ACTION_DOWN時CallBack的tryCaptureView()返回true時對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]);
            //Step 1 重點!!!!!!
            dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
            //Step 2 重點!!!!!!
            saveLastMotion(ev);
        } else {
            //ACTION_DOWN時CallBack的tryCaptureView()返回false時對mDragState沒進行賦值,故此流程
            // 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];
                //Step 3 重點!!!!!!
                reportNewEdgeDrags(dx, dy, pointerId);
                if (mDragState == STATE_DRAGGING) {
                    // Callback might have started an edge drag.
                    break;
                }

                final View toCapture = findTopChildUnder((int) x, (int) y);
                //Step 4 重點!!!!!!
                if (checkTouchSlop(toCapture, dx, dy) &&
                        tryCaptureViewForDrag(toCapture, pointerId)) {
                    break;
                }
            }
            saveLastMotion(ev);
        }
        break;
        }
    ......
    }
}

可以看見,當ACTION_MOVE事件多次觸發時該段代碼會依據我們重寫CallBack的代碼分為可以托拽當前View和不能托拽兩種情況。

我們先來看下不能托拽的情況,這種相對比較簡單,可以看到上面代碼的Step 3部分,reportNewEdgeDrags()方法是我們的重點,如下:

    //在托拽時該方法會被多次調運
    private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
        int dragsStarted = 0;
        ......//四個方向,省略三個
        if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
            dragsStarted |= EDGE_BOTTOM;
        }

        if (dragsStarted != 0) {
            mEdgeDragsInProgress[pointerId] |= dragsStarted;
        //該方法只會被調運一次,checkNewEdgeDrag方法中有處理
            mCallback.onEdgeDragStarted(dragsStarted, pointerId);
        }
    }

可以發現,當我們在ACTION_DOWN觸發時重寫CallBack的tryCaptureView()方法返回false(當前View不能托拽)且是邊沿觸摸時移動時首先會回調Callback的onEdgeDragStarted()方法通知自定義ViewGroup開始邊沿托拽。接著我們把目光投向Step 4部分,if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) 判斷是我們關注的核心,toCapture其實就是當前捕獲的View(這個View在邊沿模式時一般摸不到,所以其實拿到的不是想要的childView,所以一般我們會在回調onEdgeDragStarted()方法中重寫手動調用captureChildView()方法,傳入我們摸不到的View,這樣就相當於繞過tryCaptureView將狀態設置為STATE_DRAGGING了),下面我們先看下checkTouchSlop()方法,如下:

//檢查手指移動的距離有沒有超過觸發處理移動事件的最短距離mTouchSlop
private boolean checkTouchSlop(View child, float dx, float dy) {
    if (child == null) {
        return false;
    }
    //如果想讓某個View滑動,就要返回大於0,否則processTouchEvent()的ACTION_MOVE就不會調用tryCaptureViewForDrag()來捕獲當前觸摸的View
    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;
}

到此ACTION_MOVE不能托拽的情況就分析完畢了,我們再來看下可以托拽的情況,請看上面processTouchEvent()代碼的Step 1,2部分,重點在於dragTo()方法,當我們正常捕獲到View時ACTION_MOVE就不停的調用dragTo()對mCaptureView進行拖動,源碼如下:

//left、top為mCapturedView.getLeft()+dx、mCapturedView.getTop()+dy,即期望目標坐標
//dx、dy為前後兩次ACTION_MOVE移動的距離
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);
        //這是View中定義的方法,實質是改變View的mLeft、mRight、mTop、mBottom達到移動View的效果,類似layout()方法的效果
        //clampedX為新位置,oldLeft為舊位置,若想不動保證插值為0即可!!!!
        mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
    }
    if (dy != 0) {
        //重寫固定縱坐標移動到的位置
        clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
        //這是View中定義的方法,實質是改變View的mLeft、mRight、mTop、mBottom達到移動View的效果,類似layout()方法的效果
        mCapturedView.offsetTopAndBottom(clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
        final int clampedDx = clampedX - oldLeft;
        final int clampedDy = clampedY - oldTop;
        //當位置有變化時回調Callback的onViewPositionChanged方法實時通知
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
            clampedDx, clampedDy);
    }
}

到此可以發現ACTION_MOVE時如果可以托拽則會實時挪動View的位置,同時回調很多方法。具體移動到哪和范圍由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()來決定。到此一次ACTION_MOVE事件的觸發處理也就分析完畢了。下面就該是松手時ACTION_UP或者ACTION_MOVE被mParentView的上級View攔截觸發的ACTION_CANCEL事件了,他們與ACTION_MOVE類似,直接觸發processTouchEvent()的ACTION_UP或者ACTION_CANCEL,如下:

public void processTouchEvent(MotionEvent ev) {
    ......
    switch (action) {
        ......
        case MotionEvent.ACTION_UP: {
        if (mDragState == STATE_DRAGGING) {
            releaseViewForPointerUp();
        }
        //重置所有的狀態記錄
        cancel();
        break;
        }

        case MotionEvent.ACTION_CANCEL: {
        if (mDragState == STATE_DRAGGING) {
            dispatchViewReleased(0, 0);
        }
        //重置所有的狀態記錄
        cancel();
        break;
        }
    }
}

可以看見,ACTION_UP和ACTION_CANCEL的實質都是重置資源和通知View觸摸被釋放,一個調運了releaseViewForPointerUp方法,另一個調運了dispatchViewReleased方法而已。那我們先看下releaseViewForPointerUp方法,源碼如下:

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

哎喲,握草,dispatchViewReleased(xvel, yvel)不就是ACTION_CANCEL傳參為0調運的方法嗎?也就是說ACTION_CANCEL與ACTION_UP邏輯一致,只是傳入的速率不同而已。那我們就來看看這個方法,如下:

    private void dispatchViewReleased(float xvel, float yvel) {
        mReleaseInProgress = true;
    //通知外部View被釋放了
        mCallback.onViewReleased(mCapturedView, xvel, yvel);
        mReleaseInProgress = false;
    //如果之前是STATE_DRAGGING狀態,則復位狀態為STATE_IDLE
        if (mDragState == STATE_DRAGGING) {
            // onViewReleased didn't call a method that would have changed this. Go idle.
            setDragState(STATE_IDLE);
        }
    }

可以看見dispatchViewReleased()方法主要就是通過CallBack通知手指松開了,同時將狀態置位為STATE_IDLE。但是如果你留意該方法的注釋和方法裡的mReleaseInProgress變量的話,你一定會有疑惑。下面我就從注釋和該變量出現的地方進行下簡單分析,然後回過頭就知道咋回事了,該方法注釋提到了mReleaseInProgress變量與settleCapturedViewAt()和flingCapturedView()方法有關,那我們就來看下是啥關系,查看這兩個方法的開頭可以發現一個共性如下:

if (!mReleaseInProgress) {
    throw new IllegalStateException("Cannot XXXXXXXXXX outside of a call to " +
            "Callback#onViewReleased");
}

我靠,默認mReleaseInProgress是false,在dispatchViewReleased()中CallBack回調onViewReleased()方法前把他置位了true,onViewReleased()後置位了false。這就是為啥注釋裡說唯一可以調用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()裡,這下你指定就明白了,因為別的地方會拋出異常哇。

握草,這兩方法為啥這麼神奇,為啥只能在CallBack回調onViewReleased()中使用啊,我們先來分析一下就知道了,如下先看settleCapturedViewAt()方法源碼:

//限制最終慣性滾動到的終極位置及滾動過去
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
    //表明只能在CallBack回調onViewReleased()中使用
    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));
}

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
    ......
    if (dx == 0 && dy == 0) {
        // Nothing to do. Send callbacks, be done.
        mScroller.abortAnimation();
        setDragState(STATE_IDLE);
        return false;
    }
    //直接用過Scroller滾動到指定位置
    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
    mScroller.startScroll(startLeft, startTop, dx, dy, duration);
    //滾動時設置狀態為STATE_SETTLING
    setDragState(STATE_SETTLING);
    return true;
}

再看下flingCapturedView()方法,如下:

//不限制終點,由松手時加速度決定慣性滾動過去,fling效果
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");
    }
    //直接用過Scroller滾動
    mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
        (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
        (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
        minLeft, maxLeft, minTop, maxTop);
    //滾動時設置狀態為STATE_SETTLING
    setDragState(STATE_SETTLING);
}

到此確實很奇怪,為啥這兩個方法要這麼限制設計?我還是沒想明白,請教大神指點一下!但是我全局搜索mScroller的相關調運又發現了新的方法,如下:

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

這貨也能達到上面受限的settleCapturedViewAt()方法的效果,額額額,我凌亂了,這麼設計是為啥額,不清楚。不過到此一次完整的ViewDragHelper的觸摸移動主流程就分析完成了,其他相關狀態請自行分析,對我來說有這些就差不多夠了,主流程掌握了基本就能明白他提供的相關API含義了,其它的遇到問題再現查即可。

4 源碼局部淺析總結

經過上面的淺析與注釋總結歸納如下總結方便日後使用;同時我們可以發現ViewDragHelper的本質其實涉及的知識點還是很多的,主要在事件處理上,不得不佩服該工具類考慮的強大。

//常用核心API歸納總結
public class ViewDragHelper {
    //當前View處於空閒狀態,靜止
    public static final int STATE_IDLE = 0;
    //當前View處於托動狀態中
    public static final int STATE_DRAGGING = 1;
    //當前View處於滾動慣性到settling坐標間的狀態
    public static final int STATE_SETTLING = 2;
    //可托拽邊緣方向常量
    public static final int EDGE_LEFT = 1 << 0;
    public static final int EDGE_RIGHT = 1 << 1;
    public static final int EDGE_TOP = 1 << 2;
    public static final int EDGE_BOTTOM = 1 << 3;
    public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;

    ...

    //公有靜態內部抽象回調類,當ViewDragHelper控制的ViewGroup中View變化時會被回調
    public static abstract class Callback {
        //當托拽狀態變化時回調,譬如動畫結束後回調為STATE_IDLE等
        //state有三種狀態,均以STATE_XXXX模式
        public void onViewDragStateChanged(int state) {}
        //當前被觸摸的View位置變化時回調
        //changedView為位置變化的View,left/top變化時新的x左/y頂坐標,dx/dy為從舊到新的偏移量
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
        //tryCaptureViewForDrag()成功捕獲到子View時或者手動調用captureChildView()時回調
        public void onViewCaptured(View capturedChild, int activePointerId) {}
        //當子View被松手或者ACTION_CANCEL時時回調,xvel/yvel為離開屏幕時各方向每秒運動的速率,為px
        public void onViewReleased(View releasedChild, float xvel, float yvel) {}
        //當觸摸ACTION_DOWN或ACTION_POINTER_DOWN邊沿時回調
        public void onEdgeTouched(int edgeFlags, int pointerId) {}
        //返回true鎖定edgeFlags對應的邊緣,鎖定後的邊緣就不會回調onEdgeDragStarted()
        public boolean onEdgeLock(int edgeFlags) {
            return false;
        }
        //ACTION_MOVE且沒有鎖定邊緣時觸發
        //可在此手動調用captureChildView()觸發從邊緣拖動子View,有點類似略過tryCaptureView返回false響應重定向其他View的效果
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
        //尋找當前觸摸點View時回調此方法
        //如果需要改變子View的倒序遍歷查詢順序則可改寫此方法,譬如讓重疊的下層View先於上層View被捕獲
        public int getOrderedChildIndex(int index) {
            return index;
        }
        //返回給定子View在相應方向上可以被拖動的最遠距離,默認為0,一般是可被挪動View時指定為指定View的大小等
        public int getViewHorizontalDragRange(View child) {
            return 0;
        }
        public int getViewVerticalDragRange(View child) {
            return 0;
        }
        //傳遞當前觸摸上的子View,如果需要當前觸摸的子View進行拖拽移動就返回true,否則返回false
        public abstract boolean tryCaptureView(View child, int pointerId);
        //決定要拖拽的子View在所屬方向上應該移動到的位置
        //child為拖拽的子View,left為期望值,dx為挪動差值
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return 0;
        }
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }
    }

    ...

    //構造工廠方法,sensitivity用來調節mTouchSlop的值,默認一般傳遞1即可
    //sensitivity越大,mTouchSlop越小,對滑動的檢測就越敏感,譬如手指move多少才算滑動,否則忽略
    public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {...}
    //設置允許父View的某個邊緣可以用來響應托拽
    //相當於控制了CallBack對象的onEdgeTouched()和onEdgeDragStarted()方法是否被回調
    public void setEdgeTrackingEnabled(int edgeFlags) {...}
    //兩個傳遞MotionEvent的方法
    public boolean shouldInterceptTouchEvent(MotionEvent ev) {...}
    public void processTouchEvent(MotionEvent ev) {...}
    //主動在父View內捕獲指定的子view用於拖曳,會回調tryCaptureView()
    public void captureChildView(View childView, int activePointerId) {...}
    //指定某個View自動滾動到指定的位置,初速度為0,可在任何地方調用
    //如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
    public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {...}
    //以松手前的滑動速度為初值,讓捕獲到的子View自動滾動到指定位置,只能在Callback的onViewReleased()中使用
    //如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
    public boolean settleCapturedViewAt(int finalLeft, int finalTop) {...}
    //以松手前的滑動速度為初值,讓捕獲到的子View在指定范圍內fling慣性運動,只能在Callback的onViewReleased()中使用
    //如果這個方法返回true,那麼在接下來動畫移動的每一幀中都會回調continueSettling(boolean)方法,直到結束
    public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {...}
    /**
     * 在整個settle狀態中,這個方法會返回true,deferCallbacks決定滑動是否Runnable推遲,一般推遲
     * 在調用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()時,
     * 需要實現mParentView的computeScroll()方法,如下:
     * @Override
     * public void computeScroll() {
     *     if (mDragHelper.continueSettling(true)) {
     *         ViewCompat.postInvalidateOnAnimation(this);
     *     }
     * }
     */
    public boolean continueSettling(boolean deferCallbacks) {...}

    ...

    //設置與獲取最小速率,一般保持默認
    public void setMinVelocity(float minVel) {...}
    public float getMinVelocity() {...}
    //獲取當前子View所處狀態
    public int getViewDragState() {...}
    //返回可觸摸反饋區域邊緣大小,單位為px
    public int getEdgeSize() {...}
    //返回當前捕獲的子View,如果沒有則為null
    public View getCapturedView() {...}
    //獲取當前拖曳的View的Pointer ID
    public int getActivePointerId() {...}
    //獲取最小觸發拖曳動作的靈敏度差值,單位為px
    public int getTouchSlop() {...}
    //類似ACTION_CANCEL事件的觸發調運
    public void cancel() {...}
    //終止手勢,結束動畫滾動等,恢復初始STATE_IDLE狀態
    public void abort() {...}
    ...
}

可以看見,上面基本就是整個ViewDragHelper的相關public等可控制的API解釋了,我們基本上可以通過他們的組合搞出各種自定義的控件來玩玩的。下面我們再粗略給出ViewDragHelper使用的流程,如下:

自定義ViewGroup裡通過ViewDragHelper靜態工廠方法create()創建實例並實現ViewDragHelper.CallBack抽象類。

在自定義ViewGroup的onInterceptTouchEvent()方法裡調用並返回ViewDragHelper的shouldInterceptTouchEvent()方法,在onTouchEvent()方法裡調用ViewDragHelper()的processTouchEvent()方法,且返回true(因為ACTION_DOWN時如果子View沒有消費事件,我們需要在onTouchEvent()中返回true,否則收不到後續的事件,從而不會產生拖動等效果)。

依據自己需求實現ViewDragHelper.CallBack中相關方法即可。

至此已經實現了子View拖動效果,如果需要Fling或者慣性滾動效果則還需要實現自定義ViewGroup的computeScroll()方法進行手動刷幀。

以上就是使用ViewDragHelper的全過程,可以發現其真的很牛逼。

5 ViewDragHelper進階實戰與總結

扯了上面那麼多,下面我們給出一個使用ViewDragHelper搞出來的東東,大家一看就知道他很牛逼了。如下是效果圖(Ubuntu下面,GIF圖Low爆了,請諒解):

這裡寫圖片描述

該控件效果就不解釋了,代碼也不多,源碼可以點我下載即可

至此整個ViewDragHelper進階就介紹完了,如果你還認為不夠的話,那就可以自行看看官方的DrawerLayout控件實現即可,可以把它當作該文的進階實例,這裡我就不再分析了,我們的項目中重寫了DrawerLayout,因為一些特殊的交互需求。


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