五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面 簡單&優雅:LayoutManager+ItemTouchHelper

mcxtzhang發表於2016-12-19

經濟上支援我 or 想通過視訊看我是怎麼實現的:

edu.csdn.net/course/deta…

概述

前幾天看有人實現了仿人人美劇的訂閱介面,不過在細節之處以及實現方式我個人認為都不是最佳的姿勢。
於是我也動手擼了一個,還順帶擼了個探探的介面,先看GIF:

五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面  簡單&優雅:LayoutManager+ItemTouchHelper
探探皇帝翻牌子即視感

五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面  簡單&優雅:LayoutManager+ItemTouchHelper
人人美劇訂閱介面

這裡吐個槽,探探這種設計真的像皇帝翻牌子的感覺,不喜歡左滑,喜歡右滑。

人人影視版特點(需求):

  • 動畫:最多可見的這四層,在頂層卡片滑動時,每一層都會位移&放大動畫,有種補充到頂層的感覺
  • 動畫:鬆手時,如果未被判定為刪除,則會有頂層以下每一層卡片收縮回原位的動畫。
  • 無限迴圈:模仿人人影視,頂層卡片被刪除後,補充到最底層。

除上述動畫特點,探探版特點(需求):

  • Roate的變化:左右滑動時,頂層卡片會慢慢旋轉,到閾值max大概十五度。
  • Alpha的變化:左滑時頂層卡片的刪除按鈕會慢慢顯現,右滑時愛心按鈕會慢慢顯現
  • 顯然,鬆手時,以上動畫也需要復位。

我們的效果,基本上和原版一致了,寫起來怎麼樣呢?
我不是標題黨,如標題所說:

  • 簡單:思路簡單清晰易理解
  • 優雅:效能沒有任何隱患,LayoutManager只會載入顯示螢幕上可見的數量的View
  • 快速:利用ItemTouchHelper處理拖拽&滑動刪除邏輯,核心程式碼不超過50行。且經過封裝,四行程式碼就可以用。

伸手黨福利:

如果懶得看這麼多文字只想用,直接移步gayhub,gradle匯入相關檔案or複製。然後如下,搞定。

        mRv.setLayoutManager(new OverLayCardLayoutManager());
        CardConfig.initConfig(this);
        ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
        itemTouchHelper.attachToRecyclerView(mRv);複製程式碼

而且我將一些參
數都以變數形式計算,這樣就做到了可配置,假如老闆讓你一開始多顯示幾層卡片,例如6層,你只需要修改一個引數即可,效果如圖:

五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面  簡單&優雅:LayoutManager+ItemTouchHelper
6層View

正確的姿勢

正確的姿勢就是:

  • 利用LayoutManager實現卡片層疊佈局,值得注意的是,只layout出介面上可能會看見的那些View。
  • 搭配ItemTouchHelper,它本身實現了拖拽&滑動刪除邏輯,我們只需要在onChildDraw()中繪製動畫和onSwiped()中處理資料集(迴圈or刪除)。

所以本文也算是填了LayoutManger系列的坑,實現了一個酷炫效果的佈局。
Let's Go!

轉載請標明出處: gold.xitu.io/post/585682…
gold.xitu.io/post/585682…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…

LayoutManager的實現卡片層疊

其實本例中的LayoutManager十分簡單,因為ItemTouchHelper的存在,LayoutManager根本不需要處理它的滑動事件,而LayoutManager中最難寫的就是在滑動時的View回收和複用,以及layoutView的處理。

關於LayoutManager的基礎知識和鋪墊,我就不再贅述,可參考我以前的文章:LayoutManager實現流式佈局

唯一注意事項

但是即便如此,還是有一個唯一的注意事項。我們只layout出介面上可能會看見的那些View即可。
因為考慮到動畫,所以是可能會看見
我們看人人美劇的介面:

五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面  簡單&優雅:LayoutManager+ItemTouchHelper
底部細節

