RecyclerView 擴(kuò)展(二) - 手把手教你認(rèn)識ItemTouchHelper

??今天我們來學(xué)習(xí)一下RecyclerView另一個鮮為人知的輔助類--ItemTouchHelper两嘴。我們在做列表視圖嘿棘,就比如說智嚷,ListView或者RecyclerView馍悟,通常會有兩種需求:1. 側(cè)滑刪除狮含;2. 拖動交換位置柒傻。對于第一種需求使用傳統(tǒng)的版本實現(xiàn)還比較簡單孝赫,我們可以自定義ItemView來實現(xiàn);而第二種的話红符,可能就稍微有一點(diǎn)復(fù)雜青柄,可能需要重寫LayoutManager
??這些辦法也不否認(rèn)是有效的解決方案预侯,但是是否是簡單和低耦合性的辦法呢致开?當(dāng)然不是,踩過坑的同學(xué)應(yīng)該都知道萎馅,不管是自定義View還是自定義LayoutManager都不是一件簡單的事情双戳,其次,自定義ItemView導(dǎo)致Adapter的通用性降低校坑。這些實現(xiàn)方式都是比較麻煩的拣技。
??而谷歌爸爸真是貼心,知道我們都有這種需求耍目,就小手一抖膏斤,隨便幫我們實現(xiàn)了一個Helper類,來減輕我們的工作量邪驮。這就是ItemTouchHelper的作用莫辨。
??本文打算從兩個方面來教大家認(rèn)識ItemTouchHelper類:

  1. ItemTouchHelper的基本使用
  2. ItemTouchHelper的源碼分析

??本文參考資料:

  1. RecyclerView高級進(jìn)階總結(jié):ItemTouchHelper實現(xiàn)拖拽和側(cè)滑刪除
  2. ItemTouchHelper源碼分析

1. 概述

??在正式介紹ItemTouchHelper之前,我們先來了解ItemTouchHelper是什么東西毅访。
??從ItemTouchHelper的源碼中沮榜,我們可以看出來,ItemTouchHelper繼承了ItemDecoration喻粹,根本上就是一個ItemDecoration蟆融。關(guān)于ItemDecoration的分析,有興趣的同學(xué)可以參考我的文章:RecyclerView 擴(kuò)展(一) - 手把手教你認(rèn)識ItemDecoration守呜。

public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener {
}

??至于為什么ItemTouchHelper會繼承ItemDecoration型酥,后面會詳細(xì)的解釋山憨,這里就先賣一下關(guān)子。
??然后弥喉,我們先來看看ItemTouchHelper實現(xiàn)的效果郁竟,讓大家有一個直觀的體驗。
??先是側(cè)滑刪除的效果:


??然后是拖動交換位置:

??本文打算從上面兩種效果來介紹ItemTouchHelper的使用由境。

2. ItemTouchHelper的基本使用

??既然是手把手教大家認(rèn)識ItemTouchHelper棚亩,所以自然需要介紹它的的基本使用,現(xiàn)在讓我們來看看究竟怎么使用ItemTouchHelper虏杰。
??在正式介紹ItemTouchHelper的基本使用之前讥蟆,我們還必須了解一個類--ItemTouchHelper.CallbackItemTouchHelper就是依靠這個類來實現(xiàn)側(cè)滑刪除和拖動位置兩種效果的嘹屯,我來看看它攻询。

(1). ItemTouchHelper.Callback

??我們在使用ItemTouchHelper時,必須自定義一個ItemTouchHelper.Callback州弟,我們來了解一下其中比較重要的幾個方法。

方法名 作用
getMovementFlags 在此方法里面我們需要構(gòu)建兩個flag低零,一個是dragFlags婆翔,表示拖動效果支持的方向,另一個是swipeFlags掏婶,表示側(cè)滑效果支持的方向啃奴。在我們的Demo中,拖動執(zhí)行上下兩個方向雄妥,側(cè)滑執(zhí)行左右兩個方向最蕾,這些操作我們都可以在此方法里面定義。
onMove 當(dāng)拖動效果已經(jīng)產(chǎn)生了老厌,會回調(diào)此方法瘟则。在此方法里面,我們通常會更新數(shù)據(jù)源枝秤,就比如說醋拧,一個ItemView從0拖到了1位置,那么對應(yīng)的數(shù)據(jù)源也需要更改位置淀弹。
onSwiped 當(dāng)側(cè)滑效果以上產(chǎn)生了丹壕,會回調(diào)此方法。在此方法里面薇溃,我們也會更新數(shù)據(jù)源菌赖。與onMove方法不同到的是,我們在這個方法里面從數(shù)據(jù)源里面移除相應(yīng)的數(shù)據(jù)沐序,然后調(diào)用notifyXXX方法就行了琉用。

??對于ItemTouchHelper的基本使用來說忿峻,我們只需要了解這三個方法就已經(jīng)OK了。接下來辕羽,我將正式介紹ItemTouchHelper的基本使用逛尚。

(2). 基本使用

??首先,我們需要自定義一個ItemTouchHelper.Callback刁愿,如下:

public class CustomItemTouchCallback extends ItemTouchHelper.Callback {

    private final ItemTouchStatus mItemTouchStatus;

    public CustomItemTouchCallback(ItemTouchStatus itemTouchStatus) {
        mItemTouchStatus = itemTouchStatus;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        // 上下拖動
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        // 向左滑動
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // 交換在數(shù)據(jù)源中相應(yīng)數(shù)據(jù)源的位置
        return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        // 從數(shù)據(jù)源中移除相應(yīng)的數(shù)據(jù)
        mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
    }
}

??然后绰寞,我們在使用RecyclerView時,添加這兩行代碼就行了:

        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);

??最終的效果就是上面的動圖展示的铣口,是不是覺得非常的簡單呢滤钱?接下來,我將正式的分析ItemTouchHelper的源碼脑题。

(4).源碼

??為了方便大家理解件缸,我將我的代碼上傳到github,有興趣的同學(xué)可以看看:ItemTouchHelperDemo叔遂。

3. ItemTouchHelper的源碼分析

??我們從基本使用中了解到他炊,ItemTouchHelper的使用是非常簡單的,所以大家內(nèi)心有沒有一種好奇呢已艰?那就是ItemTouchHelper究竟是怎么實現(xiàn)痊末,為什么兩個相對比較復(fù)雜的效果,通過幾行代碼就能實現(xiàn)呢哩掺?接下來的內(nèi)容就能找到答案凿叠。

(1). attachToRecyclerView方法

??我們都知道,ItemTouchHelper的入口方法就是attachToRecyclerView方法嚼吞,接下來盒件,我們先來看看這個方法為我們做了哪些事情。

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (recyclerView != 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();
        }
    }

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        startGestureDetection();
    }

    private void startGestureDetection() {
        mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
        mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                mItemTouchHelperGestureListener);
    }

??相對來說舱禽,attachToRecyclerView方法是比較簡單的炒刁。這其中,我們發(fā)現(xiàn)ItemTouchHelper是通過ItemTouchListener接口來為每個ItemView處理事件呢蔫,同時切心,從這里我們可以看出來,在ItemTouchHelper內(nèi)部還使用了GestureDetector片吊,而這里GestureDetector的作用主要是來判斷ItemView是否進(jìn)行了長按行為绽昏。
??ItemTouchHelper的分析重點(diǎn)應(yīng)該是事件處理,但是在這之前俏脊,我們先來看一個方法全谤,這個方法非常的重要的。

(2). select方法

??當(dāng)我們的操作觸發(fā)了長按或者側(cè)滑的行為爷贫,都會回調(diào)此方法认然,同時當(dāng)我們手勢釋放补憾,也會回調(diào)此方法。
??所以從大的時機(jī)來看卷员,當(dāng)手勢開始或者釋放都會回調(diào)select方法盈匾;而每個大時機(jī)又分為兩個小時機(jī),分別是長按和側(cè)滑毕骡,分別表示拖動交換位置和側(cè)滑刪除操作削饵。
??在正式分析select方法的代碼之前,我們需要了解兩個東西:

  1. selected表示被選中的ViewHolder未巫。其中窿撬,selected如果為null,則表示當(dāng)前處于手勢(包括長按和側(cè)滑)釋放時機(jī)叙凡;反之劈伴,selected不為null,則表示當(dāng)前處于手勢開始的時機(jī)握爷。
  2. actionState表示當(dāng)前的狀態(tài)跛璧,一共有三個值可選,分別是:1. ACTION_STATE_IDLE表示沒有任何手勢饼拍,此時selected對應(yīng)的應(yīng)當(dāng)是null赡模;2. ACTION_STATE_SWIPE表示當(dāng)前ItemView處于側(cè)滑狀態(tài);3. ACTION_STATE_DRAG表示當(dāng)前ItemView處于拖動狀態(tài)师抄。在ItemTouchHelper內(nèi)部,就是通過這三個狀態(tài)來判斷ItemView處于什么狀態(tài)教硫。

??接下來我們來看看select方法的代碼:

    void select(ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        mDragScrollStartTimeInMs = Long.MIN_VALUE;
        final int prevActionState = mActionState;
        endRecoverAnimation(selected, true);
        mActionState = actionState;
        // 如果當(dāng)前是拖動行為,給RecyclerView設(shè)置一個ChildDrawingOrderCallback接口
        // 主要是為了調(diào)整ItemView繪制的順序
        if (actionState == ACTION_STATE_DRAG) {
            mOverdrawChild = selected.itemView;
            addChildDrawingOrderCallback();
        }
        int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
                - 1;
        boolean preventLayout = false;
        // 1.手勢釋放
        if (mSelected != null) {
           // ······
        }
        // 2. 手勢開始
        // selected不為null表示手勢開始叨吮,反之selected為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) {
            rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
        }
        if (!preventLayout) {
            mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
        }
        mCallback.onSelectedChanged(mSelected, mActionState);
        mRecyclerView.invalidate();
    }

??從上面的代碼中,我們可以總結(jié)出來幾個結(jié)論:

  1. 如果處于手勢開始階段瞬矩,即selected不為null茶鉴,那么會通過getAbsoluteMovementFlags方法來獲取執(zhí)行我們設(shè)置的flag,從而就知道執(zhí)行哪些行為(側(cè)滑或者拖動)和方向(上景用、下涵叮、左和右)。同時還會記錄下被選中ItemView的位置伞插。簡而言之割粮,就是一些變量的初始化。
  2. 如果處于手勢釋放階段媚污,即selected為null舀瓢,同時mSelected不為null,那么此時需要做的事情就稍微有一點(diǎn)復(fù)雜耗美。手勢釋放之后京髓,需要做的事情無非有兩件:1. 相關(guān)的ItemView到正確的位置航缀,就比如說,如果滑動條件不滿足堰怨,那么就返回原來的位置芥玉,這個就是一個動畫;2. 清理操作备图,比如說將mSelected重置為null之類的灿巧。

(3).如何判斷一個ItemView是否被選中

??我們知道,一旦調(diào)用selected就意味著一個ItemView被選中诬烹,接下來的就會隨著手勢出現(xiàn)側(cè)滑或者拖動的效果了砸烦。但是怎么來判斷一個ItemView是否被選中,我們從代碼來看看绞吁,我們分兩步來理解:1.側(cè)滑的選中幢痘;2. 拖動的選中。

A. 側(cè)滑

??判斷側(cè)滑行為是否選中主要在checkSelectForSwipe方法家破,我們來看看checkSelectForSwipe放大的代碼:

    boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
        // 如果mSelected不為null表示已經(jīng)有ItemView被選中
        // 同時從這里可以看出來Callback的isItemViewSwipeEnabled方法的作用
        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);
        if (vh == null) {
            return false;
        }
        final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

        final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
        // 如果flag沒有支持側(cè)滑的方向值颜说,那么返回為false
        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;
        }
        // 這里主要是判斷一個滑動是否符合側(cè)滑的條件
        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);
        // 表示當(dāng)前ItemView被側(cè)滑行為選中
        select(vh, ACTION_STATE_SWIPE);
        return true;
    }

??checkSelectForSwipe方法的代碼相對來說比較長,但是無非就是判斷當(dāng)前ItemView是否符合側(cè)滑行為汰聋,如果到最后符合的話门粪,那么就會調(diào)用select方法來初始化一些值。
??同時烹困,我們看一下checkSelectForSwipe方法的調(diào)用時機(jī)只有兩個地方:

  1. onTouchEvent方法
  2. onInterceptTouchEvent方法

??調(diào)用的時機(jī)也是比較正確的玄妈,至于為什么需要兩個地方來調(diào)用這個方法,我也不太清楚髓梅,估計做什么保險操作吧拟蜻。

B. 拖動選中

