源碼分析 ItemTouchHelper手勢的入口 (OnInterceptTouchEvent onLongPress等)

ItemTouchHelper是v7包RecyclerView的ItemDecoration接口的一個實現,其前身是v4包的ViewDragHelper(可在任意ViewGroup中使用)在DrawerLayout、SlidingPaneLayout源碼中都有應用。

兩者使用方法也很類似,都是實現各自的Callback

ItemTouchHelper手勢的實現都在匿名內部類mOnItemTouchListener中實現,

private final OnItemTouchListener mOnItemTouchListener
            = new OnItemTouchListener()

與RecyclerView的綁定,則在attachToRecyclerView方法中的setupCallbacks()中

add到RV中

    /**
     * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already
     * attached to a RecyclerView, it will first detach from the previous one. You can call this
     * method with {@code null} to detach it from the current RecyclerView.
     *
     * @param recyclerView The RecyclerView instance to which you want to add this helper or
     *                     {@code null} if you want to remove ItemTouchHelper from the current
     *                     RecyclerView.
     */
    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks(); //避免重複綁定
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            final Resources resources = recyclerView.getResources();
            mSwipeEscapeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
            mMaxSwipeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
            setupCallbacks(); //初始化 setup
        }
    }

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop(); //初始化TouchSlop
        mRecyclerView.addItemDecoration(this);  //綁定Callback.OnDraw供實現onChildDraw
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); //綁定手勢監聽
        mRecyclerView.addOnChildAttachStateChangeListener(this); 
        initGestureDetector();
    }

    private void destroyCallbacks() {
        mRecyclerView.removeItemDecoration(this);  //避免重複綁定
        mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); //避免重複綁定
        mRecyclerView.removeOnChildAttachStateChangeListener(this); //避免重複綁定
        // clean all attached
        final int recoverAnimSize = mRecoverAnimations.size();
        for (int i = recoverAnimSize - 1; i >= 0; i--) {
            final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
            mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
        }
        mRecoverAnimations.clear();
        mOverdrawChild = null;
        mOverdrawChildPosition = -1;
        releaseVelocityTracker();
    }

進入正題

嚴格的說,ItemTouchHelper的手勢正式接管方法是select()方法,而通過手勢開始(達到某種條件)而進入select的入口總共3個

1、長按

2、與rv垂直方向的swipe移動(超過touchSlop的Move)

3、按下一個正在做恢復動畫的vh(對RecoverAnimation沒做完的vh觸發Down事件)

既然是手勢接管,毫無疑問,這些入口的入口一定來自mOnItemTouchListener的OnInterceptTouchEvent方法

        @Override
        public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);//主要實現onLongPress的監聽  
這裏就是入口1,下有對於ItemTouchHelperGestureListener的onLongPress具體分析

    //如果是個長按 那麼攔截短路掉RV自己的滑動
    //並判定Down選中的vh ItemTouchHelper正式接管 
            if (DEBUG) {
                Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
            }
            final int action = MotionEventCompat.getActionMasked(event);
            if (action == MotionEvent.ACTION_DOWN) {
                mActivePointerId = event.getPointerId(0);
下面記錄down的位置,注意這裏是DOWN的時候無條件記錄!!!
因爲接管手勢是未來的事,之後的位移比對等等都來自這個初始記錄值
長按的時候這個初始值還會被替換,(因爲長按的起始是長按600ms而不是DOWN)
如果這裏代碼沒有被執行 startDrag startSwipe等方法在外部調用是無法被正確工作的! 
startDrag等方法工作前提的說明見我的另一篇點擊打開鏈接
                mInitialTouchX = event.getX(); 
                mInitialTouchY = event.getY(); 
                obtainVelocityTracker();
                if (mSelected == null) {
                    final RecoverAnimation animation = findAnimation(event);
下面是入口3,一個並不常見的場景
                    if (animation != null) { //特別注意!!! 這裏並不是一個主流程的手勢接管入口
                    //該段代碼僅處理DOWN按下一個正在做恢復動畫的vh的場景!
                    //千萬不要誤解,通常DOWN的時候是沒有恢復動畫的,所以mSelected不會被賦值
                    //intercept也就不會成功,後面的手勢OnItemTouchListener會正常執行onIntercepter方法
                    //若都返回false 也會傳遞到vh的itemView的DispatchTouchEvent裏
                        mInitialTouchX -= animation.mX; 
                        mInitialTouchY -= animation.mY;
                        endRecoverAnimation(animation.mViewHolder, true); //肯定把正在進行的恢復動畫先幹掉咯
                        if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { 
							//mPendingCleanup 存儲動畫之後就應該detach並被cleanup的Views
                            mCallback.clearView(mRecyclerView, animation.mViewHolder); //如果正在被移除 恢復vh
                        }
                        select(animation.mViewHolder, animation.mActionState); //調用select 接管!
                        updateDxDy(event, mSelectedFlags, 0);
                    }
                }
            }

