RecyclerView 擴展(二) - 手把手教你認識ItemTouchHelper

  今天咱們來學習一下RecyclerView另外一個不爲人知的輔助類--ItemTouchHelper。咱們在作列表視圖,就好比說,ListView或者RecyclerView,一般會有兩種需求:1. 側滑刪除;2. 拖動交換位置。對於第一種需求使用傳統的版本實現還比較簡單,咱們能夠自定義ItemView來實現;而第二種的話,可能就稍微有一點複雜,可能須要重寫LayoutManagergit

  這些辦法也不否定是有效的解決方案,可是是不是簡單和低耦合性的辦法呢?固然不是,踩過坑的同窗應該都知道,不論是自定義View仍是自定義LayoutManager都不是一件簡單的事情,其次,自定義ItemView致使Adapter的通用性下降。這些實現方式都是比較麻煩的。github

  而谷歌爸爸真是貼心,知道咱們都有這種需求,就小手一抖,隨便幫咱們實現了一個Helper類,來減輕咱們的工做量。這就是ItemTouchHelper的做用。bash

  本文打算從兩個方面來教你們認識ItemTouchHelper類:app

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

  本文參考資料:ide

  1. RecyclerView高級進階總結:ItemTouchHelper實現拖拽和側滑刪除
  2. ItemTouchHelper源碼分析

1. 概述

  在正式介紹ItemTouchHelper以前,咱們先來了解ItemTouchHelper是什麼東西。源碼分析

  從ItemTouchHelper的源碼中,咱們能夠看出來,ItemTouchHelper繼承了ItemDecoration,根本上就是一個ItemDecoration。關於ItemDecoration的分析,有興趣的同窗能夠參考個人文章:RecyclerView 擴展(一) - 手把手教你認識ItemDecorationpost

public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener {
}
複製代碼

  至於爲何ItemTouchHelper會繼承ItemDecoration,後面會詳細的解釋,這裏就先賣一下關子。學習

  而後,咱們先來看看ItemTouchHelper實現的效果,讓你們有一個直觀的體驗。fetch

  先是側滑刪除的效果: 動畫

  而後是拖動交換位置:
  本文打算從上面兩種效果來介紹 ItemTouchHelper的使用。

2. ItemTouchHelper的基本使用

  既然是手把手教你們認識ItemTouchHelper,因此天然須要介紹它的的基本使用,如今讓咱們來看看究竟怎麼使用ItemTouchHelper

  在正式介紹ItemTouchHelper的基本使用以前,咱們還必須瞭解一個類--ItemTouchHelper.CallbackItemTouchHelper就是依靠這個類來實現側滑刪除和拖動位置兩種效果的,我來看看它。

(1). ItemTouchHelper.Callback

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

方法名 做用
getMovementFlags 在此方法裏面咱們須要構建兩個flag,一個是dragFlags,表示拖動效果支持的方向,另外一個是swipeFlags,表示側滑效果支持的方向。在咱們的Demo中,拖動執行上下兩個方向,側滑執行左右兩個方向,這些操做咱們均可以在此方法裏面定義。
onMove 當拖動效果已經產生了,會回調此方法。在此方法裏面,咱們一般會更新數據源,就好比說,一個ItemView從0拖到了1位置,那麼對應的數據源也須要更改位置。
onSwiped 當側滑效果以上產生了,會回調此方法。在此方法裏面,咱們也會更新數據源。與onMove方法不一樣到的是,咱們在這個方法裏面從數據源裏面移除相應的數據,而後調用notifyXXX方法就好了。

  對於ItemTouchHelper的基本使用來講,咱們只須要了解這三個方法就已經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) {
        // 交換在數據源中相應數據源的位置
        return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        // 從數據源中移除相應的數據
        mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
    }
}
複製代碼

  而後,咱們在使用RecyclerView時,添加這兩行代碼就好了:

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
複製代碼

  最終的效果就是上面的動圖展現的,是否是以爲很是的簡單呢?接下來,我將正式的分析ItemTouchHelper的源碼。