??拖動選中的時機(jī)比較簡單,因為拖動觸發(fā)的前提是長按ItemView枯饿,所以我們直接在ItemTouchHelperGestureListeneronLongPress方法找到相關(guān)代碼:

        @Override
        public void onLongPress(MotionEvent e) {
            if (!mShouldReactToLongPress) {
                return;
            }
            View child = findChildView(e);
            if (child != null) {
                ViewHolder vh = mRecyclerView.getChildViewHolder(child);
                if (vh != null) {
                    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()) {
                            select(vh, ACTION_STATE_DRAG);
                        }
                    }
                }
            }
        }

??這段代碼表達(dá)的意思非常簡單酝锅,這里我就不多余的解釋了。從這里可以看出來奢方,最終還是調(diào)用了select方法表示選中一個ItemView搔扁。

(3). ItemView隨著手指滑動

??我們知道了ItemTouchHelper怎么進(jìn)行手勢判斷來選中一個ItemView,選中之后的操作就是ItemView隨著手指滑動蟋字,我們來看看ItemView是怎么實現(xiàn)的稿蹲。
??我們知道,隨著手指的滑動愉老,onTouchEvent方法會被調(diào)用场绿,我們來看看相關(guān)的代碼:

        public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            // ······
            switch (action) {
                case MotionEvent.ACTION_MOVE: {
                    // Find the index of the active pointer and fetch its position
                    if (activePointerIndex >= 0) {
                        updateDxDy(event, mSelectedFlags, activePointerIndex);
                        moveIfNecessary(viewHolder);
                        mRecyclerView.removeCallbacks(mScrollRunnable);
                        mScrollRunnable.run();
                        mRecyclerView.invalidate();
                    }
                    break;
                }
                // ······
            }
        }

??上面的代碼我將它分為4步:

  1. 更新mDxmDy的值。mDxmDy表示手指在x軸和y軸上分別滑動的距離。
  2. 如果需要焰盗,移動其他ItemView的位置璧尸。這個主要針對拖動行為。
  3. 如果需要熬拒,滑動RecyclerView爷光。這個主要針對拖動行為,而這里滑動RecyclerView的條件就是澎粟,RecyclerView本身有大量的數(shù)據(jù)蛀序,一屏顯示不完,此時如果拖動一個ItemView達(dá)到RecyclerView的底部或者頂部活烙,會滑動RecyclerView徐裸。
  4. 更新被選中的ItemView的位置。代碼體現(xiàn)在mRecyclerView.invalidate()啸盏。

??其中重贺,更新mDxmDy的值是通過updateDxDy方法來實現(xiàn)的,而updateDxDy方法方法比較簡單回懦,這里就不展開了气笙。
??我們再來看看第二步,移動其他ItemView的位置主要是通過moveIfNecessary方法實現(xiàn)的怯晕。我們來看看具體的代碼:

    void moveIfNecessary(ViewHolder viewHolder) {
        // ······
        // 以上都是不符合move的條件
        // 1.尋找可能會交換位置的ItemView
        List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
        if (swapTargets.size() == 0) {
            return;
        }
        // 2.找到符合條件交換的ItemView
        // may swap.
        ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
        if (target == null) {
            mSwapTargets.clear();
            mDistances.clear();
            return;
        }
        final int toPosition = target.getAdapterPosition();
        final int fromPosition = viewHolder.getAdapterPosition();
        // 3.回調(diào)Callback里面的onMove方法潜圃,這個方法需要我們手動實現(xiàn)
        if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
            // 保證target的可見
            // keep target visible
            mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                    target, toPosition, x, y);
        }
    }

??如上就是moveIfNecessary方法的代碼,這里講它分為3步:

  1. 調(diào)用findSwapTarget方法舟茶,尋找可能會跟選中的ItemView交換位置的ItemView谭期。這里判斷的條件是只要選中的ItemView跟某一個ItemView重疊,那么這個ItemView可能會跟選中的ItemView交換位置吧凉。
  2. 調(diào)用Callback的chooseDropTarget方法來找到符合交換條件的ItemView崇堵。這里符合的條件是指,選中的ItemViewbottom大于目標(biāo)ItemViewbottom或者ItemViewtop大于目標(biāo)ItemViewtop客燕。通常來說,我們可以重寫chooseDropTarget方法狰贯,來定義什么條件下就交換位置也搓。
  3. 回調(diào)CallbackonMove方法,這個方法需要我們自己實現(xiàn)。這里需要注意的是涵紊,如果onMove方法返回為true的話傍妒,會調(diào)用Callback另一個onMove方法來保證target可見。為什么必須保證target可見呢摸柄?從官方文檔上來看的話颤练,如果target不可見,在某些滑動的情形下驱负,target會被remove掉(回收掉)嗦玖,從而導(dǎo)致drag過早的停止患雇。