初始化時,介面上可見三個View,我們分別起名:TopView,Top-1View,Top-2View。其中TopView完全可見,Top-1View,Top-2View只有下邊緣可見。

如文首GIF,滑動TopView時,Top-1View,Top-2View開始慢慢放大,並且向上位移,直至填充至它們各自上層的View。這時候露出了Top-3View

所以我們在書寫LayoutManageronLayoutChildren()方法時,只要layout出當前資料集最後四個View即可。

前文提到的引數配置如下:
包括一些配置

  • 介面最多顯示幾個View
  • 每一級View之間的Scale差異、translationY等等

    public class CardConfig {
     //螢幕上最多同時顯示幾個Item
     public static int MAX_SHOW_COUNT;
     //每一級Scale相差0.05f,translationY相差7dp左右
     public static float SCALE_GAP;
     public static int TRANS_Y_GAP;
    
     public static void initConfig(Context context) {
         MAX_SHOW_COUNT = 6;
         SCALE_GAP = 0.05f;
         TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, context.getResources().getDisplayMetrics());
     }
    }複製程式碼

LayoutManager全部程式碼如下,佈滿註釋,如果看不懂,建議閱讀前置文章LayoutManger系列

public class OverLayCardLayoutManager extends RecyclerView.LayoutManager {
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        int itemCount = getItemCount();
        if (itemCount >= MAX_SHOW_COUNT) {
            //從可見的最底層View開始layout,依次層疊上去
            for (int position = itemCount - MAX_SHOW_COUNT; position < itemCount; position++) {
                View view = recycler.getViewForPosition(position);
                addView(view);
                measureChildWithMargins(view, 0, 0);
                int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
                int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
                //我們在佈局時,將childView居中處理,這裡也可以改為只水平居中
                layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
                        widthSpace / 2 + getDecoratedMeasuredWidth(view),
                        heightSpace / 2 + getDecoratedMeasuredHeight(view));
                /**
                 * TopView的Scale 為1,translationY 0
                 * 每一級Scale相差0.05f,translationY相差7dp左右
                 *
                 * 觀察人人影視的UI,拖動時,topView被拖動,Scale不變,一直為1.
                 * top-1View 的Scale慢慢變化至1,translation也慢慢恢復0
                 * top-2View的Scale慢慢變化至 top-1View的Scale,translation 也慢慢變化只top-1View的translation
                 * top-3View的Scale要變化,translation巋然不動
                 */

                //第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
                int level = itemCount - position - 1;
                //除了頂層不需要縮小和位移
                if (level > 0 /*&& level < mShowCount - 1*/) {
                    //每一層都需要X方向的縮小
                    view.setScaleX(1 - SCALE_GAP * level);
                    //前N層,依次向下位移和Y方向的縮小
                    if (level < MAX_SHOW_COUNT - 1) {
                        view.setTranslationY(TRANS_Y_GAP * level);
                        view.setScaleY(1 - SCALE_GAP * level);
                    } else {//第N層在 向下位移和Y方向的縮小的成都與 N-1層保持一致
                        view.setTranslationY(TRANS_Y_GAP * (level - 1));
                        view.setScaleY(1 - SCALE_GAP * (level - 1));
                    }
                }
            }
        }
    }

}複製程式碼

擼到這裡,我們的靜態介面已經成型,下面讓我們動起來:

五行程式碼實現 炫動滑動 卡片層疊佈局,仿探探、人人影視訂閱介面  簡單&優雅:LayoutManager+ItemTouchHelper
靜態介面

ItemTouchHelper實現炫動滑動:

ItemTouchHelper的基礎知識,建議大家自行學習,網上文章很多,我簡單介紹一下,

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
It works with a RecyclerView and a Callback class, which configures what type of interactions
are enabled and also receives events when user performs these actions.
Depending on which functionality you support, you should override
{@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
{@link Callback#onSwiped(ViewHolder, int)}.

翻譯 + 總結:

這貨是一個工具類,為RecyclerView擴充套件滑動消失(刪除)和drag & drop效果的。
它需要和RecyclerView、Callback 一起工作。Callback 類裡定義了 允許哪些互動,並且會接收到對應的互動事件
根據你需要哪種功能(滑動消失(刪除)和drag & drop),你需要重寫
Callback#onMove(RecyclerView, ViewHolder, ViewHolder)-----drag & drop
Callback#onSwiped(ViewHolder, int) 方法。 -----滑動消失(刪除)

總結一下入門級用法如下,三個步驟:

  • 定義一個Callback:ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(int,int),這兩個int分別代表要 監聽哪幾個方向上的拖拽、滑動事件。 常用:ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT

  • 將Callback傳給ItemTouchHelper:ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);

  • 關聯ItemTouchHelper和RecyclerView:itemTouchHelper.attachToRecyclerView(mRv)

這三個步驟做完後,ItemTouchHelper就會自動幫我們完成 滑動消失(刪除)和drag & drop 的功能。

滑動刪除

我們本例中,需要的是滑動消失(刪除) ,所以我們的Callback不需要關注onMove()方法。
且我們需要上下左右滑動都可以刪除的效果。
則如下構造Callback,傳入上下左右:

ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
                ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)複製程式碼

onSwiped()方法,是滑動刪除動作已經發生後回撥的,即,我們先滑動卡片,然後鬆手,此時ItemTouchHelper判斷我們的手勢是刪除手勢,會自動對這個卡片執行丟出螢幕外的動畫,同時回撥onSwiped()方法。
所以我們需要在其中如下寫:

            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
                //★實現迴圈的要點
                SwipeCardBean remove = mDatas.remove(viewHolder.getLayoutPosition());
                mDatas.add(0, remove);
                mAdapter.notifyDataSetChanged();
            }複製程式碼

在這裡我們完成了迴圈的操作:

  • 利用當前被刪除的ViewViewHolder拿到Position
  • 刪除資料集中對應Position的資料來源
  • 同時將該資料來源插入資料集中的首位。
  • 呼叫notifyDataSetChanged(),通知列表重新整理
    如此我們便完成了,迴圈列表的需求

這裡提一下為什麼我們要呼叫notifyDataSetChanged()
看官方文件:

ItemTouchHelper moves the items' translateX/Y properties to reposition them

即ItemTouchHelper實現的滑動刪除,其實只是隱藏了這個滑動的View。並不是真的刪除了。

LayoutManager實現流式佈局一文第五節中,我們已經提到,notifyDataSetChanged()會回撥onLayoutChildren()這個函式,而在這個函式中,我們會重新佈局,即真正的移除(不再layout)滑動掉的View,同時會補充進新的最底層的View

嗯,JavaBean也看一眼吧,沒亮點:

public class SwipeCardBean {
    private int postition;//位置
    private String url;
    private String name;
    }複製程式碼

我們寫到這裡已經完成了滑動刪除的功能,其實我們什麼都沒有寫是吧,複雜的判斷都由ItemTouchHelper幫我們處理掉了,例如速度、滑動距離是否到達刪除閾值,刪除成功移除的動畫、取消刪除復位的動畫等等。
所以我說利用ItemTouchHelper才是正確的姿勢,因為很簡單&快速。
下面我們來實現滑動時的動畫。

滑動時動畫