(4).源碼

  爲了方便你們理解,我將個人代碼上傳到github,有興趣的同窗能夠看看:ItemTouchHelperDemo

3. ItemTouchHelper的源碼分析

  咱們從基本使用中瞭解到,ItemTouchHelper的使用是很是簡單的,因此你們心裏有沒有一種好奇呢?那就是ItemTouchHelper到底是怎麼實現,爲何兩個相對比較複雜的效果,經過幾行代碼就能實現呢?接下來的內容就能找到答案。

(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方法是比較簡單的。這其中,咱們發現ItemTouchHelper是經過ItemTouchListener接口來爲每一個ItemView處理事件,同時,從這裏咱們能夠看出來,在ItemTouchHelper內部還使用了GestureDetector,而這裏GestureDetector的做用主要是來判斷ItemView是否進行了長按行爲。

  ItemTouchHelper的分析重點應該是事件處理,可是在這以前,咱們先來看一個方法,這個方法很是的重要的。

(2). select方法

  當咱們的操做觸發了長按或者側滑的行爲,都會回調此方法,同時當咱們手勢釋放,也會回調此方法。

  因此從大的時機來看,當手勢開始或者釋放都會回調select方法;而每一個大時機又分爲兩個小時機,分別是長按和側滑,分別表示拖動交換位置和側滑刪除操做。

  在正式分析select方法的代碼以前,咱們須要瞭解兩個東西:

  1. selected表示被選中的ViewHolder。其中,selected若是爲null,則表示當前處於手勢(包括長按和側滑)釋放時機;反之,selected不爲null,則表示當前處於手勢開始的時機。
  2. actionState表示當前的狀態,一共有三個值可選,分別是:1. ACTION_STATE_IDLE表示沒有任何手勢,此時selected對應的應當是null;2. ACTION_STATE_SWIPE表示當前ItemView處於側滑狀態;3. ACTION_STATE_DRAG表示當前ItemView處於拖動狀態。在ItemTouchHelper內部,就是經過這三個狀態來判斷ItemView處於什麼狀態。

  接下來咱們來看看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;
        // 若是當前是拖動行爲,給RecyclerView設置一個ChildDrawingOrderCallback接口
        // 主要是爲了調整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();
    }
複製代碼

  從上面的代碼中,咱們能夠總結出來幾個結論:

  1. 若是處於手勢開始階段,即selected不爲null,那麼會經過getAbsoluteMovementFlags方法來獲取執行咱們設置的flag,從而就知道執行哪些行爲(側滑或者拖動)和方向(上、下、左和右)。同時還會記錄下被選中ItemView的位置。簡而言之,就是一些變量的初始化。
  2. 若是處於手勢釋放階段,即selected爲null,同時mSelected不爲null,那麼此時須要作的事情就稍微有一點複雜。手勢釋放以後,須要作的事情無非有兩件:1. 相關的ItemView到正確的位置,就好比說,若是滑動條件不知足,那麼就返回原來的位置,這個就是一個動畫;2. 清理操做,好比說將mSelected重置爲null之類的

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

  咱們知道,一旦調用selected就意味着一個ItemView被選中,接下來的就會隨着手勢出現側滑或者拖動的效果了。可是怎麼來判斷一個ItemView是否被選中,咱們從代碼來看看,咱們分兩步來理解:1.側滑的選中;2. 拖動的選中。

A. 側滑

  判斷側滑行爲是否選中主要在checkSelectForSwipe方法,咱們來看看checkSelectForSwipe放大的代碼:

boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
        // 若是mSelected不爲null表示已經有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沒有支持側滑的方向值,那麼返回爲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;
        }
        // 這裏主要是判斷一個滑動是否符合側滑的條件
        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);
        // 表示當前ItemView被側滑行爲選中
        select(vh, ACTION_STATE_SWIPE);
        return true;
    }
複製代碼

  checkSelectForSwipe方法的代碼相對來講比較長,可是無非就是判斷當前ItemView是否符合側滑行爲,若是到最後符合的話,那麼就會調用select方法來初始化一些值。   同時,咱們看一下checkSelectForSwipe方法的調用時機只有兩個地方:

  1. onTouchEvent方法
  2. onInterceptTouchEvent方法

  調用的時機也是比較正確的,至於爲何須要兩個地方來調用這個方法,我也不太清楚,估計作什麼保險操做吧。

