Android無限循環(huán)RecyclerView的完美實現(xiàn)方案
背景
項目中要實現(xiàn)橫向列表的無限循環(huán)滾動,自然而然想到了RecyclerView,但我們常用的RecyclerView是不支持無限循環(huán)滾動的,所以就需要一些辦法讓它能夠無限循環(huán)。
方案選擇
方案1 對Adapter進行修改
網(wǎng)上大部分博客的解決方案都是這種方案,對Adapter做修改。具體如下
首先,讓 Adapter 的 getItemCount() 方法返回 Integer.MAX_VALUE,使得position數(shù)據(jù)達(dá)到很大很大;
其次,在 onBindViewHolder() 方法里對position參數(shù)取余運算,拿到position對應(yīng)的真實數(shù)據(jù)索引,然后對itemView綁定數(shù)據(jù)
最后,在初始化RecyclerView的時候,讓其滑動到指定位置,如 Integer.MAX_VALUE/2,這樣就不會滑動到邊界了,如果用戶一根筋,真的滑動到了邊界位置,再加一個判斷,如果當(dāng)前索引是0,就重新動態(tài)調(diào)整到初始位置
這個方案是挺簡單,但并不完美。一是對我們的數(shù)據(jù)和索引做了計算操作,二是如果滑動到邊界,再動態(tài)調(diào)整到中間,會有一個不明顯的卡頓操作,使得滑動不是很順暢。所以,直接看方案二。
方案2 自定義LayoutManager,修改RecyclerView的布局方式
這個算得上是一勞永逸的解決方案了,也是我今天要詳細(xì)介紹的方案。我們都知道,RecyclerView的數(shù)據(jù)綁定是通過Adapter來處理的,而排版方式以及View的回收控制等,則是通過LayoutManager來實現(xiàn)的,因此我們直接修改itemView的排版方式就可以實現(xiàn)我們的目標(biāo),讓RecyclerView無限循環(huán)。
自定義LayoutManager
1.創(chuàng)建自定義LayoutManager
首先,自定義 LooperLayoutManager 繼承自 RecyclerView.LayoutManager,然后需要實現(xiàn)抽象方法 generateDefaultLayoutParams(),這個方法的作用是給 itemView 設(shè)置默認(rèn)的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.打開滾動開關(guān)
接著,對滾動方向做處理,重寫canScrollHorizontally()方法,打開橫向滾動開關(guān)。注意我們是實現(xiàn)橫向無限循環(huán)滾動,所以實現(xiàn)此方法,如果要對垂直滾動做處理,則要實現(xiàn)canScrollVertically()方法。
@Override public boolean canScrollHorizontally() { return true; }
3.對RecyclerView進行初始化布局
好了,以上兩部是基礎(chǔ)工作,接下來,重寫 onLayoutChildren() 方法,開始對itemView初始化布局。
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (getItemCount() <= 0) { return; } //標(biāo)注1.如果當(dāng)前時準(zhǔn)備狀態(tài),直接返回 if (state.isPreLayout()) { return; } //標(biāo)注2.將視圖分離放入scrap緩存中,以準(zhǔn)備重新對view進行排版 detachAndScrapAttachedViews(recycler); int autualWidth = 0; for (int i = 0; i < getItemCount(); i++) { //標(biāo)注3.初始化,將在屏幕內(nèi)的view填充 View itemView = recycler.getViewForPosition(i); addView(itemView); //標(biāo)注4.測量itemView的寬高 measureChildWithMargins(itemView, 0, 0); int width = getDecoratedMeasuredWidth(itemView); int height = getDecoratedMeasuredHeight(itemView); //標(biāo)注5.根據(jù)itemView的寬高進行布局 layoutDecorated(itemView, autualWidth, 0, autualWidth + width, height); autualWidth += width; //標(biāo)注6.如果當(dāng)前布局過的itemView的寬度總和大于RecyclerView的寬,則不再進行布局 if (autualWidth > getWidth()) { break; } } }
onLayoutChildren() 方法顧名思義,就是對所有的 itemView 進行布局,一般會在初始化和調(diào)用 Adapter 的 notifyDataSetChanged() 方法時調(diào)用。代碼思路已經(jīng)注釋的很清楚了,其中有幾個方法需要簡單提下:
標(biāo)注2處 detachAndScrapAttachedViews(recycler) 方法會將所有的 itemView 從View樹中全部detach,然后放入scrap緩存中。了解過RecyclerView的同學(xué)應(yīng)該知道,RecyclerView是有一個二級緩存的,一級緩存是 scrap 緩存,二級緩存是 recycler 緩存,其中從View樹上detach的View會放入scrap緩存里,調(diào)用removeView()刪除的View會放入recycler緩存中。
標(biāo)注3處 recycler.getViewForPosition(i) 方法會從緩存中拿到對應(yīng)索引的 itemView,這個方法內(nèi)部會先從 scrap 緩存中取 itemView,如果沒有則從 recycler 緩存中取,如果還沒有則調(diào)用 adapter 的 onCreateViewHolder() 去創(chuàng)建 itemView。
標(biāo)注5處 layoutDecorated() 方法會對 itemView 進行布局排版,這里可以看出來,我們是根據(jù)寬依次往父容器的右邊排下去,直到下一個 itemView的頂點位置超過了RecyclerView 的寬度。
4.對RecyclerView進行滾動和回收itemView處理
對RecyclerView的子item進行排版布局后,運行一下效果就會出現(xiàn)了,不過這時候我們滑動列表會發(fā)現(xiàn)滑動后變成空白了,所以就該對滑動操作進行處理了。
前面說過,我們打開了橫向滾動的開關(guān),所以對應(yīng)的,我們要重寫 scrollHorizontallyBy()方法進行橫向滑動操作。
@Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { //標(biāo)注1.橫向滑動的時候,對左右兩邊按順序填充itemView int travl = fill(dx, recycler, state); if (travl == 0) { return 0; } //2.滑動 offsetChildrenHorizontal(-travl); //3.回收已經(jīng)不可見的itemView recyclerHideView(dx, recycler, state); return travl; }
可以看到,滑動邏輯很簡單,總結(jié)為三步:
- 橫向滑動的時候,對左右兩邊按順序填充itemView
- 滑動itemView
- 回收已經(jīng)不可見的itemView
下面一步一步介紹:
首先第一步,滑動的時候調(diào)用自定義的 fill() 方法,對左右兩邊進行填充。還沒忘了,我們是來實現(xiàn)循環(huán)滑動的,所以這一步尤其重要,先看代碼:
/** * 左右滑動的時候,填充 */ private int fill(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { if (dx > 0) { //標(biāo)注1.向左滾動 View lastView = getChildAt(getChildCount() - 1); if (lastView == null) { return 0; } int lastPos = getPosition(lastView); //標(biāo)注2.可見的最后一個itemView完全滑進來了,需要補充新的 if (lastView.getRight() < getWidth()) { View scrap = null; //標(biāo)注3.判斷可見的最后一個itemView的索引, // 如果是最后一個,則將下一個itemView設(shè)置為第一個,否則設(shè)置為當(dāng)前索引的下一個 if (lastPos == getItemCount() - 1) { if (looperEnable) { scrap = recycler.getViewForPosition(0); } else { dx = 0; } } else { scrap = recycler.getViewForPosition(lastPos + 1); } if (scrap == null) { return dx; } //標(biāo)注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。
看標(biāo)注3,往右邊填充的時候需要檢測當(dāng)前最后一個可見itemView的索引,如果索引是最后一個,則需要新填充的itemView為第0個,這樣就可以實現(xiàn)往左邊滑動時候無限循環(huán)了。然后將需要新填充的itemView進行測量布局操作,將填充進去了。
同理,往右滑動的邏輯跟往左滑動相似,就不一一再闡述了。
第二步:填充完新的itemView后,就開始進行滑動了,這里直接調(diào)用 LayoutManager 的 offsetChildrenHorizontal() 方法滑動-travl 距離,travl 是通過fill方法計算出來的,通常情況下都為 dx,只有當(dāng)滑動到最后一個itemView,并且循環(huán)滾動開關(guān)沒有打開的時候才為0,也就是不滾動了。
//2.滾動 offsetChildrenHorizontal(travl * -1);
第三步:回收已經(jīng)不可見的itemView。只有對不可見的itemView進行回收,才能做到回收利用,防止內(nèi)存爆增。
/** * 回收界面不可見的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) { //標(biāo)注1.向左滾動,移除左邊不在內(nèi)容里的view if (view.getRight() < 0) { removeAndRecycleView(view, recycler); Log.d(TAG, "循環(huán): 移除 一個view childCount=" + getChildCount()); } } else { //標(biāo)注2.向右滾動,移除右邊不在內(nèi)容里的view if (view.getLeft() > getWidth()) { removeAndRecycleView(view, recycler); Log.d(TAG, "循環(huán): 移除 一個view childCount=" + getChildCount()); } } } }
代碼也很簡單,遍歷所有添加進 RecyclerView 里的item,然后根據(jù) itemView 的頂點位置進行判斷,移除不可見的item。移除 itemView 調(diào)用 removeAndRecycleView(view, recycler) 方法,會對移除的item進行回收,然后存入 RecyclerView 的緩存里。
至此,一個可以實現(xiàn)左右無限循環(huán)的LayoutManager就實現(xiàn)了,調(diào)用方式跟通常我們用RrcyclerView沒有任何區(qū)別,只需要給 RecyclerView 設(shè)置 LayoutManager 時指定我們的LayoutManager,如下:
recyclerView.setAdapter(new MyAdapter()); LooperLayoutManager layoutManager = new LooperLayoutManager(); layoutManager.setLooperEnable(true); recyclerView.setLayoutManager(layoutManager);
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android的HTTP類庫Volley入門學(xué)習(xí)教程
這篇文章主要介紹了Android應(yīng)用開發(fā)框架Volley的入門學(xué)習(xí)教程,Volley適合于輕量級的通信功能開發(fā),善于處理JSON對象,需要的朋友可以參考下2016-02-02Android使用HttpURLConnection實現(xiàn)網(wǎng)絡(luò)訪問流程
早些時候其實我們都習(xí)慣性使用HttpClient,但是后來Android6.0之后不再支持HttpClient,需要添加Apache的jar才行,所以,就有很多開發(fā)者放棄使用HttpClient了,HttpURLConnection畢竟是標(biāo)準(zhǔn)Java接口(java.net) ,適配性還是很強的2022-12-12Flutter學(xué)習(xí)之創(chuàng)建一個內(nèi)嵌的navigation詳解
我們在flutter中可以使用Navigator.push或者Navigator.pushNamed方法來向Navigator中添加不同的頁面,從而達(dá)到頁面調(diào)整的目的。本文就來聊聊如何創(chuàng)建一個內(nèi)嵌的navigation吧2023-03-03Android第三方HTTP網(wǎng)絡(luò)支持包OkHttp的基礎(chǔ)使用教程
在GitHub上開源的安卓HTTP編程包OkHttp正在積累著越來越高的人氣,這里我們就來看一下這款A(yù)ndroid第三方HTTP網(wǎng)絡(luò)支持包OkHttp的基礎(chǔ)使用教程:2016-07-07Android仿今日頭條APP實現(xiàn)下拉導(dǎo)航選擇菜單效果
這篇文章主要為大家詳細(xì)介紹了Android仿今日頭條APP實現(xiàn)下拉導(dǎo)航選擇菜單效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06Android實戰(zhàn)教程第四篇之簡單實現(xiàn)短信發(fā)送器
這篇文章主要為大家詳細(xì)介紹了Android實戰(zhàn)教程第四篇之簡單實現(xiàn)短信發(fā)送器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化
這篇文章主要為大家介紹了Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02Android實現(xiàn)自定義驗證碼輸入框效果(實例代碼)
這篇文章主要介紹了Android實現(xiàn)自定義驗證碼輸入框效果,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01