無(wú)限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案

轉(zhuǎn)自

https://juejin.im/post/5cfa198ff265da1b8c197c2f

背景

項(xiàng)目中要實(shí)現(xiàn)橫向列表的無(wú)限循環(huán)滾動(dòng)嘶摊,自然而然想到了RecyclerView悴侵,但我們常用的RecyclerView是不支持無(wú)限循環(huán)滾動(dòng)的排宰,所以就需要一些辦法讓它能夠無(wú)限循環(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è)方案是挺簡(jiǎn)單琳拨,但并不完美瞭恰。一是對(duì)我們的數(shù)據(jù)和索引做了計(jì)算操作,二是如果滑動(dòng)到邊界狱庇,再動(dòng)態(tài)調(diào)整到中間惊畏,會(huì)有一個(gè)不明顯的卡頓操作,使得滑動(dòng)不是很順暢密任。所以颜启,直接看方案二。

方案2 自定義LayoutManager浪讳,修改RecyclerView的布局方式

這個(gè)算得上是一勞永逸的解決方案了缰盏,也是我今天要詳細(xì)介紹的方案。我們都知道,RecyclerView的數(shù)據(jù)綁定是通過(guò)Adapter來(lái)處理的口猜,而排版方式以及View的回收控制等负溪,則是通過(guò)LayoutManager來(lái)實(shí)現(xiàn)的,因此我們直接修改itemView的排版方式就可以實(shí)現(xiàn)我們的目標(biāo)济炎,讓RecyclerView無(wú)限循環(huán)川抡。

自定義LayoutManager

1.創(chuàng)建自定義LayoutManager

首先,自定義 LooperLayoutManager 繼承自 RecyclerView.LayoutManager须尚,然后需要實(shí)現(xiàn)抽象方法 generateDefaultLayoutParams()崖堤,這個(gè)方法的作用是給 itemView 設(shè)置默認(rèn)的LayoutParams,直接返回如下就行耐床。

public?classLooperLayoutManagerextendsRecyclerView.LayoutManager{

????????@Override

????public?RecyclerView.LayoutParamsgenerateDefaultLayoutParams(){

????????return?new?RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,

????????????????ViewGroup.LayoutParams.WRAP_CONTENT);

????}

}

2.打開(kāi)滾動(dòng)開(kāi)關(guān)

接著倘感,對(duì)滾動(dòng)方向做處理,重寫canScrollHorizontally()方法咙咽,打開(kāi)橫向滾動(dòng)開(kāi)關(guān)。注意我們是實(shí)現(xiàn)橫向無(wú)限循環(huán)滾動(dòng)淤年,所以實(shí)現(xiàn)此方法钧敞,如果要對(duì)垂直滾動(dòng)做處理,則要實(shí)現(xiàn)canScrollVertically()方法麸粮。

?@Override

????publicbooleancanScrollHorizontally(){

????????return?true;

????}

3.對(duì)RecyclerView進(jìn)行初始化布局

好了溉苛,以上兩部是基礎(chǔ)工作,接下來(lái)弄诲,重寫 onLayoutChildren() 方法愚战,開(kāi)始對(duì)itemView初始化布局。

@Override

????publicvoidonLayoutChildren(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)前布局過(guò)的itemView的寬度總和大于RecyclerView的寬梗摇,則不再進(jìn)行布局

????????????if?(autualWidth?>?getWidth())?{

????????????????break;

????????????}

????????}

????}

onLayoutChildren() 方法顧名思義拓哟,就是對(duì)所有的 itemView 進(jìn)行布局,一般會(huì)在初始化和調(diào)用 Adapter 的 notifyDataSetChanged() 方法時(shí)調(diào)用伶授。代碼思路已經(jīng)注釋的很清楚了断序,其中有幾個(gè)方法需要簡(jiǎn)單提下:

標(biāo)注2處 detachAndScrapAttachedViews(recycler) 方法會(huì)將所有的 itemView 從View樹(shù)中全部detach,然后放入scrap緩存中糜烹。了解過(guò)RecyclerView的同學(xué)應(yīng)該知道违诗,RecyclerView是有一個(gè)二級(jí)緩存的,一級(jí)緩存是 scrap 緩存,二級(jí)緩存是 recycler 緩存疮蹦,其中從View樹(shù)上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亮蒋,如果沒(méi)有則從 recycler 緩存中取扣典,如果還沒(méi)有則調(diào)用 adapter 的 onCreateViewHolder() 去創(chuàng)建 itemView。

標(biāo)注5處 layoutDecorated() 方法會(huì)對(duì) itemView 進(jìn)行布局排版慎玖,這里可以看出來(lái)贮尖,我們是根據(jù)寬依次往父容器的右邊排下去,直到下一個(gè) itemView的頂點(diǎn)位置超過(guò)了RecyclerView 的寬度趁怔。

4.對(duì)RecyclerView進(jìn)行滾動(dòng)和回收itemView處理

對(duì)RecyclerView的子item進(jìn)行排版布局后湿硝,運(yùn)行一下效果就會(huì)出現(xiàn)了,不過(guò)這時(shí)候我們滑動(dòng)列表會(huì)發(fā)現(xiàn)滑動(dòng)后變成空白了润努,所以就該對(duì)滑動(dòng)操作進(jìn)行處理了关斜。

前面說(shuō)過(guò),我們打開(kāi)了橫向滾動(dòng)的開(kāi)關(guān),所以對(duì)應(yīng)的铺浇,我們要重寫 scrollHorizontallyBy()方法進(jìn)行橫向滑動(dòng)操作痢畜。

@Override

????publicintscrollHorizontallyBy(intdx,?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)不可見(jiàn)的itemView

????????recyclerHideView(dx,?recycler,?state);

????????return?travl;

????}

可以看到鳍侣,滑動(dòng)邏輯很簡(jiǎn)單丁稀,總結(jié)為三步:

橫向滑動(dòng)的時(shí)候,對(duì)左右兩邊按順序填充itemView

滑動(dòng)itemView

回收已經(jīng)不可見(jiàn)的itemView

下面一步一步介紹:

首先第一步倚聚,滑動(dòng)的時(shí)候調(diào)用自定義的 fill() 方法线衫,對(duì)左右兩邊進(jìn)行填充。還沒(méi)忘了惑折,我們是來(lái)實(shí)現(xiàn)循環(huán)滑動(dòng)的授账,所以這一步尤其重要,先看代碼:

/**

*?左右滑動(dòng)的時(shí)候惨驶,填充

*/

????privateintfill(intdx,?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.可見(jiàn)的最后一個(gè)itemView完全滑進(jìn)來(lái)了白热,需要補(bǔ)充新的

????????????if?(lastView.getRight()?<?getWidth())?{

????????????????View?scrap?=?null;

????????????????//標(biāo)注3.判斷可見(jiàn)的最后一個(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)來(lái)并對(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)長(zhǎng),不過(guò)邏輯很清晰休建。首先分為兩部分乍恐,往左填充或是往右填充,dx為將要滑動(dòng)的距離测砂,如果 dx > 0茵烈,則是往左邊滑動(dòng),則需要判斷右邊的邊界砌些,如果最后一個(gè)itemView完全顯示出來(lái)后呜投,在右邊填充一個(gè)新的itemView加匈。

看標(biāo)注3,往右邊填充的時(shí)候需要檢測(cè)當(dāng)前最后一個(gè)可見(jiàn)itemView的索引仑荐,如果索引是最后一個(gè)雕拼,則需要新填充的itemView為第0個(gè),這樣就可以實(shí)現(xiàn)往左邊滑動(dòng)時(shí)候無(wú)限循環(huán)了粘招。然后將需要新填充的itemView進(jìn)行測(cè)量布局操作啥寇,將填充進(jìn)去了。

同理洒扎,往右滑動(dòng)的邏輯跟往左滑動(dòng)相似,就不一一再闡述了辑甜。

第二步:填充完新的itemView后,就開(kāi)始進(jìn)行滑動(dòng)了袍冷,這里直接調(diào)用 LayoutManager 的 offsetChildrenHorizontal() 方法滑動(dòng)-travl 距離磷醋,travl 是通過(guò)fill方法計(jì)算出來(lái)的,通常情況下都為 dx胡诗,只有當(dāng)滑動(dòng)到最后一個(gè)itemView邓线,并且循環(huán)滾動(dòng)開(kāi)關(guān)沒(méi)有打開(kāi)的時(shí)候才為0,也就是不滾動(dòng)了煌恢。

//2.滾動(dòng)

????????offsetChildrenHorizontal(travl?*?-1);

復(fù)制代碼第三步:回收已經(jīng)不可見(jiàn)的itemView骇陈。只有對(duì)不可見(jiàn)的itemView進(jìn)行回收,才能做到回收利用症虑,防止內(nèi)存爆增。

????/**

*?回收界面不可見(jiàn)的view

*/

????privatevoidrecyclerHideView(intdx,?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());

????????????????}

????????????}