B. 拖動選中

  拖動選中的時機比較簡單,由於拖動觸發的前提是長按ItemView,因此咱們直接在ItemTouchHelperGestureListeneronLongPress方法找到相關代碼:

@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);
                        }
                    }
                }
            }
        }
複製代碼

  這段代碼表達的意思很是簡單,這裏我就很少餘的解釋了。從這裏能夠看出來,最終仍是調用了select方法表示選中一個ItemView

(3). ItemView隨着手指滑動

  咱們知道了ItemTouchHelper怎麼進行手勢判斷來選中一個ItemView,選中以後的操做就是ItemView隨着手指滑動,咱們來看看ItemView是怎麼實現的。

  咱們知道,隨着手指的滑動,onTouchEvent方法會被調用,咱們來看看相關的代碼:

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自己有大量的數據,一屏顯示不完,此時若是拖動一個ItemView達到RecyclerView的底部或者頂部,會滑動RecyclerView
  4. 更新被選中的ItemView的位置。代碼體如今mRecyclerView.invalidate()

  其中,更新mDxmDy的值是經過updateDxDy方法來實現的,而updateDxDy方法方法比較簡單,這裏就不展開了。

  咱們再來看看第二步,移動其餘ItemView的位置主要是經過moveIfNecessary方法實現的。咱們來看看具體的代碼:

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.回調Callback裏面的onMove方法,這個方法須要咱們手動實現
        if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
            // 保證target的可見
            // keep target visible
            mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                    target, toPosition, x, y);
        }
    }
複製代碼

  如上就是moveIfNecessary方法的代碼,這裏講它分爲3步:

  1. 調用findSwapTarget方法,尋找可能會跟選中的ItemView交換位置的ItemView。這裏判斷的條件是隻要選中的ItemView跟某一個ItemView重疊,那麼這個ItemView可能會跟選中的ItemView交換位置。
  2. 調用Callback的chooseDropTarget方法來找到符合交換條件的ItemView。這裏符合的條件是指,選中的ItemViewbottom大於目標ItemViewbottom或者ItemViewtop大於目標ItemViewtop。一般來講,咱們能夠重寫chooseDropTarget方法,來定義什麼條件下就交換位置。
  3. 回調CallbackonMove方法,這個方法須要咱們本身實現。這裏須要注意的是,若是onMove方法返回爲true的話,會調用Callback另外一個onMove方法來保證target可見。爲何必須保證target可見呢?從官方文檔上來看的話,若是target不可見,在某些滑動的情形下,target會被remove掉(回收掉),從而致使drag過早的中止。

  關於ItemTouchHelper是怎麼來選擇交換位置的ItemView,重點就在findSwapTarget方法和chooseDropTarget方法。其中findSwapTarget方法是找到可能會交換位置的ItemViewchooseDropTarget方法是找到會交換位置的ItemView,這是兩個方法的不一樣點。同時,若是此時在拖動,可是拖動的ItemView還未達到交換條件,也就是跟另外一個ItemView只是重疊了一小部分,這種狀況下,findSwapTargets方法返回的集合不爲空,可是chooseDropTarget方法尋找的ItemView爲空。

  而後就是第三步,第三步的做用是當ItemView拖動到邊緣,若是此時RecyclerView能夠滑動,那麼RecyclerView會滾動。具體的實現是在mScrollRunnablerun方法調用:

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()的執行。這裏須要理解的是,爲何經過invalidate方法就能更新ItemView的位置呢?由於ItemView在隨着手指移動時,變化的是translationXtranslationY兩個屬性,因此只須要調用invalidate方法就行。調用invalidate方法以後,至關於RecyclerView會從新繪製一次,那麼全部ItemDecorationonDrawonDrawOver方法都會被調用,而剛好的是,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方法裏面,調用了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);
            }
        }
