这篇文章主要介绍“RecyclerView无限循环效果怎么实现”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“RecyclerView无限循环效果怎么实现”文章能帮助大家解决问题。
1、修改adpter和数据映射实现
google了一下,有关recyclerView无限循环的博客很多,内容基本一模一样。大部分的博客都提到/使用了一种修改adpter以及数据映射的方式,主要有以下几步:
1. 修改adapter的getItemCount()方法,让其返回Integer.MAX_VALUE
2. 在取item的数据时,使用索引为position % list.size
3. 初始化的时候,让recyclerView滑到近似Integer.MAX_VALUE/2的位置,避免用户滑到边界。
在逛stackOverFlow时找到了这种方案的出处: java - How to cycle through items in Android RecyclerView? - Stack Overflow
这个方法是建立了一个数据和位置的映射关系,因为itemCount无限大,所以用户可以一直滑下去,又因对位置与数据的取余操作,就可以在每经历一个数据的循环后重新开始。看上去RecyclerView就是无限循环的。
很多博客会说这种方法并不好,例如对索引进行了计算/用户可能会滑到边界导致需要再次动态调整到中间之类的。然后自己写了一份自定义layoutManager后觉得用自定义layoutManager的方法更好。
其实我倒不这么觉得。
事实上,这种方法已经可以很好地满足大部分无限循环的场景,并且由于它依然沿用了LinearLayoutManager。就代表列表依旧可以使用LLM(LinearLayoutManager)封装好的布局和缓存机制。
首先索引计算这个谈不上是个问题。至于用户滑到边界的情况,也可以做特殊处理调整位置。(另外真的有人会滑约Integer.MAX_VALUE/2大约1073741823个position吗?
性能上也无需担心。从数字的直觉上,设置这么多item然后初始化scrollToPosition(Integer.MAX_VALUE/2)看上去好像很可怕,性能上可能有问题,会卡顿巴拉巴拉。
实际从初始化到scrollPosition到真正onlayoutChildren系列操作,主要经过了以下几步。
设置mPendingScrollPosition,确定要滑动的位置,然后requestLayout()请求布局;
@Overridepublic void scrollToPosition(int position) { mPendingScrollPosition = position;//更新position mPendingScrollPositionOffset = INVALID_OFFSET; if (mPendingSavedState != null) { mPendingSavedState.invalidateAnchor(); } requestLayout();}
请求布局后会触发recyclerView的dispatchLayout,最终会调用onLayoutChildren进行子View的layout,如官方注释里描述的那样,onLayoutChildren最主要的工作是:确定锚点、layoutState,调用fill填充布局。
onLayoutChildren部分源码:
@Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom. //.............. // 省略,前面主要做了一些异常状态的检测、针对焦点的特殊处理、确定锚点对anchorInfo赋值、偏移量计算 int startOffset; int endOffset; final int firstLayoutDirection; if (mAnchorInfo.mLayoutFromEnd) { // fill towards start updateLayoutStateToFillStart(mAnchorInfo); //根据mAnchorInfo更新layoutState mLayoutState.mExtraFillSpace = extraForStart; fill(recycler, mLayoutState, state, false);//填充 startOffset = mLayoutState.mOffset; final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } // fill towards end updateLayoutStateToFillEnd(mAnchorInfo);//更新layoutState为fill做准备 mLayoutState.mExtraFillSpace = extraForEnd; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false);//填充 endOffset = mLayoutState.mOffset; if (mLayoutState.mAvailable > 0) { // end could not consume all. add more items towards start extraForStart = mLayoutState.mAvailable; updateLayoutStateToFillStart(firstElement, startOffset);//更新layoutState为fill做准备 mLayoutState.mExtraFillSpace = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; } } else { //layoutFromStart 同理,省略 } //try to fix gap , 省略
onLayoutChildren中会调用updateAnchorInfoForLayout更新anchoInfo锚点信息,updateLayoutStateToFillStart/End再根据anchorInfo更新layoutState为fill填充做准备。
fill的源码: `
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // max offset we should set is mFastScroll + available final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; // (不限制layout个数/还有剩余空间) 并且 有剩余数据 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.beginSection("LLM LayoutChunk"); } layoutChunk(recycler, state, layoutState, layoutChunkResult); if (RecyclerView.VERBOSE_TRACING) { TraceCompat.endSection(); } if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState);//回收子view } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } } if (DEBUG) { validateChildOrder(); } return start - layoutState.mAvailable;
fill主要干了两件事:
循环调用layoutChunk布局子view并计算可用空间
回收那些不在屏幕上的view
所以可以清晰地看到LLM是按需layout、回收子view。
就算创建一个无限大的数据集,再进行滑动,它也是如此。可以写一个修改adapter和数据映射来实现无限循环的例子,验证一下我们的猜测:
//adapter关键代码@NonNull@Overridepublic DemoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); Log.d("DemoAdapter","onCreateViewHolder"); return new DemoViewHolder(inflater.inflate(R.layout.item_demo, parent, false));}@Overridepublic void onBindViewHolder(@NonNull DemoViewHolder holder, int position) { Log.d("DemoAdapter","onBindViewHolder: position"+position); String text = mData.get(position % mData.size()); holder.bind(text);}@Overridepublic int getItemCount() { return Integer.MAX_VALUE;}
在代码我们里打印了onCreateViewHolder、onBindViewHolder的情况。我们只要观察这viewHolder的情况,就知道进入界面再滑到Integer.MAX_VALUE/2时会初始化多少item。 `
RecyclerView recyclerView = findViewById(R.id.rv);recyclerView.setAdapter(new DemoAdapter());LinearLayoutManager layoutManager = new LinearLayoutManager(this);layoutManager.setOrientation(RecyclerView.VERTICAL);recyclerView.setLayoutManager(layoutManager);recyclerView.scrollToPosition(Integer.MAX_VALUE/2);
日志打印:
可以看到,页面上共有5个item可见,LLM也按需创建、layout了5个item。
2、自定义layoutManager
找了找网上自定义layoutManager去实现列表循环的博客和代码,拷贝和复制的很多,找不到源头是哪一篇,这里就不贴链接了。大家都是先说第一种修改adapter的方式不好,然后甩了一份自定义layoutManager的代码。
然而自定义layoutManager难点和坑都很多,很容易不小心就踩到,一些博客的代码也有类似问题。 基本的一些坑点在张旭童大佬的博客中有提及, 【Android】掌握自定义LayoutManager
比较常见的问题是:
不计算可用空间和子view消费的空间,layout出所有的子view。相当于抛弃了子view的复用机制
没有合理利用recyclerView的回收机制
没有支持一些常用但比较重要的api的实现,如前面提到的scrollToPosition。
其实最理想的办法是继承LinearLayoutManager然后修改,但由于LinearLayoutManager内部封装的原因,不方便像GridLayoutManager那样去继承LinearLayoutManager然后进行扩展(主要是包外的子类会拿不到layoutState等)。
要实现一个线性布局的layoutManager,最重要的就是实现一个类似LLM的fill(前面有提到过源码,可以翻回去看看)和layoutChunk方法。
(当然,可以照着LLM写一个丐版,本文就是这么做的。)
fill方法很重要,就如同官方注释里所说的,它是一个magic func。
从OnLayoutChildren到触发scroll滑动,都是调用fill来实现布局。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
前面提到过fill主要干了两件事:
循环调用layoutChunk布局子view并计算可用空间
回收那些不在屏幕上的view
而负责子view布局的layoutChunk则和把一个大象放进冰箱一样,主要分三步走:
add子view
measure
layout 并计算消费了多少空间
就像下面这样:
private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler, state); if (view == null) { result.mFinished = true; return; } RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); // add if (layoutState.mLayoutDirection != LayoutState.LAYOUT_START) { addView(view); } else { addView(view, 0); } Rect insets = new Rect(); calculateItemDecorationsForChild(view, insets); // 测量 measureChildWithMargins(view, 0, 0); //布局 layoutChild(view, result, params, layoutState, state); // Consume the available space if the view is not removed OR changed if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable = view.hasFocusable();}
那最关键的如何实现循环呢??
其实和修改adapter的实现方法有异曲同工之妙,本质都是修改位置与数据的映射关系。
修改layoutStae的方法:
boolean hasMore(RecyclerView.State state) { return Math.abs(mCurrentPosition) <= state.getItemCount(); } View next(RecyclerView.Recycler recycler, RecyclerView.State state) { int itemCount = state.getItemCount(); mCurrentPosition = mCurrentPosition % itemCount; if (mCurrentPosition < 0) { mCurrentPosition += itemCount; } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }}
关于“RecyclerView无限循环效果怎么实现”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网行业资讯频道,小编每天都会为大家更新不同的知识点。