您好,登錄后才能下訂單哦!
這篇文章主要介紹“RecyclerView無限循環效果怎么實現”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“RecyclerView無限循環效果怎么實現”文章能幫助大家解決問題。
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()請求布局;
/** * <p>Scroll the RecyclerView to make the position visible.</p> * * <p>RecyclerView will scroll the minimum amount that is necessary to make the * target position visible. If you are looking for a similar behavior to * {@link android.widget.ListView#setSelection(int)} or * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use * {@link #scrollToPositionWithOffset(int, int)}.</p> * * <p>Note that scroll position change will not be reflected until the next layout call.</p> * * @param position Scroll to this adapter position * @see #scrollToPositionWithOffset(int, int) */ @Override public 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部分源碼:
@Override public 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; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ 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 @Override public 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)); } @Override public void onBindViewHolder(@NonNull DemoViewHolder holder, int position) { Log.d("DemoAdapter","onBindViewHolder: position"+position); String text = mData.get(position % mData.size()); holder.bind(text); } @Override public 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。
找了找網上自定義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來實現布局。
/** * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly * independent from the rest of the {@link LinearLayoutManager} * and with little change, can be made publicly available as a helper class. */ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
前面提到過fill主要干了兩件事:
循環調用layoutChunk布局子view并計算可用空間
回收那些不在屏幕上的view
而負責子view布局的layoutChunk則和把一個大象放進冰箱一樣,主要分三步走:
add子view
measure
layout 并計算消費了多少空間
就像下面這樣:
/** * layout具體子view */ 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無限循環效果怎么實現”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。