????????}

????}

代碼也很簡(jiǎn)單,遍歷所有添加進(jìn) RecyclerView 里的item主籍,然后根據(jù) itemView 的頂點(diǎn)位置進(jìn)行判斷习贫,移除不可見(jiàn)的item。移除 itemView 調(diào)用 removeAndRecycleView(view, recycler) 方法千元,會(huì)對(duì)移除的item進(jìn)行回收苫昌,然后存入 RecyclerView 的緩存里。

至此幸海,一個(gè)可以實(shí)現(xiàn)左右無(wú)限循環(huán)的LayoutManager就實(shí)現(xiàn)了祟身,調(diào)用方式跟通常我們用RrcyclerView沒(méi)有任何區(qū)別,只需要給 RecyclerView 設(shè)置 LayoutManager 時(shí)指定我們的LayoutManager物独,如下:

recyclerView.setAdapter(new?MyAdapter());

????????LooperLayoutManager?layoutManager?=?new?LooperLayoutManager();

????????layoutManager.setLooperEnable(true);

????????recyclerView.setLayoutManager(layoutManager);

源碼地址:

https://github.com/Xiasm/LooperLayoutManager

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袜硫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挡篓,更是在濱河造成了極大的恐慌婉陷,老刑警劉巖帚称,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秽澳,居然都是意外死亡闯睹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門担神,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)楼吃,“玉大人,你說(shuō)我怎么就攤上這事杏瞻∷叮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵捞挥,是天一觀的道長(zhǎng)浮创。 經(jīng)常有香客問(wèn)我,道長(zhǎng)砌函,這世上最難降的妖魔是什么斩披? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮讹俊,結(jié)果婚禮上垦沉,老公的妹妹穿的比我還像新娘。我一直安慰自己仍劈,他們只是感情好厕倍,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著贩疙,像睡著了一般讹弯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上这溅,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天组民,我揣著相機(jī)與錄音,去河邊找鬼悲靴。 笑死臭胜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的癞尚。 我是一名探鬼主播耸三,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼浇揩!你這毒婦竟也來(lái)了吕晌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤临燃,失蹤者是張志新(化名)和其女友劉穎睛驳,沒(méi)想到半個(gè)月后烙心,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乏沸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年淫茵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹬跃。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡匙瘪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝶缀,到底是詐尸還是另有隱情丹喻,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布翁都,位于F島的核電站碍论,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏柄慰。R本人自食惡果不足惜鳍悠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坐搔。 院中可真熱鬧藏研,春花似錦、人聲如沸概行。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凳忙。三九已至业踏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間消略,已是汗流浹背堡称。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工瞎抛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艺演,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓桐臊,卻偏偏與公主長(zhǎng)得像胎撤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子断凶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 背景 項(xiàng)目中要實(shí)現(xiàn)橫向列表的無(wú)限循環(huán)滾動(dòng)伤提,自然而然想到了RecyclerView,但我們常用的RecyclerVi...
    xiasem閱讀 14,991評(píng)論 8 22
  • 這篇文章分三個(gè)部分认烁,簡(jiǎn)單跟大家講一下 RecyclerView 的常用方法與奇葩用法肿男;工作原理與ListView比...
    LucasAdam閱讀 4,377評(píng)論 0 27
  • 背景 項(xiàng)目中要實(shí)現(xiàn)橫向列表的無(wú)限循環(huán)滾動(dòng)介汹,自然而然想到了RecyclerView,但我們常用的RecyclerVi...
    Fitz_e74a閱讀 961評(píng)論 0 6
  • 你一定知道你不想要的舶沛,但你是否真的可以改變它嘹承? 因?yàn)橄敫淖儯圆庞袆?chuàng)業(yè)如庭。 《精益創(chuàng)業(yè)》涉及的理論知識(shí)較多叹卷,經(jīng)過(guò)幾...
    貝加大人閱讀 403評(píng)論 0 0
  • 經(jīng)濟(jì)學(xué)有一個(gè)“口紅效應(yīng)”骤竹,是指因經(jīng)濟(jì)蕭條而導(dǎo)致口紅熱賣的一種有趣的經(jīng)濟(jì)現(xiàn)象。在美國(guó)往毡,每當(dāng)在經(jīng)濟(jì)不景氣時(shí)蒙揣,口紅的銷量...
    打折的楊過(guò)閱讀 1,478評(píng)論 0 3