處理cancel與擡起

 else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mActivePointerId = ACTIVE_POINTER_ID_NONE; //如果是cancel或up 那麼select(null)
                select(null, ACTION_STATE_IDLE);
            }
再下面就是Swipe的入口了 入口2
 else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                // in a non scroll orientation, if distance change is above threshold, we
                // can select the item
                final int index = event.findPointerIndex(mActivePointerId);
                if (DEBUG) {
                    Log.d(TAG, "pointer index " + index);
                }
                if (index >= 0) { //檢驗PointerId合法性 不是ACTIVE_POINTER_ID_NONE -1
                    checkSelectForSwipe(action, event, index); //Swipe的入口
                    //前提:(沒有接管過mSelect爲空、必須是Move事件、支持swipe、拖動超過Slop且與Rv方向垂直)
                }
            }
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(event);
            }
            return mSelected != null;  //特別特別注意: mSelected是否爲空決定是否接管!!!(select方法是否調用)
        }

onIntercepterTouchEvent 到此結束 注意它的返回值


下面來看一下入口1和入口2的具體實現

長按手勢的入口ItemTouchHelperGestureListener, 也就是GestureDetector的回調實現。

    private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {

        ItemTouchHelperGestureListener() {
        }

        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            View child = findChildView(e);
			//先找點了誰(vh) 點到花花草草(divider分割線之類)可不作數哦
            if (child != null) {
                ViewHolder vh = mRecyclerView.getChildViewHolder(child);
                if (vh != null) {
					//注意 這個hasDragFlag方法和hasSwipeFlag一樣調用public可重寫的getAbsoluteMovementFlags方法
					//入參有vh 也就是說可以像checkSelectForSwipe定製swipe方向一樣
					//定製各個vh自己的move方向
                    if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
                        return;
                    }
                    int pointerId = e.getPointerId(0);
                    // Long press is deferred.
                    // Check w/ active pointer id to avoid selecting after motion
                    // event is canceled.
                    if (pointerId == mActivePointerId) {
                        final int index = e.findPointerIndex(mActivePointerId);
                        final float x = e.getX(index);
                        final float y = e.getY(index);
                        mInitialTouchX = x; //如果長按了 那麼長按纔是手勢的正式入口
                        mInitialTouchY = y;
                        mDx = mDy = 0f;
                        if (DEBUG) {
                            Log.d(TAG,
                                    "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
                        }
                        if (mCallback.isLongPressDragEnabled()) {//最關鍵的校驗 如果你通過重寫這方法
                        //禁用了LongPressDragEnabled HOHO 那它肯定也就不拖動了
                            select(vh, ACTION_STATE_DRAG);
                        }
                    }
                }
            }
        }
    }
接下來看Move事件Swipe的入口
    /**
     * Checks whether we should select a View for swiping.
     */
    boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
        if (mSelected != null || action != MotionEvent.ACTION_MOVE
                || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
            return false;
        }
        if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
            return false;
        }
        final ViewHolder vh = findSwipedView(motionEvent);//檢驗手勢目前是否符合Swipe 下有分析
        if (vh == null) {
            return false;
        }
		//之前校驗了一堆 如果已經有mSelected 那麼不用再校驗了
		//校驗只檢驗Move事件 且itemTouchHelper必須是拖動狀態
		//Callback必須支持Swipe isItemViewSwipeEnabled沒有被重寫flase
		//如果RecyclerView已經處理了拖動狀態————對不起也不行
		//如果點擊的地方沒有viewHoler不行(比如點到了divider?)
        final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
		//獲取支持的swipe方向,注意!這裏getAbsoluteMovementFlags入參有vh,也就是說你
		//可以定製每一個vh的swipe或者move方向(見OnLongPress)
        final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);

        if (swipeFlags == 0) {
            return false;
        }

        // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
        // updateDxDy to avoid swiping if user moves more in the other direction
        final float x = motionEvent.getX(pointerIndex);
        final float y = motionEvent.getY(pointerIndex);

        // Calculate the distance moved
        final float dx = x - mInitialTouchX;
        final float dy = y - mInitialTouchY;
        // swipe target is chose w/o applying flags so it does not really check if swiping in that
        // direction is allowed. This why here, we use mDx mDy to check slope value again.
        final float absDx = Math.abs(dx);
        final float absDy = Math.abs(dy);

        if (absDx < mSlop && absDy < mSlop) {
            return false;
        }
        if (absDx > absDy) {
            if (dx < 0 && (swipeFlags & LEFT) == 0) {
                return false;
            }
            if (dx > 0 && (swipeFlags & RIGHT) == 0) {
                return false;
            }
        } else {
            if (dy < 0 && (swipeFlags & UP) == 0) {
                return false;
            }
            if (dy > 0 && (swipeFlags & DOWN) == 0) {
                return false;
            }
        }
        mDx = mDy = 0f;
        mActivePointerId = motionEvent.getPointerId(0);
        select(vh, ACTION_STATE_SWIPE);
        return true;
    }
檢驗這個手勢是否是swipe一個ViewHolder

    private ViewHolder findSwipedView(MotionEvent motionEvent) {
        final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
        if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { //再次校驗pointerId是否有效
            return null;
        }
        final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
        final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
        final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
        final float absDx = Math.abs(dx);
        final float absDy = Math.abs(dy);

        if (absDx < mSlop && absDy < mSlop) { //判斷是否超過TouchSlop
            return null;
        }
        if (absDx > absDy && lm.canScrollHorizontally()) { //如果RecyclerView滑動的同一個方向
        //的位移更大,那麼這應該交給RV處理,這裏不是個swipe
            return null;
        } else if (absDy > absDx && lm.canScrollVertically()) {
            return null;
        }
        View child = findChildView(motionEvent);
        if (child == null) {
            return null;
        }
        return mRecyclerView.getChildViewHolder(child);
    }
下面分析一下最重要的接管方法select()

    /**
     * Starts dragging or swiping the given View. Call with null if you want to clear it.
     *
     * @param selected    The ViewHolder to drag or swipe. Can be null if you want to cancel the
     *                    current action
     * @param actionState The type of action
     */
    void select(ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;  //如果狀態沒有改變 不需要重複處理
        }
        mDragScrollStartTimeInMs = Long.MIN_VALUE;
        final int prevActionState = mActionState;
        // prevent duplicate animations
        endRecoverAnimation(selected, true);
        mActionState = actionState;
        if (actionState == ACTION_STATE_DRAG) {
            // we remove after animation is complete. this means we only elevate the last drag
            // child but that should perform good enough as it is very hard to start dragging a
            // new child before the previous one settles.
            mOverdrawChild = selected.itemView;
            addChildDrawingOrderCallback();
        }
        int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
                - 1;
        boolean preventLayout = false;

        if (mSelected != null) { //如果原來有接管目標vh的 那麼那個vh要壽終正寢 完成後事動畫
            final ViewHolder prevSelected = mSelected;
            if (prevSelected.itemView.getParent() != null) {
                final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
                        : swipeIfNecessary(prevSelected);
                releaseVelocityTracker();
                // find where we should animate to
                final float targetTranslateX, targetTranslateY;
                int animationType;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        targetTranslateY = 0;
                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                        break;
                    case UP:
                    case DOWN:
                        targetTranslateX = 0;
                        targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
                        break;
                    default:
                        targetTranslateX = 0;
                        targetTranslateY = 0;
                }
                if (prevActionState == ACTION_STATE_DRAG) { //如果原來有接管對象vh的
                    animationType = ANIMATION_TYPE_DRAG; //拖動的變狀態爲拖動動畫
                } else if (swipeDir > 0) {
                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS; //swipe的變狀態爲swipe動畫
                } else {
                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
                }
                getSelectedDxDy(mTmpPosition);
                final float currentTranslateX = mTmpPosition[0];
                final float currentTranslateY = mTmpPosition[1];
                final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                        prevActionState, currentTranslateX, currentTranslateY,
                        targetTranslateX, targetTranslateY) { //後事(哦不, 恢復)動畫
                    @Override
                    public void onAnimationEnd(ValueAnimatorCompat animation) {
                        super.onAnimationEnd(animation);
                        if (this.mOverridden) {
                            return;
                        }
                        if (swipeDir <= 0) {
                            // this is a drag or failed swipe. recover immediately
                            //如果是個拖拉(onMove)或者沒有過閾值的swipe 是沒有動畫的
                            //立即恢復
                            mCallback.clearView(mRecyclerView, prevSelected);
                            // full cleanup will happen on onDrawOver
                        } else {
                            // wait until remove animation is complete.
                            mPendingCleanup.add(prevSelected.itemView);
                            mIsPendingCleanup = true;
                            if (swipeDir > 0) {
                                // Animation might be ended by other animators during a layout.
                                // We defer callback to avoid editing adapter during a layout.
                                //動畫可能在佈局過程中由其他animators完成。
								//所以這裏用遞歸判斷isRunning!!!,以避免在佈局期間編輯適配器。
								//保證onSwipe的回調(特別注意:動畫結束不意味着mRecoverAnimations裏
								//對應的vh被清除! 依賴於onSwipe裏移除對應數據而移除vh之後執行
								//onChildViewDetachedFromWindow的回調裏endRecoverAnimation()移除)
                                postDispatchSwipe(this, swipeDir); 
                            }
                        }
                        // removed from the list after it is drawn for the last time
                        if (mOverdrawChild == prevSelected.itemView) {
                            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                        }
                    }
                };
                final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                        targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
                rv.setDuration(duration); //注意!!! 這個CallBack的getAnimationDuration方法可以重寫
                //自定義動畫時間
                mRecoverAnimations.add(rv);
                rv.start();
                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView, prevSelected);
            }
            mSelected = null; //舊接管目標清空
        }
        if (selected != null) { //新接管目標
            mSelectedFlags =
                    (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
                            >> (mActionState * DIRECTION_FLAG_COUNT);
            mSelectedStartX = selected.itemView.getLeft();
            mSelectedStartY = selected.itemView.getTop();
            mSelected = selected; //賦值

            if (actionState == ACTION_STATE_DRAG) {
                mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            }
        }
        final ViewParent rvParent = mRecyclerView.getParent();
        if (rvParent != null) { //如果select有值 那麼Disallow攔截 這個號理解
            rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
        }
        if (!preventLayout) {
            mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
        }
        mCallback.onSelectedChanged(mSelected, mActionState); //注意 這裏接管和放開接管都有CallBack回調
        //可以重寫的!!!
        mRecyclerView.invalidate();
    }




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