自定義RecyclerView.LayoutManager之卡片式滑動

前言

之前在Github上看見了一個卡片式滑動的效果,非常的炫酷,當時就想著怎么去實現,剛開始我的構思是自定義一個ViewGroup,但通過自定義ViewGroup實現起來會非常復雜,要對子View位置進行擺放、重寫onTouchEvent結合Scroller進行拖拽、對子View進行動畫設置、最后還要對子View進行復用,然后我就想有沒有其他的實現方式,首先考慮到了RecyclerView,因為RecyclerView.LayoutManager可以對子View位置進行設置,而且可以通過ItemTouchHelper拖拽子View,同時RecyclerView具備復用功能,所以我就嘗試著通過RecyclerView來實現卡片式滑動,最終效果還不錯,特此開一篇文章與大家分享實現流程。

在真正描述 自定義LayoutManager實現前我先把效果圖亮出來讓大家爽一波


1546406457342.gif

閱讀本篇文章的你需要具備的技能:RecyclerView基本使用、LayoutManager、ItemTouchHelper、RecyclerView復用機制、View基本動畫,如果對這些知識點還不熟悉推薦閱讀啟艦大神的RecyclerView系列

1.自定義LayoutManager

首先定義一個類CardManager繼承RecyclerView.LayoutManager,重寫generateDefaultLayoutParams()方法和onLayoutChildren()方法,代碼如下

@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();
        //如果RecyclerView中只有一個item或者沒有,什么都不做
        if(itemCount<1){
            return;
        }
        //最底部item的角標
        int bottomPosition;
        if(itemCount<MAX_COUNT){
            bottomPosition = 0;
        }else {
            bottomPosition = itemCount - MAX_COUNT;
        }
        //從最底層的item開始擺放
        for(int i =bottomPosition;i<itemCount;i++){
            //從緩沖池中獲取到itemView
            View view = recycler.getViewForPosition(i);
            //將itemView添加到RecyclerView中  
            addView(view);
            //測量itemView
            measureChildWithMargins(view,0,0);
            //recyclerView寬度-itemView寬度
            int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
            int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
            //將itemView水平居中
            layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2
                    ,widthSpace/2+getDecoratedMeasuredWidth(view)
                    ,heightSpace/2+getDecoratedMeasuredHeight(view));
            //改變View的大小跟位置
            int level = Math.abs(i-itemCount+1);
            if(level>MAX_COUNT-3){
                view.setScaleX(1-SCALE_RATIO*(MAX_COUNT-2));
                view.setTranslationY(TRANS_RATIO*(MAX_COUNT-2));
            }else if(level>0){
                view.setScaleX(1-SCALE_RATIO*level);
                view.setTranslationY(TRANS_RATIO*level);
            }
        }
    }

generateDefaultLayoutParams()方法的作用是來設置子Item的LayoutParams,此處我們直接設置為自適應wrap_content。

onLayoutChildren()為自定義item位置的核心方法,首先執行detachAndScrapAttachedViews(recycler)方法,將所有的holderView從RecyclerView中剝離出來,等待重新布局。MAX_COUNT為最大View個數,這個取決于用戶個人需求,本篇文章中MAX_COUNT為4,通過MAX_COUNT計算出最底部item的角標。然后通過for循環對item進行布局,for循環中內容我來分步為大家解析:

  • 調用getViewForPosition(i)從RecyclerView的緩沖池中獲取到itemView并通過addView()加入到RecyclerView中
  • 調用measureChildWithMargins()測量itemView寬高,如不進行測量獲取到的寬高為0
  • 調用layoutDecoratedWithMargins()將itemView居中,參數分別為itemView、LEFT、TOP、RIGHT、BOTTOM四種邊距
  • 對itemView進行平移和縮放操作,注意:最底層的兩個View大小和位置全部一致,因為在進行拖動的時候倒數第二個itemView會逐漸平移和縮放,所以要多添加一個看不見的itemView讓底部動畫看起來更加的和諧。