??關(guān)于ItemTouchHelper是怎么來選擇交換位置的ItemView,重點(diǎn)就在findSwapTarget方法和chooseDropTarget方法宇挫。其中findSwapTarget方法是找到可能會交換位置的ItemView苛吱,chooseDropTarget方法是找到會交換位置的ItemView,這是兩個方法的不同點(diǎn)楞黄。同時恤磷,如果此時在拖動什猖,但是拖動的ItemView還未達(dá)到交換條件,也就是跟另一個ItemView只是重疊了一小部分援所,這種情況下,findSwapTargets方法返回的集合不為空欣除,但是chooseDropTarget方法尋找的ItemView為空住拭。
??然后就是第三步,第三步的作用是當(dāng)ItemView拖動到邊緣耻涛,如果此時RecyclerView可以滑動废酷,那么RecyclerView會滾動。具體的實現(xiàn)是在mScrollRunnablerun方法調(diào)用:

    final Runnable mScrollRunnable = new Runnable() {
        @Override
        public void run() {
            if (mSelected != null && scrollIfNecessary()) {
                if (mSelected != null) { //it might be lost during scrolling
                    moveIfNecessary(mSelected);
                }
                mRecyclerView.removeCallbacks(mScrollRunnable);
                ViewCompat.postOnAnimation(mRecyclerView, this);
            }
        }
    };

??在run方法里面通過scrollIfNecessary方法來判斷RecyclerView是否滾動抹缕,如果需要滾動,scrollIfNecessary方法會自動完成滾動操作澈蟆。
??最后一步就是ItemView位置的更新,也就是mRecyclerView.invalidate()的執(zhí)行卓研。這里需要理解的是趴俘,為什么通過invalidate方法就能更新ItemView的位置呢?因為ItemView在隨著手指移動時奏赘,變化的是translationXtranslationY兩個屬性寥闪,所以只需要調(diào)用invalidate方法就行。調(diào)用invalidate方法之后磨淌,相當(dāng)于RecyclerView會重新繪制一次疲憋,那么所有ItemDecorationonDrawonDrawOver方法都會被調(diào)用,而恰好的是梁只,ItemTouchHelper就是一個ItemDecoration缚柳。我們想要知道ItemView是怎么隨著手指移動的,答案就在onDraw方法里面:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // ······
        mCallback.onDraw(c, parent, mSelected,
                mRecoverAnimations, mActionState, dx, dy);
    }

??在onDraw方法里面搪锣,調(diào)用了CallbackonDraw方法秋忙。我們來看看CallbackonDraw方法:

        void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
                List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
                int actionState, float dX, float dY) {
            final int recoverAnimSize = recoverAnimationList.size();
            for (int i = 0; i < recoverAnimSize; i++) {
                final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
                anim.update();
                final int count = c.save();
                onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
                        false);
                c.restoreToCount(count);
            }
            if (selected != null) {
                final int count = c.save();
                onChildDraw(c, parent, selected, dX, dY, actionState, true);
                c.restoreToCount(count);
            }
        }

??代碼還是比較長,但是表示的意思是非常簡單的构舟。就是調(diào)用onChildDraw方法灰追,將所有正在交換位置的ItemView和被選中的ItemView作為參數(shù)傳遞過去。
??而在onChildDraw方法里面,調(diào)用了ItemTouchUIUtilonDraw方法弹澎。我們從ItemTouchUiUtil的實現(xiàn)類BaseImpl找到答案:

        @Override
        public void onDraw(Canvas c, RecyclerView recyclerView, View view,
                float dX, float dY, int actionState, boolean isCurrentlyActive) {
            view.setTranslationX(dX);
            view.setTranslationY(dY);
        }

