無限循環RecyclerView的完美實現方案

背景緩存

項目中要實現橫向列表的無限循環滾動,天然而然想到了RecyclerView,但咱們經常使用的RecyclerView是不支持無限循環滾動的,因此就須要一些辦法讓它可以無限循環。ide

方案選擇oop

方案1 對Adapter進行修改佈局

網上大部分博客的解決方案都是這種方案,對Adapter作修改。具體以下code

首先,讓 Adapter 的 getItemCount() 方法返回 Integer.MAX_VALUE,使得position數據達到很大很大;繼承

其次,在 onBindViewHolder() 方法裏對position參數取餘運算,拿到position對應的真實數據索引,而後對itemView綁定數據索引

最後,在初始化RecyclerView的時候,讓其滑動到指定位置,如 Integer.MAX_VALUE/2,這樣就不會滑動到邊界了,若是用戶一根筋,真的滑動到了邊界位置,再加一個判斷,若是當前索引是0,就從新動態調整到初始位置內存

這個方案是挺簡單,但並不完美。一是對咱們的數據和索引作了計算操做,二是若是滑動到邊界,再動態調整到中間,會有一個不明顯的卡頓操做,使得滑動不是很順暢。因此,直接看方案二。rem

方案2 自定義LayoutManager,修改RecyclerView的佈局方式get

這個算得上是一勞永逸的解決方案了,也是我今天要詳細介紹的方案。咱們都知道,RecyclerView的數據綁定是經過Adapter來處理的,而排版方式以及View的回收控制等,則是經過LayoutManager來實現的,所以咱們直接修改itemView的排版方式就能夠實現咱們的目標,讓RecyclerView無限循環。

自定義LayoutManager
1.建立自定義LayoutManager

首先,自定義 LooperLayoutManager 繼承自 RecyclerView.LayoutManager,而後須要實現抽象方法 generateDefaultLayoutParams(),這個方法的做用是給 itemView 設置默認的LayoutParams,直接返回以下就行。

public class LooperLayoutManager extends RecyclerView.LayoutManager {
        @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }
}

2.打開滾動開關
接着,對滾動方向作處理,重寫canScrollHorizontally()方法,打開橫向滾動開關。注意咱們是實現橫向無限循環滾動,因此實現此方法,若是要對垂直滾動作處理,則要實現canScrollVertically()方法。

@Override
    public boolean canScrollHorizontally() {
        return true;
    }

3.對RecyclerView進行初始化佈局
好了,以上兩部是基礎工做,接下來,重寫 onLayoutChildren() 方法,開始對itemView初始化佈局。

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0) {
            return;
        }
        //標註1.若是當前時準備狀態,直接返回
        if (state.isPreLayout()) {
            return;
        }
        //標註2.將視圖分離放入scrap緩存中,以準備從新對view進行排版
        detachAndScrapAttachedViews(recycler);

        int autualWidth = 0;
        for (int i = 0; i < getItemCount(); i++) {
            //標註3.初始化,將在屏幕內的view填充
            View itemView = recycler.getViewForPosition(i);
            addView(itemView);
            //標註4.測量itemView的寬高
            measureChildWithMargins(itemView, 0, 0);
            int width = getDecoratedMeasuredWidth(itemView);
            int height = getDecoratedMeasuredHeight(itemView);
            //標註5.根據itemView的寬高進行佈局
            layoutDecorated(itemView, autualWidth, 0, autualWidth + width, height);

            autualWidth += width;
            //標註6.若是當前佈局過的itemView的寬度總和大於RecyclerView的寬,則再也不進行佈局
            if (autualWidth > getWidth()) {
                break;
            }
        }
    }

onLayoutChildren() 方法顧名思義,就是對全部的 itemView 進行佈局,通常會在初始化和調用 Adapter 的 notifyDataSetChanged() 方法時調用。代碼思路已經註釋的很清楚了,其中有幾個方法須要簡單提下:

標註2處 detachAndScrapAttachedViews(recycler) 方法會將全部的 itemView 從View樹中所有detach,而後放入scrap緩存中。瞭解過RecyclerView的同窗應該知道,RecyclerView是有一個二級緩存的,一級緩存是 scrap 緩存,二級緩存是 recycler 緩存,其中從View樹上detach的View會放入scrap緩存裏,調用removeView()刪除的View會放入recycler緩存中。

標註3處 recycler.getViewForPosition(i) 方法會從緩存中拿到對應索引的 itemView,這個方法內部會先從 scrap 緩存中取 itemView,若是沒有則從 recycler 緩存中取,若是尚未則調用 adapter 的 onCreateViewHolder() 去建立 itemView。

標註5處 layoutDecorated() 方法會對 itemView 進行佈局排版,這裏能夠看出來,咱們是根據寬依次往父容器的右邊排下去,直到下一個 itemView的頂點位置超過了RecyclerView 的寬度。

4.對RecyclerView進行滾動和回收itemView處理
對RecyclerView的子item進行排版佈局後,運行一下效果就會出現了,不過這時候咱們滑動列表會發現滑動後變成空白了,因此就該對滑動操做進行處理了。

前面說過,咱們打開了橫向滾動的開關,因此對應的,咱們要重寫 scrollHorizontallyBy()方法進行橫向滑動操做。

@Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //標註1.橫向滑動的時候,對左右兩邊按順序填充itemView
        int travl = fill(dx, recycler, state);
        if (travl == 0) {
            return 0;
        }

        //2.滑動
        offsetChildrenHorizontal(-travl);

        //3.回收已經不可見的itemView
        recyclerHideView(dx, recycler, state);
        return travl;
    }