複製代碼

  代碼仍是比較長,可是表示的意思是很是簡單的。就是調用onChildDraw方法,將全部正在交換位置的ItemView和被選中的ItemView做爲參數傳遞過去。

  而在onChildDraw方法裏面,調用了ItemTouchUIUtilonDraw方法。咱們從ItemTouchUiUtil的實現類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,從而實現了ItemView隨着手指移動的效果。

  從這裏,咱們能夠看出來,一旦調用RecyclerViewinvalidate方法,ItemTouchHelperonDraw方法和onDrawOver方法都會被執行。這個可能就是ItemTouchHelper繼承ItemDecoration的緣由吧。

(4).爲何拖動的ItemView始終在其餘ItemView的上面?

  當咱們在上下拖動的時候,咱們發現一個問題,就是拖動的ItemView始終在其餘ItemView的上面。這裏,咱們不由疑惑,咱們都知道,在ViewGroup裏面,全部的child都有繪製順序。一般來講,先添加的child先繪製,後添加的child後繪製,在RecyclerView中也是不例外,上面的ItemView先繪製,而下面的ItemView後繪製。而在這個拖動效果中,爲何不符合這個規則呢?咱們來看看ItemTouchHelper是怎麼幫忙實現的。

  答案得分爲兩個種狀況,一種是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);
    }
複製代碼

  實現的原理就是給RecyclerView設置了一個ChildDrawingOrderCallback接口來改變child的繪製順序,這樣能保證被選中的ItemView後於重疊的ItemView繪製,這樣就實現了被選中的ItemView始終在上面。

  不過使用ChildDrawingOrderCallback接口時,咱們須要注意的是:要想是接口有效,必須保證全部childelevation是同樣的,若是不同,那麼elevation優先級更高

  從上面的注意點,咱們應該都知道Api大於等於21時,使用的是什麼方式來實現的吧。沒錯就是經過改變 ItemViewelevation值實現的。咱們來看看具體實現,在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,因此當Api小於21時,調用setElevation是無效的。如上就是Api大於等於21時實現被選中的ItemView在全部ItemView上面的代碼。

(5). 手勢釋放以後

  不論是拖動仍是側滑,當咱們手勢釋放以後,作的操做無非兩種:1. 回到原位;2.移動到正確的位置。那這部分的具體實如今哪裏呢?沒錯,就在咱們以前分析過的select方法裏面,此時看select方法代碼時,咱們需得注意兩個點:

  1. 此時,參數selected爲null。
  2. 此時,變量mSelected不爲null。

  而後,咱們在來看看相關代碼:

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.建立動畫
                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.執行動畫
                rv.start();
                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView, prevSelected);
            }
            mSelected = null;
        }
        // ······
    }
複製代碼

  上面的代碼仍是比較長,我簡單的將它分爲3步,分別是:

  1. 計算ItemView此時須要移動的距離。
  2. 根據計算出來的距離,建立動畫。
  3. 執行動畫,讓ItemView回到正確的位置。

  而這三步的具體實現都是比較簡單的,在這裏就不過多的解釋了。

4.總結

  到此爲止,ItemTouchHelper就差很少了,在這裏我對ItemTouchHelper作一個簡單的總結。

  1. 咱們使用ItemTouchHelper時,須要實現一個ItemTouchHelper.Callback類。在這個實現類裏面,咱們須要實現 三個方法,分別是:1. getMovementFlags,主要是設置ItemTouchHelper執行那些行爲和方向;2. onMove方法,表示當前有兩個ItemView發生了交換,此時須要咱們更新數據源;3. onSwiped方法,表示當前有ItemView被側滑刪除,也須要咱們更新數據源。
  2. ItemTouochHelper是經過ItemTouchListener來獲取每一個ItemView的事件,經過GestureDetector來判斷長按行爲。
  3. ItemTouchHelper是經過改變ItemViewtranslationXtranslationY屬性值,進而改變每一個ItemView的位置。
  4. ItemTouchHelper是經過ChildDrawingOrderCallback接口和Elevation來改變ItemView的繪製順序的。
相關文章
相關標籤/搜索