??在這里改變了每個ItemViewtranslationXtranslationY,從而實現(xiàn)了ItemView隨著手指移動的效果朴下。
??從這里,我們可以看出來裁奇,一旦調(diào)用RecyclerViewinvalidate方法桐猬,ItemTouchHelperonDraw方法和onDrawOver方法都會被執(zhí)行。這個可能就是ItemTouchHelper繼承ItemDecoration的原因吧刽肠。

(4).為什么拖動的ItemView始終在其他ItemView的上面溃肪?

??當(dāng)我們在上下拖動的時候,我們發(fā)現(xiàn)一個問題音五,就是拖動的ItemView始終在其他ItemView的上面惫撰。這里,我們不禁疑惑躺涝,我們都知道厨钻,在ViewGroup里面,所有的child都有繪制順序坚嗜。通常來說夯膀,先添加的child先繪制,后添加的child后繪制苍蔬,在RecyclerView中也是不例外诱建,上面的ItemView先繪制,而下面的ItemView后繪制碟绑。而在這個拖動效果中俺猿,為什么不符合這個規(guī)則呢?我們來看看ItemTouchHelper是怎么幫忙實現(xiàn)的格仲。
??答案得分為兩個種情況押袍,一種是Api小于21,一種是Api大于等于21凯肋。
??我們先來看看Api小于21的情況谊惭。這個得從addChildDrawingOrderCallback方法里面去尋找答案:

    private void addChildDrawingOrderCallback() {
        if (Build.VERSION.SDK_INT >= 21) {
            return; // we use elevation on Lollipop
        }
        if (mChildDrawingOrderCallback == null) {
            mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
                @Override
                public int onGetChildDrawingOrder(int childCount, int i) {
                    if (mOverdrawChild == null) {
                        return i;
                    }
                    int childPosition = mOverdrawChildPosition;
                    if (childPosition == -1) {
                        childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
                        mOverdrawChildPosition = childPosition;
                    }
                    if (i == childCount - 1) {
                        return childPosition;
                    }
                    return i < childPosition ? i : i + 1;
                }
            };
        }
        mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
    }

??實現(xiàn)的原理就是給RecyclerView設(shè)置了一個ChildDrawingOrderCallback接口來改變child的繪制順序,這樣能保證被選中的ItemView后于重疊的ItemView繪制侮东,這樣就實現(xiàn)了被選中的ItemView始終在上面午笛。
??不過使用ChildDrawingOrderCallback接口時,我們需要注意的是:要想是接口有效苗桂,必須保證所有childelevation是一樣的,如果不一樣告组,那么elevation優(yōu)先級更高煤伟。
??從上面的注意點(diǎn),我們應(yīng)該都知道Api大于等于21時,使用的是什么方式來實現(xiàn)的吧便锨。沒錯就是通過改變 ItemViewelevation值實現(xiàn)的围辙。我們來看看具體實現(xiàn),在Api21ImplonDraw方法里面:

        @Override
        public void onDraw(Canvas c, RecyclerView recyclerView, View view,
                float dX, float dY, int actionState, boolean isCurrentlyActive) {
            if (isCurrentlyActive) {
                Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
                if (originalElevation == null) {
                    originalElevation = ViewCompat.getElevation(view);
                    float newElevation = 1f + findMaxElevation(recyclerView, view);
                    ViewCompat.setElevation(view, newElevation);
                    view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
                }
            }
            super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
        }

??因為這里使用的是ViewCompcat放案,所以當(dāng)Api小于21時姚建,調(diào)用setElevation是無效的。如上就是Api大于等于21時實現(xiàn)被選中的ItemView在所有ItemView上面的代碼吱殉。

(5). 手勢釋放之后

??不管是拖動還是側(cè)滑掸冤,當(dāng)我們手勢釋放之后,做的操作無非兩種:1. 回到原位友雳;2.移動到正確的位置稿湿。那這部分的具體實現(xiàn)在哪里呢?沒錯押赊,就在我們之前分析過的select方法里面饺藤,此時看select方法代碼時,我們需得注意兩個點(diǎn):

  1. 此時流礁,參數(shù)selected為null涕俗。
  2. 此時,變量mSelected不為null神帅。