LayoutManager我么定義好了,將它設置到RecyclerView的setLayoutManager(manager)中,適配器和常規的定義方法相同,我們來看一下效果圖:

card1.PNG

一不小心就實現了,是不是很簡單?但還不夠,這樣只能看到我大威少一人,是不能拖拽滑動的,怎么實現拖拽?聰明的同學可能已經想到,通過ItemTouchHelper,沒錯,就是它,我們接著往下看。

2.定義ItemTouchHelper

創建一個類,繼承自ItemTouchHelper.Callback,實現其抽象方法,并重寫onChildDraw()方法,代碼如下:

     //定義滑動方向
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags = 0;
        int swipeFlags = 0;
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof CardManager) {
            //允許上下滑動
            swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT |
                    ItemTouchHelper.UP | ItemTouchHelper.DOWN ;
        }
        return makeMovementFlags(dragFlags, swipeFlags);
    }

     //itemView滑出了屏幕
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        Log.i("zs","direction  "+direction);
        if(mListener!=null){
            mListener.onSwiped(viewHolder.getAdapterPosition(),direction);
        }
    }

     //拖動itemView時對部分itemView施加動畫效果
    @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);
        float trans;
        //以偏移量大的方向為標準
        if(Math.abs(dX)>Math.abs(dY)){
            trans = Math.abs(dX);
        }else {
            trans = Math.abs(dY);
        }
         //滑動比例
        float ratio = trans/getThreshold(recyclerView,viewHolder);
        if(ratio>1){
            ratio = 1;
        }
        //獲取itemView總量
        int itemCount = recyclerView.getChildCount();

        //移除時為底部顯示的View增加動畫
        for (int i = 1;i<CardManager.MAX_COUNT-1;i++){
            View view = recyclerView.getChildAt(i);
            float t = 1/(1-CardManager.SCALE_RATIO*ratio)-CardManager.SCALE_RATIO*(itemCount-i-1);
            view.setScaleX(t);
            view.setTranslationY(-CardManager.TRANS_RATIO*ratio+CardManager.TRANS_RATIO*(itemCount-i-1));
        }
        //為被拖動的View增加透明度動畫
        View view = recyclerView.getChildAt(itemCount-1);

        view.setAlpha(1 - Math.abs(ratio) * 0.2f);
    }

     //獲取劃出屏幕的距離閾值
    private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return recyclerView.getWidth() * getSwipeThreshold(viewHolder);
    }
  • getMovementFlags():用來設置滑動和拖動方向,兩個參數為滑動和拖動方向,本例中只支持滑動不支持拖動,滑動支持上下左右四個方向。
  • onSwiped():當itemView滑出屏幕后會回調該方法,我定義了一個回調接口,在onSwiped()中調用該接口通知適配器做更新操作。
  • onChildDraw():滑動時動畫可以在該方法內進行。
  • getThreshold():用來獲取劃出屏幕距離閾值,比如通過setThreshold()設置了滑動閾值為100像素,當itemView滑動超過100像素時松手itemView會自定滑出屏幕。

以上幾個方法只有onChildDraw()略微麻煩,其他幾個都比較簡單,所以我只對onChildDraw()中內容進行消息描述:

  • 從方法中獲取到x、y軸平移的距離dx、dy,然后從二者中選出一個絕對值較大的作為滑動距離。
  • 計算滑動比例ratio,用來設置動畫
  • 獲取到總item數,開啟一個for循環逐個為itemView設置平緩度過的動畫 (除去第一個和最后一個),進行平移和縮放的時候一定要建立在之前的基礎上
  • 獲取到正在被滑動的itemVIew,為其施加透明度動畫

在實現平移和縮放動畫的時候一定要在已經進行的平移和縮放的基礎上進行,一定一定一定,重要的事情說三遍。以上為過度動畫的內容,難度也不是很高。

Activity中的應用

與RecyclerView進行綁定

        CardHelperCallback itemTouchHelpCallback = new CardHelperCallback();
        ItemTouchHelper helper = new ItemTouchHelper(itemTouchHelpCallback);
        //將ItemTouchHelper和RecyclerView進行綁定
        helper.attachToRecyclerView(recyclerView);

