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

經濟上支持我 or 想經過視頻看我是怎麼實現的:git

edu.csdn.net/course/deta…github

概述

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

探探皇帝翻牌子即視感

人人美劇訂閱界面

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

人人影視版特色(需求):app

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

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

  • 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層,你只須要修改一個參數便可,效果如圖:
oop

6層View

正確的姿式

正確的姿式就是:佈局

  • 利用LayoutManager實現卡片層疊佈局,值得注意的是,只layout出界面上可能會看見的那些View。
  • 搭配ItemTouchHelper,它自己實現了拖拽&滑動刪除邏輯,咱們只須要在onChildDraw()中繪製動畫和onSwiped()中處理數據集(循環or刪除)。

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

轉載請標明出處: juejin.cn/post/684490…
juejin.cn/post/684490…
本文出自:【張旭童的稀土掘金】(juejin.cn/user/905653…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…

LayoutManager的實現卡片層疊

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

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

惟一注意事項

可是即使如此,仍是有一個惟一的注意事項。咱們只layout出界面上可能會看見的那些View便可。
由於考慮到動畫,因此是可能會看見
咱們看人人美劇的界面:

底部細節

初始化時,界面上可見三個 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));
                    }
                }
            }
        }
    }

}複製代碼

擼到這裏,咱們的靜態界面已經成型,下面讓咱們動起來:

靜態界面

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

轉載請標明出處:
juejin.cn/post/684490…
本文出自:【張旭童的稀土掘金】(juejin.cn/user/905653…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/Z…

剛建了個QQ搞基交流羣:557266366裏面如今沒有人。嗯,就這樣吧。

相關文章
相關標籤/搜索