??然后再姑,我們在來看看相關(guān)代碼:

    void select(ViewHolder selected, int actionState) {
        // ······
        if (mSelected != null) {
            final ViewHolder prevSelected = mSelected;
            if (prevSelected.itemView.getParent() != null) {
                // 1. 計算需要移動的距離
                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) {
                    animationType = ANIMATION_TYPE_DRAG;
                } else if (swipeDir > 0) {
                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
                } else {
                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
                }
                getSelectedDxDy(mTmpPosition);
                final float currentTranslateX = mTmpPosition[0];
                final float currentTranslateY = mTmpPosition[1];
                // 2.創(chuàng)建動畫
                final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                        prevActionState, currentTranslateX, currentTranslateY,
                        targetTranslateX, targetTranslateY) {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        if (this.mOverridden) {
                            return;
                        }
                        if (swipeDir <= 0) {
                            // this is a drag or failed swipe. recover immediately
                            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.
                                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);
                mRecoverAnimations.add(rv);
                // 3.執(zhí)行動畫
                rv.start();
                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView, prevSelected);
            }
            mSelected = null;
        }
        // ······
    }

??上面的代碼還是比較長,我簡單的將它分為3步枕稀,分別是:

  1. 計算ItemView此時需要移動的距離询刹。
  2. 根據(jù)計算出來的距離,創(chuàng)建動畫萎坷。
  3. 執(zhí)行動畫凹联,讓ItemView回到正確的位置。

??而這三步的具體實現(xiàn)都是比較簡單的哆档,在這里就不過多的解釋了蔽挠。

4.總結(jié)

??到此為止,ItemTouchHelper就差不多了瓜浸,在這里我對ItemTouchHelper做一個簡單的總結(jié)澳淑。

  1. 我們使用ItemTouchHelper時,需要實現(xiàn)一個ItemTouchHelper.Callback類插佛。在這個實現(xiàn)類里面杠巡,我們需要實現(xiàn) 三個方法,分別是:1. getMovementFlags,主要是設(shè)置ItemTouchHelper執(zhí)行那些行為和方向雇寇;2. onMove方法氢拥,表示當(dāng)前有兩個ItemView發(fā)生了交換蚌铜,此時需要我們更新數(shù)據(jù)源;3. onSwiped方法嫩海,表示當(dāng)前有ItemView被側(cè)滑刪除冬殃,也需要我們更新數(shù)據(jù)源。
  2. ItemTouochHelper是通過ItemTouchListener來獲取每個ItemView的事件叁怪,通過GestureDetector來判斷長按行為审葬。
  3. ItemTouchHelper是通過改變ItemViewtranslationXtranslationY屬性值,進(jìn)而改變每個ItemView的位置奕谭。
  4. ItemTouchHelper是通過ChildDrawingOrderCallback接口和Elevation來改變ItemView的繪制順序的涣觉。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市展箱,隨后出現(xiàn)的幾起案子旨枯,更是在濱河造成了極大的恐慌,老刑警劉巖混驰,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件攀隔,死亡現(xiàn)場離奇詭異,居然都是意外死亡栖榨,警方通過查閱死者的電腦和手機(jī)昆汹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婴栽,“玉大人满粗,你說我怎么就攤上這事∮拚” “怎么了映皆?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長轰枝。 經(jīng)常有香客問我捅彻,道長,這世上最難降的妖魔是什么鞍陨? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任步淹,我火速辦了婚禮,結(jié)果婚禮上诚撵,老公的妹妹穿的比我還像新娘缭裆。我一直安慰自己,他們只是感情好寿烟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布澈驼。 她就那樣靜靜地躺著,像睡著了一般筛武。 火紅的嫁衣襯著肌膚如雪盅藻。 梳的紋絲不亂的頭發(fā)上购桑,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音氏淑,去河邊找鬼。 笑死硕噩,一個胖子當(dāng)著我的面吹牛假残,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炉擅,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辉懒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谍失?” 一聲冷哼從身側(cè)響起眶俩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎快鱼,沒想到半個月后颠印,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抹竹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年线罕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窃判。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡钞楼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出袄琳,到底是詐尸還是另有隱情询件,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布唆樊,位于F島的核電站宛琅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏窗轩。R本人自食惡果不足惜夯秃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望痢艺。 院中可真熱鬧仓洼,春花似錦、人聲如沸堤舒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舌缤。三九已至箕戳,卻和暖如春某残,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陵吸。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工玻墅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壮虫。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓澳厢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親囚似。 傳聞我的和親對象是個殘疾皇子剩拢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354