一般要在自定義ItemTouchHelper.Callback中定義一個接口,用來itemView被滑出屏幕后即onSwiped()方法被調用的與Adapter通信

 public interface OnItemTouchCallbackListener {
        //item被滑動的回調方法
        void onSwiped(int position, int direction);
    }

在需要的地方(一般都是在Activity中)實現接口中的方法onSwiped(int position, int direction)

//實現OnItemTouchCallbackListener 接口
@Override
    public void onSwiped(int position,int direction) {
        if(direction==CardManager.MAX_COUNT){//左滑
            //Toast.makeText(this,"left",Toast.LENGTH_SHORT).show();
        }else {//右滑
            //Toast.makeText(this,"right",Toast.LENGTH_SHORT).show();
        }
        if(mCardBeanList!=null){
            for (int i = 0;i<recyclerView.getChildCount();i++){
                View view = recyclerView.getChildAt(i);
                view.setAlpha(1);
            }
            mCardBeanList.remove(position);
            //加載更多
            if(mCardBeanList.size()<CardManager.MAX_COUNT){
                loadMore();
            }
            mCardAdapter.notifyDataSetChanged();
        }
    }

改方法中有三點需要注意:

  • 重置itemView的透明度,因為itemView是要進行復用的,所以放入緩沖池中時要進行重置
  • 刪除List中對用的元素
  • 更新適配器,讓數據重新填充
拓展

我們都知道RecyclerView是可以對View進行復用的,那么它的復用原理是什么呢?在這我就簡單說一下,雖然View緩沖池由RecyclerView掌控,但是想要完成整個View的復用需要LayoutManager配合,因為RecyclerView并不知道什么時候需要將View回收,所以需要LayoutManager告訴RecyclerView回收哪個View,通過recyclerView.removeAndRecycleView(child, recycler)來實現,所以在本例中應該在onSwiped()中調用該方法,但細心的同學可能發現我并未調用這個方法通知RecyclerView回收,其實是因為itemView被滑出屏幕后ItemTouchHelper內部會對該itemView進行回收操作,關于這一塊內容大家做一個了解就行了,如果想要深入了解RecyclerView復用機制可以參考文章開頭推薦的啟艦大神RecyclerView系列。

Demo已托管至github,可運行

總結

RecyclerView是Android中非常重要的一個控件,功能十分強大,并且Google對它的封裝只能用完美的不能再完美來形容,Adapter、ItemTouchHepler、LayoutManager職責明確,完全獨立于RecyclerView存在,嚴格遵守低耦合,所以在我們制作卡片式滑動時RecyclerView和Adapter未受到任何影響,這也使得開發者更加清晰的去使用RecyclerView。看了本篇文章相信你對ItemTouchHepler和LayoutManager的理解又更加的深入了一些,如果覺得我幫助到你了,就去github上給我一個start,再次萬分感謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容

  • 內容 抽屜菜單 ListView WebView SwitchButton 按鈕 點贊按鈕 進度條 TabLayo...
    小狼W閱讀 1,618評論 0 10
  • 這篇文章分三個部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比...
    LucasAdam閱讀 4,408評論 0 27
  • “我不愿讓你一個人,一個人接受這殘忍,一個人忍受著午夜時分……”,這是在看《婚紗》時,一直盤環在腦海的旋律。 無論...
    LilyanSiena閱讀 1,699評論 12 18
  • 從前有一只豬長得特別帥。所有的母豬都喜歡他。可是他卻愛上了他家的女主人。然后每天喂食的時候,他都使勁往前拱,希望能...
    素顏的兔子閱讀 191評論 0 0
  • 剛剛入坑應該是兩年前,我和老公搬了新家,朋友送我們一盆多肉拼盤,那個時候并沒有入坑,所以連圖片都沒有留...兩年下...
    青蛙一小窩閱讀 270評論 0 0