Android無限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案
背景
項(xiàng)目中要實(shí)現(xiàn)橫向列表的無限循環(huán)滾動(dòng),自然而然想到了RecyclerView,但我們常用的RecyclerView是不支持無限循環(huán)滾動(dòng)的,所以就需要一些辦法讓它能夠無限循環(huán)。
方案選擇
方案1 對(duì)Adapter進(jìn)行修改
網(wǎng)上大部分博客的解決方案都是這種方案,對(duì)Adapter做修改。具體如下
首先,讓 Adapter 的 getItemCount() 方法返回 Integer.MAX_VALUE,使得position數(shù)據(jù)達(dá)到很大很大;
其次,在 onBindViewHolder() 方法里對(duì)position參數(shù)取余運(yùn)算,拿到position對(duì)應(yīng)的真實(shí)數(shù)據(jù)索引,然后對(duì)itemView綁定數(shù)據(jù)
最后,在初始化RecyclerView的時(shí)候,讓其滑動(dòng)到指定位置,如 Integer.MAX_VALUE/2,這樣就不會(huì)滑動(dòng)到邊界了,如果用戶一根筋,真的滑動(dòng)到了邊界位置,再加一個(gè)判斷,如果當(dāng)前索引是0,就重新動(dòng)態(tài)調(diào)整到初始位置
這個(gè)方案是挺簡單,但并不完美。一是對(duì)我們的數(shù)據(jù)和索引做了計(jì)算操作,二是如果滑動(dòng)到邊界,再動(dòng)態(tài)調(diào)整到中間,會(huì)有一個(gè)不明顯的卡頓操作,使得滑動(dòng)不是很順暢。所以,直接看方案二。
方案2 自定義LayoutManager,修改RecyclerView的布局方式
這個(gè)算得上是一勞永逸的解決方案了,也是我今天要詳細(xì)介紹的方案。我們都知道,RecyclerView的數(shù)據(jù)綁定是通過Adapter來處理的,而排版方式以及View的回收控制等,則是通過LayoutManager來實(shí)現(xiàn)的,因此我們直接修改itemView的排版方式就可以實(shí)現(xiàn)我們的目標(biāo),讓RecyclerView無限循環(huán)。
自定義LayoutManager
1.創(chuàng)建自定義LayoutManager
首先,自定義 LooperLayoutManager 繼承自 RecyclerView.LayoutManager,然后需要實(shí)現(xiàn)抽象方法 generateDefaultLayoutParams(),這個(gè)方法的作用是給 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.打開滾動(dòng)開關(guān)
接著,對(duì)滾動(dòng)方向做處理,重寫canScrollHorizontally()方法,打開橫向滾動(dòng)開關(guān)。注意我們是實(shí)現(xiàn)橫向無限循環(huán)滾動(dòng),所以實(shí)現(xiàn)此方法,如果要對(duì)垂直滾動(dòng)做處理,則要實(shí)現(xiàn)canScrollVertically()方法。
@Override
public boolean canScrollHorizontally() {
return true;
}
3.對(duì)RecyclerView進(jìn)行初始化布局
好了,以上兩部是基礎(chǔ)工作,接下來,重寫 onLayoutChildren() 方法,開始對(duì)itemView初始化布局。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0) {
return;
}
//標(biāo)注1.如果當(dāng)前時(shí)準(zhǔn)備狀態(tài),直接返回
if (state.isPreLayout()) {
return;
}
//標(biāo)注2.將視圖分離放入scrap緩存中,以準(zhǔn)備重新對(duì)view進(jìn)行排版
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.測(cè)量itemView的寬高
measureChildWithMargins(itemView, 0, 0);
int width = getDecoratedMeasuredWidth(itemView);
int height = getDecoratedMeasuredHeight(itemView);
//標(biāo)注5.根據(jù)itemView的寬高進(jìn)行布局
layoutDecorated(itemView, autualWidth, 0, autualWidth + width, height);
autualWidth += width;
//標(biāo)注6.如果當(dāng)前布局過的itemView的寬度總和大于RecyclerView的寬,則不再進(jìn)行布局
if (autualWidth > getWidth()) {
break;
}
}
}
onLayoutChildren() 方法顧名思義,就是對(duì)所有的 itemView 進(jìn)行布局,一般會(huì)在初始化和調(diào)用 Adapter 的 notifyDataSetChanged() 方法時(shí)調(diào)用。代碼思路已經(jīng)注釋的很清楚了,其中有幾個(gè)方法需要簡單提下:
標(biāo)注2處 detachAndScrapAttachedViews(recycler) 方法會(huì)將所有的 itemView 從View樹中全部detach,然后放入scrap緩存中。了解過RecyclerView的同學(xué)應(yīng)該知道,RecyclerView是有一個(gè)二級(jí)緩存的,一級(jí)緩存是 scrap 緩存,二級(jí)緩存是 recycler 緩存,其中從View樹上detach的View會(huì)放入scrap緩存里,調(diào)用removeView()刪除的View會(huì)放入recycler緩存中。
標(biāo)注3處 recycler.getViewForPosition(i) 方法會(huì)從緩存中拿到對(duì)應(yīng)索引的 itemView,這個(gè)方法內(nèi)部會(huì)先從 scrap 緩存中取 itemView,如果沒有則從 recycler 緩存中取,如果還沒有則調(diào)用 adapter 的 onCreateViewHolder() 去創(chuàng)建 itemView。
標(biāo)注5處 layoutDecorated() 方法會(huì)對(duì) itemView 進(jìn)行布局排版,這里可以看出來,我們是根據(jù)寬依次往父容器的右邊排下去,直到下一個(gè) itemView的頂點(diǎn)位置超過了RecyclerView 的寬度。
4.對(duì)RecyclerView進(jìn)行滾動(dòng)和回收itemView處理
對(duì)RecyclerView的子item進(jìn)行排版布局后,運(yùn)行一下效果就會(huì)出現(xiàn)了,不過這時(shí)候我們滑動(dòng)列表會(huì)發(fā)現(xiàn)滑動(dòng)后變成空白了,所以就該對(duì)滑動(dòng)操作進(jìn)行處理了。
前面說過,我們打開了橫向滾動(dòng)的開關(guān),所以對(duì)應(yīng)的,我們要重寫 scrollHorizontallyBy()方法進(jìn)行橫向滑動(dòng)操作。
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
//標(biāo)注1.橫向滑動(dòng)的時(shí)候,對(duì)左右兩邊按順序填充itemView
int travl = fill(dx, recycler, state);
if (travl == 0) {
return 0;
}
//2.滑動(dòng)
offsetChildrenHorizontal(-travl);
//3.回收已經(jīng)不可見的itemView
recyclerHideView(dx, recycler, state);
return travl;
}
可以看到,滑動(dòng)邏輯很簡單,總結(jié)為三步:
- 橫向滑動(dòng)的時(shí)候,對(duì)左右兩邊按順序填充itemView
- 滑動(dòng)itemView
- 回收已經(jīng)不可見的itemView
下面一步一步介紹:
首先第一步,滑動(dòng)的時(shí)候調(diào)用自定義的 fill() 方法,對(duì)左右兩邊進(jìn)行填充。還沒忘了,我們是來實(shí)現(xiàn)循環(huán)滑動(dòng)的,所以這一步尤其重要,先看代碼:
/**
* 左右滑動(dòng)的時(shí)候,填充
*/
private int fill(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (dx > 0) {
//標(biāo)注1.向左滾動(dòng)
View lastView = getChildAt(getChildCount() - 1);
if (lastView == null) {
return 0;
}
int lastPos = getPosition(lastView);
//標(biāo)注2.可見的最后一個(gè)itemView完全滑進(jìn)來了,需要補(bǔ)充新的
if (lastView.getRight() < getWidth()) {
View scrap = null;
//標(biāo)注3.判斷可見的最后一個(gè)itemView的索引,
// 如果是最后一個(gè),則將下一個(gè)itemView設(shè)置為第一個(gè),否則設(shè)置為當(dāng)前索引的下一個(gè)
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進(jìn)來并對(duì)其測(cè)量和布局
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 {
//向右滾動(dòng)
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;
}
代碼是有點(diǎn)長,不過邏輯很清晰。首先分為兩部分,往左填充或是往右填充,dx為將要滑動(dòng)的距離,如果 dx > 0,則是往左邊滑動(dòng),則需要判斷右邊的邊界,如果最后一個(gè)itemView完全顯示出來后,在右邊填充一個(gè)新的itemView。
看標(biāo)注3,往右邊填充的時(shí)候需要檢測(cè)當(dāng)前最后一個(gè)可見itemView的索引,如果索引是最后一個(gè),則需要新填充的itemView為第0個(gè),這樣就可以實(shí)現(xiàn)往左邊滑動(dòng)時(shí)候無限循環(huán)了。然后將需要新填充的itemView進(jìn)行測(cè)量布局操作,將填充進(jìn)去了。
同理,往右滑動(dòng)的邏輯跟往左滑動(dòng)相似,就不一一再闡述了。
第二步:填充完新的itemView后,就開始進(jìn)行滑動(dòng)了,這里直接調(diào)用 LayoutManager 的 offsetChildrenHorizontal() 方法滑動(dòng)-travl 距離,travl 是通過fill方法計(jì)算出來的,通常情況下都為 dx,只有當(dāng)滑動(dòng)到最后一個(gè)itemView,并且循環(huán)滾動(dòng)開關(guān)沒有打開的時(shí)候才為0,也就是不滾動(dòng)了。
//2.滾動(dòng)
offsetChildrenHorizontal(travl * -1);
第三步:回收已經(jīng)不可見的itemView。只有對(duì)不可見的itemView進(jìn)行回收,才能做到回收利用,防止內(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.向左滾動(dòng),移除左邊不在內(nèi)容里的view
if (view.getRight() < 0) {
removeAndRecycleView(view, recycler);
Log.d(TAG, "循環(huán): 移除 一個(gè)view childCount=" + getChildCount());
}
} else {
//標(biāo)注2.向右滾動(dòng),移除右邊不在內(nèi)容里的view
if (view.getLeft() > getWidth()) {
removeAndRecycleView(view, recycler);
Log.d(TAG, "循環(huán): 移除 一個(gè)view childCount=" + getChildCount());
}
}
}
}
代碼也很簡單,遍歷所有添加進(jìn) RecyclerView 里的item,然后根據(jù) itemView 的頂點(diǎn)位置進(jìn)行判斷,移除不可見的item。移除 itemView 調(diào)用 removeAndRecycleView(view, recycler) 方法,會(huì)對(duì)移除的item進(jìn)行回收,然后存入 RecyclerView 的緩存里。
至此,一個(gè)可以實(shí)現(xiàn)左右無限循環(huán)的LayoutManager就實(shí)現(xiàn)了,調(diào)用方式跟通常我們用RrcyclerView沒有任何區(qū)別,只需要給 RecyclerView 設(shè)置 LayoutManager 時(shí)指定我們的LayoutManager,如下:
recyclerView.setAdapter(new MyAdapter());
LooperLayoutManager layoutManager = new LooperLayoutManager();
layoutManager.setLooperEnable(true);
recyclerView.setLayoutManager(layoutManager);
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android RecyclerView上拉加載更多功能回彈實(shí)現(xiàn)代碼
這篇文章主要介紹了Android RecyclerView上拉加載更多功能回彈實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
非常實(shí)用的側(cè)滑刪除控件SwipeLayout
這篇文章主要為大家詳細(xì)介紹了非常實(shí)用的側(cè)滑刪除控件SwipeLayout,類似于QQ側(cè)滑點(diǎn)擊刪除效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android自動(dòng)測(cè)試工具M(jìn)onkey的實(shí)現(xiàn)方法
本文主要介紹Android Monkey 實(shí)現(xiàn)方法,Monkey測(cè)試是一種為了測(cè)試軟件的穩(wěn)定性、健壯性的快速有效的方法,具有非常重要的參考價(jià)值,希望對(duì)小伙伴有所幫助2016-07-07
Android實(shí)現(xiàn)紅包雨動(dòng)畫效果
本篇文章主要介紹了Android實(shí)現(xiàn)紅包雨動(dòng)畫效果,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
APP添加CNZZ統(tǒng)計(jì)插件教程 Android版添加phonegap
這篇文章主要介紹了APP添加CNZZ統(tǒng)計(jì)插件教程,Android版添加phonegap,感興趣的小伙伴們可以參考一下2015-12-12
Android組件間通信--深入理解Intent與IntentFilter
本篇文章是對(duì)Android組件間通信Intent與IntentFilter進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05