我們需要重寫CallbackonChildDraw()方法,這個方法引數較多:

     * @param c                 The canvas which RecyclerView is drawing its children

     * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to

     * @param viewHolder        The ViewHolder which is being interacted by the User or it was
                                interacted and simply animating to its original position

     * @param dX                The amount of horizontal displacement caused by user's action

     * @param dY                The amount of vertical displacement caused by user's action

     * @param actionState       是拖拽還是滑動事件  The type of interaction on the View. Is either {@link #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.  

     * @param isCurrentlyActive 事件是使用者產生還是動畫產生的 True if this view is currently being controlled by the user or false it is simply animating back to its original state. 複製程式碼

對我們比較有用的有dX dX,可以判斷滑動方向,以及計算滑動的比例,從而控制縮放、位移動畫的程度

本文如下編寫,對View的縮放、位移,其實是對LayoutManager裡的操作的逆操作,值得注意的是最後一層,即top-3View在Y軸上是保持不變的:

@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);
                //先根據滑動的dxdy 算出現在動畫的比例係數fraction
                double swipValue = Math.sqrt(dX * dX + dY * dY);
                double fraction = swipValue / getThreshold(viewHolder);
                //邊界修正 最大為1
                if (fraction > 1) {
                    fraction = 1;
                }
                //對每個ChildView進行縮放 位移
                int childCount = recyclerView.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    View child = recyclerView.getChildAt(i);
                    //第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
                    int level = childCount - i - 1;
                    if (level > 0) {
                        child.setScaleX((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));

                        if (level < MAX_SHOW_COUNT - 1) {
                            child.setScaleY((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));
                            child.setTranslationY((float) (TRANS_Y_GAP * level - fraction * TRANS_Y_GAP));
                        }
                    }
                }
            }複製程式碼

getThreshold(viewHolder)函式,返回是否可以被回收掉的閾值,關於它為什麼這麼寫,我是從原始碼裡找到的,本末會講解:

            //水平方向是否可以被回收掉的閾值
            public float getThreshold(RecyclerView.ViewHolder viewHolder) {
                return mRv.getWidth() * getSwipeThreshold(viewHolder);
            }複製程式碼

探探效果的實現

一開始文章擼到這裡應該結束了,群裡出來一個馬小跳,告訴我探探和這略有不同,希望我一併實現。
嗯,好吧。表示沒聽說過探探,那我先去下載一個看看吧。
loading-install-open........
哎喲呵,十分鐘過去了,我還在滑動看美女 忘記了要幹什麼,被女票看到胖揍了我一頓。
好的,我捂著臉繼續分析。

探探和人人影視有兩點不同:

  • Roate的變化:左右滑動時,頂層卡片會慢慢旋轉,到閾值max大概十五度。
  • Alpha的變化:左滑時頂層卡片的刪除按鈕會慢慢顯現,右滑時愛心按鈕會慢慢顯現

感覺也是炒雞簡單,來吧。五分鐘擼完吃外賣。修改點:

  • 在layout佈局新增『 X 』&『 愛心 』。
  • onChildDraw()裡,按比例修改TopView的Rotate & Alpha

監聽方向

還有一點小不同,上滑下滑不再能刪除,所以我們構造時只傳入左右即可:

ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)複製程式碼

佈局新增兩個按鈕

onChildDraw()

在上文人人影視的基礎上擴充套件,上文的效果,對TopView是不做任何操作的。這裡只需要再對TopView做額外操作即可:

            @Override
            public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
                ...
                for (int i = 0; i < childCount; i++) {
                    View child = recyclerView.getChildAt(i);
                    //第幾層,舉例子,count =7, 最後一個TopView(6)是第0層,
                    int level = childCount - i - 1;
                    if (level > 0) {
                        ...
                    } else {
                        //探探只是第一層加了rotate & alpha的操作
                        //不過他區分左右
                        float xFraction = dX / getThreshold(viewHolder);
                        //邊界修正 最大為1
                        if (xFraction > 1) {
                            xFraction = 1;
                        } else if (xFraction < -1) {
                            xFraction = -1;
                        }
                        //rotate
                        child.setRotation(xFraction * MAX_ROTATION);

                        //自己感受一下吧 Alpha
                        if (viewHolder instanceof ViewHolder) {
                            ViewHolder holder = (ViewHolder) viewHolder;
                            if (dX > 0) {
                                //露出左邊,比心
                                holder.setAlpha(R.id.iv_love, xFraction);
                            } else {
                                //露出右邊,滾犢子
                                holder.setAlpha(R.id.iv_del, -xFraction);
                            }
                        }
                    }
                }
            }複製程式碼

