RecyclerView 擴展(三) - 使用ItemTouchHelper和LayoutManager實現滑動卡片效果

  最近樓主在忙碌於本身的畢設項目,在畢設當中須要實現一個滑動卡片的效果,樓主花了一點時間本身實現了一下,使用是ItemTouchHelperLayoutManager方式實現的。咱們先來看一下效果: git

  上面的效果說難也不難,說不難呢,可是這裏面又有不少的小細節須要注意。   有人說,這動畫很好作啊,使用 ViewPager就能夠實現了,這是沒錯的,可是 ViewPager一直有一個詬病--那就是 View的複用性不高。考慮到性能, RecyclerView天然是當之無愧的王者,既然咱們學過 RecyclerView,爲何不嘗試着實現的呢?

1. 效果分析

  看着這個動畫麻煩,其實咱們將它分爲兩個部分實現就很是簡單了。首先,每一個ItemView是疊加樣式展示的,這個效果在咱們經常使用到的LayoutManger沒有這種樣式,因此得須要咱們自定義一個LayoutManager來實現一個這種樣式。這是其一。   其二,滑動切換的效果怎麼實現呢?還記得咱們以前分析過ItemTouchHelper這個類嗎?這個類的做用是用來實現側滑刪除以及長按拖動的效果的,而這裏切換卡片的效果就至關於側滑刪除,只不過是側滑時作的動畫不同。這裏的動畫主要包括卡片的位移和角度變化,而ItemTouchHelper怎麼實現根據手指滑動來作相應的動畫呢?答案就在onChildDraw方法裏面。   其實,咱們從ItemTouchHelperonChildDraw方法裏面就知道,原生只是作了水平位置的變化,因此,咱們能夠重寫這個方法,從而加上咱們想要的動畫。   這樣來分析,這個動畫是否是很是簡單呢?接下來,咱們從看看代碼吧。github

2. LayoutManager

  自定義LayoutManager的相關知識,我在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章裏面已經詳細的解釋了,這裏我就不重複了。咱們直接來看代碼吧,關鍵代碼在於onLayoutChildren方法裏面:bash

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        final int layoutCount = Math.min(getItemCount(), mMaxVisibleCount);
        detachAndScrapAttachedViews(recycler);
        for (int i = layoutCount - 1; i >= 0; i--) {
            final View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
            int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
            layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
                    widthSpace / 2 + getDecoratedMeasuredWidth(view),
                    heightSpace / 2 + getDecoratedMeasuredHeight(view));
            // 給每一個ItemView設置scale
            view.setScaleX((float) Math.pow(DEFAULT_SCALE, i));
            view.setScaleY((float) Math.pow(DEFAULT_SCALE, i));
            if (i == 0) {
                view.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v);
                        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                            // 這裏須要手動告訴ItemTouchHelper能夠側滑
                            mItemTouchHelper.startSwipe(childViewHolder);
                        }
                        return false;
                    }
                });
            } else {
                // 因爲ItemView會複用,因此必定要設置null
                view.setOnTouchListener(null);
            }
        }
    }
複製代碼

  相信上面的代碼你們都能看的懂,這裏我就不逐行的解釋了。可是有一點須要咱們特別注意:ide

for (int i = layoutCount - 1; i >= 0; i--) {
          // ······
        }
複製代碼

  這裏咱們是倒着添加View,也就是一個ItemView雖然在RecyclerView的內部index爲0,可是在Adapter中,倒是layoutCount - 1,這個在咱們自定義ItemTouchHelper.Callback時,會有很大的做用。源碼分析

3.ItemTouchHelper.Callback

  關於ItemTouchHelper的知識,我在RecyclerView 擴展(二) - 手把手教你認識ItemTouchHelper文章裏面已經詳細的解釋過了,因此在這裏我也不重複了。咱們直接來看實現代碼,關鍵在onChildDraw方法:性能

@Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        // 跟着手指移動
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        final View itemView = viewHolder.itemView;
        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            float ratio = dX / getThreshold(recyclerView, viewHolder);
            if (ratio > 1) {
                ratio = 1;
            } else if (ratio < -1) {
                ratio = -1;
            }
            // 跟着角度旋轉
            itemView.setRotation(ratio * 15);
            for (int i = 0; i < mMaxVisibleCount - 1; i++) {
                // 下面的ItemView跟着手指縮放
                View child = recyclerView.getChildAt(i);
                final float currentScale = (float) Math.pow(DEFAULT_SCALE, 2 - i);
                final float nextScale = currentScale / DEFAULT_SCALE;
                final float scale = (nextScale - currentScale);
                child.setScaleX(Math.min(1, currentScale + scale * Math.abs(ratio)));
                child.setScaleY(Math.min(1, currentScale + scale * Math.abs(ratio)));
            }
        }
    }
複製代碼

  上面代碼的做用我在註釋已經解釋比較清楚了,這裏就不解釋了。不過這裏還須要一點:動畫

for (int i = 0; i < mMaxVisibleCount - 1; i++) {
                 // ······
            }
複製代碼

  這裏我縮放的也是0 ~ mMaxVisibleCount - 1的ItemView,請記住,這個不是ItemViewAdapter中的position,而是ItemViewRecyclerView內部的index值。在前面的LayoutManager中,我已經解釋過,這倆是反着的。因此這裏應該是0 ~ mMaxVisibleCount - 1。   整個實現就是這麼的簡單,其實還有坑沒有說,好比說:ui

@Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        viewHolder.itemView.setRotation(0f);
    }
複製代碼

  在clearView方法裏面必須進行重置,由於ItemView是複用的,不重置的話會出問題的。   在好比說,必須重寫isItemViewSwipeEnabled方法(雖然不重寫也沒有問題,可是官方文檔建議重寫):spa

@Override
    public boolean isItemViewSwipeEnabled() {
        return false;
    }
複製代碼

4. 跟SwipeRefreshLayout事件衝突

  使用上面代碼來實現效果以後,咱們會發現一個問題,若是將RecyclerView放在SwipeRefreshLayout內部,會出現事件衝突。   我簡單的描述一下事件衝突的狀況:當咱們左右滑動時,這是正常的,每一個ItemView都是正常的側換;可是一旦上下滑動時,正常來講應該是SwipeRefreshLayout滑動,可是實際上仍是ItemView在側滑。   關於解決方案的話,我有兩種方案:1. 重寫SwipeRefreshLayoutonInterceptTouchEvent方法,進行事件攔截,讓事件不能傳遞到ItemView中;2. 取消手動調用ItemTouchHelperstartSwipe方法,讓ItemTouchHelper本身來判斷是否符合側滑的條件。   這裏,我特別的說明一下第一種方法。爲何要特別說明第一種方法呢?由於此方法有很大的問題:1. 會重寫SwipeRefreshLayout,這個形成了沒必要要的工做,這是其一;2. 重寫了SwipeRefreshLayout會破壞SwipeRefreshLayout的結構,這個纔是最大的缺點。   爲何重寫SwipeRefreshLayout會破壞它的結構呢?咱們能夠從SwipeRefreshLayout的源碼看出來,SwipeRefreshLayout不會主動的攔截事件,由於SwipeRefreshLayout是經過嵌套滑動機制來實現滑動,若是咱們在onInterceptTouchEvent方法裏面進行事件攔截,就違背了SwipeRefreshLayout的設計。因此,第一種方法是特別不推薦的!!!   其次,咱們來看看第二種方案的實現方式,第二種方案很是簡單,歸根結底就是兩句話:設計

  1. Callback裏面不要重寫isItemViewSwipeEnabled方法,
  2. LayoutManager裏面不要在每一個ItemViewOnTouchListener裏面調用ItemTouchHelperstartSwipe方法。

  我在這裏簡單的解釋第二種方式爲何這樣作就不會衝突了,不過要了解爲何不衝突,必須得了解之前爲何會衝突。   SwipeRefreshLayout自己不會攔截事件,因此全部的事件均可以傳遞到RecyclerView裏面的每一個ItemView裏面。由於咱們在OnTouchListener調用ItemTouchHelperstartSwipe表示選中了一個ItemView能夠側滑,從而致使後面事件都會被該ItemView消費,進而致使了事件衝突。   而取消startSwipe方法的調用,讓ItemTouchHelper本身來選中一個能夠側滑的ItemView,ItemTouchHelper自己就處理了上下滑和左右滑的衝突的(若是沒有處理,RecyclerView的上下滑跟ItemView的側滑會衝突)。這就是第二種方式的原理。

5. 源碼

  爲了方便你們的理解,我將本身的Demo代碼上傳到github,供你們參考:SlideCardDemo

相關文章
相關標籤/搜索