能夠看到,滑動邏輯很簡單,總結爲三步:

  • 橫向滑動的時候,對左右兩邊按順序填充itemView
  • 滑動itemView
  • 回收已經不可見的itemView

下面一步一步介紹:
首先第一步,滑動的時候調用自定義的 fill() 方法,對左右兩邊進行填充。還沒忘了,咱們是來實現循環滑動的,因此這一步尤爲重要,先看代碼:

/**
     * 左右滑動的時候,填充
     */
    private int fill(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (dx > 0) {
            //標註1.向左滾動
            View lastView = getChildAt(getChildCount() - 1);
            if (lastView == null) {
                return 0;
            }
            int lastPos = getPosition(lastView);
            //標註2.可見的最後一個itemView徹底滑進來了,須要補充新的
            if (lastView.getRight() < getWidth()) {
                View scrap = null;
                //標註3.判斷可見的最後一個itemView的索引,
                // 若是是最後一個,則將下一個itemView設置爲第一個,不然設置爲當前索引的下一個
                if (lastPos == getItemCount() - 1) {
                    if (looperEnable) {
                        scrap = recycler.getViewForPosition(0);
                    } else {
                        dx = 0;
                    }
                } else {
                    scrap = recycler.getViewForPosition(lastPos + 1);
                }
                if (scrap == null) {
                    return dx;
                }
                //標註4.將新的itemViewadd進來並對其測量和佈局
                addView(scrap);
                measureChildWithMargins(scrap, 0, 0);
                int width = getDecoratedMeasuredWidth(scrap);
                int height = getDecoratedMeasuredHeight(scrap);
                layoutDecorated(scrap,lastView.getRight(), 0,
                        lastView.getRight() + width, height);
                return dx;
            }
        } else {
            //向右滾動
            View firstView = getChildAt(0);
            if (firstView == null) {
                return 0;
            }
            int firstPos = getPosition(firstView);

            if (firstView.getLeft() >= 0) {
                View scrap = null;
                if (firstPos == 0) {
                    if (looperEnable) {
                        scrap = recycler.getViewForPosition(getItemCount() - 1);
                    } else {
                        dx = 0;
                    }
                } else {
                    scrap = recycler.getViewForPosition(firstPos - 1);
                }
                if (scrap == null) {
                    return 0;
                }
                addView(scrap, 0);
                measureChildWithMargins(scrap,0,0);
                int width = getDecoratedMeasuredWidth(scrap);
                int height = getDecoratedMeasuredHeight(scrap);
                layoutDecorated(scrap, firstView.getLeft() - width, 0,
                        firstView.getLeft(), height);
            }
        }
        return dx;
    }

代碼是有點長,不過邏輯很清晰。首先分爲兩部分,往左填充或是往右填充,dx爲將要滑動的距離,若是 dx > 0,則是往左邊滑動,則須要判斷右邊的邊界,若是最後一個itemView徹底顯示出來後,在右邊填充一個新的itemView。
看標註3,往右邊填充的時候須要檢測當前最後一個可見itemView的索引,若是索引是最後一個,則須要新填充的itemView爲第0個,這樣就能夠實現往左邊滑動時候無限循環了。而後將須要新填充的itemView進行測量佈局操做,將填充進去了。

同理,往右滑動的邏輯跟往左滑動類似,就不一一再闡述了。

第二步:填充完新的itemView後,就開始進行滑動了,這裏直接調用 LayoutManager 的 offsetChildrenHorizontal() 方法滑動-travl 距離,travl 是經過fill方法計算出來的,一般狀況下都爲 dx,只有當滑動到最後一個itemView,而且循環滾動開關沒有打開的時候才爲0,也就是不滾動了。

//2.滾動
        offsetChildrenHorizontal(travl * -1);

第三步:回收已經不可見的itemView。只有對不可見的itemView進行回收,才能作到回收利用,防止內存爆增。

/**
     * 回收界面不可見的view
     */
    private void recyclerHideView(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view == null) {
                continue;
            }
            if (dx > 0) {
                //標註1.向左滾動,移除左邊不在內容裏的view
                if (view.getRight() < 0) {
                    removeAndRecycleView(view, recycler);
                    Log.d(TAG, "循環: 移除 一個view  childCount=" + getChildCount());
                }
            } else {
                //標註2.向右滾動,移除右邊不在內容裏的view
                if (view.getLeft() > getWidth()) {
                    removeAndRecycleView(view, recycler);
                    Log.d(TAG, "循環: 移除 一個view  childCount=" + getChildCount());
                }
            }
        }

    }

代碼也很簡單,遍歷全部添加進 RecyclerView 裏的item,而後根據 itemView 的頂點位置進行判斷,移除不可見的item。移除 itemView 調用 removeAndRecycleView(view, recycler) 方法,會對移除的item進行回收,而後存入 RecyclerView 的緩存裏。

至此,一個能夠實現左右無限循環的LayoutManager就實現了,調用方式跟一般咱們用RrcyclerView沒有任何區別,只須要給 RecyclerView 設置 LayoutManager 時指定咱們的LayoutManager,以下:

recyclerView.setAdapter(new MyAdapter());
        LooperLayoutManager layoutManager = new LooperLayoutManager();
        layoutManager.setLooperEnable(true);
        recyclerView.setLayoutManager(layoutManager);

有問題加Q羣討論:925019412

相關文章
相關標籤/搜索