實現完後,我以為結束了,結果比我們想象的還要複雜一丟丟。因為此時刪除後,notifyDataSetChanged()重新整理介面,而TopView還是傾斜的,愛心、刪除圖示也是出現的。這顯然與預期不符。所以我們需要在onSwiped()裡將其復位:

            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
            ...
                //探探只是第一層加了rotate & alpha的操作
                //對rotate進行復位
                viewHolder.itemView.setRotation(0);
                //自己感受一下吧 Alpha
                if (viewHolder instanceof ViewHolder) {
                    ViewHolder holder = (ViewHolder) viewHolder;
                    holder.setAlpha(R.id.iv_love, 0);
                    holder.setAlpha(R.id.iv_del, 0);
                }
            }複製程式碼

Ok,大功告成。效果和文首一樣,盡情去跟產品UI嘚瑟吧。

閾值的尋找之路

閾值的尋找,花費了我一些時間,因為我想做到和系統的行為保持一致
即,當刪除、喜歡圖示全顯,當Top-1View顯示完畢時,鬆手 TopView會回收。
這就決定了我們的縮放、位移的閾值不能隨便定,所以我們必須去原始碼裡找答案

    //水平方向是否可以被回收掉的閾值
    public float getThreshold(RecyclerView.ViewHolder viewHolder) {
        return mRv.getWidth() * getSwipeThreshold(viewHolder);
    }複製程式碼

因為滑動刪除操作是touch事件導致的,且應該是ACTION_UP時,觸發的,
所以在ItemTouchHelper 原始碼裡,搜尋onTouch字樣:
定位到:mOnItemTouchListener,->
繼續定位其中的onTouchEvent(),->
case MotionEvent.ACTION_UP:,->
void select(ViewHolder selected, int actionState)->
在這裡我注意到有一句程式碼:animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
這說明刪除成功,它的觸發條件是:if (swipeDir > 0)->
swipeDir的值: final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 : swipeIfNecessary(prevSelected); ->
int swipeIfNecessary(ViewHolder viewHolder)->

 if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
                return swipeDir;
  }複製程式碼

如此返回1的話,則->checkHorizontalSwipe(viewHolder, flags)->
在其中終於找到原始碼裡閾值的獲取之處:

final float threshold = mRecyclerView.getWidth() * mCallback
        .getSwipeThreshold(viewHolder);複製程式碼

於是我就直接複製出來。

總結

程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…

想經濟上支援我 or 想通過視訊看我是怎麼實現的:
edu.csdn.net/course/deta…

本文利用LayoutManager載入顯示螢幕上可見的數量的View,搭配ItemTouchHelper處理拖拽&滑動刪除邏輯,核心程式碼不超過50行。且經過封裝,四行程式碼就可以用。

記住LayoutManager,我們寫,只layout出介面上可能會看見的那些View即可。

關於ItemTouchHelper,它本身實現了拖拽&滑動刪除邏輯,我們只需要在onChildDraw()中繪製動畫和onSwiped()中處理資料集(迴圈or刪除)即可。

以後老闆讓你做這種效果,你只需要:

        CardConfig.initConfig(this);
        ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
        itemTouchHelper.attachToRecyclerView(mRv);複製程式碼

如果需要定製特殊的引數,例如顯示6層:

         CardConfig.MAX_SHOW_COUNT = 6;複製程式碼

轉載請標明出處:
gold.xitu.io/post/585682…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
程式碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…

剛建了個QQ搞基交流群:
557266366
裡面現在沒有人。
嗯,就這樣吧。

相關文章