溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

RecyclerView無限循環(huán)效果怎么實現(xiàn)

發(fā)布時間:2023-05-10 14:44:11 來源:億速云 閱讀:144 作者:zzz 欄目:開發(fā)技術(shù)

這篇文章主要介紹“RecyclerView無限循環(huán)效果怎么實現(xiàn)”的相關(guān)知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“RecyclerView無限循環(huán)效果怎么實現(xiàn)”文章能幫助大家解決問題。

    1、修改adpter和數(shù)據(jù)映射實現(xiàn)

    google了一下,有關(guān)recyclerView無限循環(huán)的博客很多,內(nèi)容基本一模一樣。大部分的博客都提到/使用了一種修改adpter以及數(shù)據(jù)映射的方式,主要有以下幾步:

    1. 修改adapter的getItemCount()方法,讓其返回Integer.MAX_VALUE

    2. 在取item的數(shù)據(jù)時,使用索引為position % list.size

    3. 初始化的時候,讓recyclerView滑到近似Integer.MAX_VALUE/2的位置,避免用戶滑到邊界。

    在逛stackOverFlow時找到了這種方案的出處: java - How to cycle through items in Android RecyclerView? - Stack Overflow

    這個方法是建立了一個數(shù)據(jù)和位置的映射關(guān)系,因為itemCount無限大,所以用戶可以一直滑下去,又因?qū)ξ恢门c數(shù)據(jù)的取余操作,就可以在每經(jīng)歷一個數(shù)據(jù)的循環(huán)后重新開始??瓷先ecyclerView就是無限循環(huán)的。

    很多博客會說這種方法并不好,例如對索引進(jìn)行了計算/用戶可能會滑到邊界導(dǎo)致需要再次動態(tài)調(diào)整到中間之類的。然后自己寫了一份自定義layoutManager后覺得用自定義layoutManager的方法更好。

    其實我倒不這么覺得。

    事實上,這種方法已經(jīng)可以很好地滿足大部分無限循環(huán)的場景,并且由于它依然沿用了LinearLayoutManager。就代表列表依舊可以使用LLM(LinearLayoutManager)封裝好的布局和緩存機制。

    • 首先索引計算這個談不上是個問題。至于用戶滑到邊界的情況,也可以做特殊處理調(diào)整位置。(另外真的有人會滑約Integer.MAX_VALUE/2大約1073741823個position嗎?

    • 性能上也無需擔(dān)心。從數(shù)字的直覺上,設(shè)置這么多item然后初始化scrollToPosition(Integer.MAX_VALUE/2)看上去好像很可怕,性能上可能有問題,會卡頓巴拉巴拉。

    實際從初始化到scrollPosition到真正onlayoutChildren系列操作,主要經(jīng)過了以下幾步。

    • 設(shè)置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();
    }
    • 請求布局后會觸發(fā)recyclerView的dispatchLayout,最終會調(diào)用onLayoutChildren進(jìn)行子View的layout,如官方注釋里描述的那樣,onLayoutChildren最主要的工作是:確定錨點、layoutState,調(diào)用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.
        //..............
        // 省略,前面主要做了一些異常狀態(tài)的檢測、針對焦點的特殊處理、確定錨點對anchorInfo賦值、偏移量計算
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo); //根據(jù)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做準(zhǔn)備
            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做準(zhǔn)備
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            //layoutFromStart 同理,省略
        }
        //try to fix gap , 省略
    • onLayoutChildren中會調(diào)用updateAnchorInfoForLayout更新anchoInfo錨點信息,updateLayoutStateToFillStart/End再根據(jù)anchorInfo更新layoutState為fill填充做準(zhǔn)備。

    • 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 &lt; 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        // (不限制layout個數(shù)/還有剩余空間) 并且 有剩余數(shù)據(jù)
        while ((layoutState.mInfinite || remainingSpace &gt; 0) &amp;&amp; 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 &lt; 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);//回收子view
            }
            if (stopOnFocusable &amp;&amp; layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;

    fill主要干了兩件事:

    • 循環(huán)調(diào)用layoutChunk布局子view并計算可用空間

    • 回收那些不在屏幕上的view

    所以可以清晰地看到LLM是按需layout、回收子view。

    就算創(chuàng)建一個無限大的數(shù)據(jù)集,再進(jìn)行滑動,它也是如此??梢詫懸粋€修改adapter和數(shù)據(jù)映射來實現(xiàn)無限循環(huán)的例子,驗證一下我們的猜測:

    //adapter關(guān)鍵代碼
    @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的情況,就知道進(jìn)入界面再滑到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);

    日志打?。?/p>

    RecyclerView無限循環(huán)效果怎么實現(xiàn)

    可以看到,頁面上共有5個item可見,LLM也按需創(chuàng)建、layout了5個item。

    2、自定義layoutManager

    找了找網(wǎng)上自定義layoutManager去實現(xiàn)列表循環(huán)的博客和代碼,拷貝和復(fù)制的很多,找不到源頭是哪一篇,這里就不貼鏈接了。大家都是先說第一種修改adapter的方式不好,然后甩了一份自定義layoutManager的代碼。

    然而自定義layoutManager難點和坑都很多,很容易不小心就踩到,一些博客的代碼也有類似問題。 基本的一些坑點在張旭童大佬的博客中有提及, 【Android】掌握自定義LayoutManager

    比較常見的問題是:

    • 不計算可用空間和子view消費的空間,layout出所有的子view。相當(dāng)于拋棄了子view的復(fù)用機制

    • 沒有合理利用recyclerView的回收機制

    • 沒有支持一些常用但比較重要的api的實現(xiàn),如前面提到的scrollToPosition。

    其實最理想的辦法是繼承LinearLayoutManager然后修改,但由于LinearLayoutManager內(nèi)部封裝的原因,不方便像GridLayoutManager那樣去繼承LinearLayoutManager然后進(jìn)行擴(kuò)展(主要是包外的子類會拿不到layoutState等)。

    要實現(xiàn)一個線性布局的layoutManager,最重要的就是實現(xiàn)一個類似LLM的fill(前面有提到過源碼,可以翻回去看看)和layoutChunk方法。

    (當(dāng)然,可以照著LLM寫一個丐版,本文就是這么做的。)

    fill方法很重要,就如同官方注釋里所說的,它是一個magic func。

    從OnLayoutChildren到觸發(fā)scroll滑動,都是調(diào)用fill來實現(xiàn)布局。

    /**
     * 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主要干了兩件事:

    • 循環(huán)調(diào)用layoutChunk布局子view并計算可用空間

    • 回收那些不在屏幕上的view

    而負(fù)責(zé)子view布局的layoutChunk則和把一個大象放進(jìn)冰箱一樣,主要分三步走:

    • 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();
    }

    那最關(guān)鍵的如何實現(xiàn)循環(huán)呢??

    其實和修改adapter的實現(xiàn)方法有異曲同工之妙,本質(zhì)都是修改位置與數(shù)據(jù)的映射關(guān)系。

    修改layoutStae的方法:

        boolean hasMore(RecyclerView.State state) {
            return Math.abs(mCurrentPosition) &lt;= state.getItemCount();
        }
        View next(RecyclerView.Recycler recycler, RecyclerView.State state) {
            int itemCount = state.getItemCount();
            mCurrentPosition = mCurrentPosition % itemCount;
            if (mCurrentPosition &lt; 0) {
                mCurrentPosition += itemCount;
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
    }

    關(guān)于“RecyclerView無限循環(huán)效果怎么實現(xiàn)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會為大家更新不同的知識點。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI