前语

前两天在逛博客的时候发现了这样一张直播间的截图,其中这个商品列表的切换和循环播映作用感觉挺好:

RecyclerView无限循环效果实现与解析

了解android的同学应该很快能想到这是recyclerView完成的线性列表,其首要有两个作用:

1.顶部item切换后样式扩大+转场动画。
2.列表自动、无限循环播映。

第一个作用比较好完成,顶部item布局的改变能够经过对RecyclerView进行OnScroll监听,判别item方位,做scale缩放。或许在自定义layoutManager在做layoutChild相关操作时判别第一个可见的item并修正样式。

自动播映则能够经过运用手势判别+延时使命来做。

本文首要供给关于第二个无限循环播映作用的自定义LayoutManager的完成。

正文

有现成的轮子吗?

先看看有没有合适的轮子,“不要重复造轮子”,除非轮子不满足需求。

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的办法更好。

RecyclerView无限循环效果实现与解析

其实我倒不这么觉得。

事实上,这种办法现已能够很好地满足大部分无限循环的场景,而且因为它仍然沿用了LinearLayoutManager。就代表列表仍旧能够运用LLM(LinearLayoutManager)封装好的布局和缓存机制。

  1. 首要索引核算这个谈不上是个问题。至于用户滑到边界的情况,也能够做特别处理调整方位。(别的真的有人会滑约Integer.MAX_VALUE/2大约1073741823个position吗?

  2. 性能上也无需忧虑。从数字的直觉上,设置这么多item然后初始化scrollToPosition(Integer.MAX_VALUE/2)看上去好像很可怕,性能上可能有问题,会卡顿巴拉巴拉。

实践从初始化到scrollPosition到真正onlayoutChildren系列操作,首要经过了以下几步。

先上一张流程图:

RecyclerView无限循环效果实现与解析

  • 设置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);

初始化后ui作用:

RecyclerView无限循环效果实现与解析

日志打印:

RecyclerView无限循环效果实现与解析

能够看到,页面上共有5个item可见,LLM也按需创立、layout了5个item。

2、自定义layoutManager

找了找网上自定义layoutManager去完成列表循环的博客和代码,拷贝和仿制的许多,找不到源头是哪一篇,这里就不贴链接了。大家都是先说第一种修正adapter的方式不好,然后甩了一份自定义layoutManager的代码。

但是自定义layoutManager难点和坑都许多,很容易不小心就踩到,一些博客的代码也有类似问题。 根本的一些坑点在张旭童大佬的博客中有提及, 【Android】掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。

比较常见的问题是:

  1. 不核算可用空间和子view消费的空间,layout出所有的子view。相当于扔掉了子view的复用机制
  2. 没有合理运用recyclerView的收回机制
  3. 没有支撑一些常用但比较重要的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则和把一个大象放进冰箱一样,首要分三步走:

  1. add子view
  2. measure
  3. layout 并核算消费了多少空间
RecyclerView无限循环效果实现与解析

就像下面这样:

/**
 * 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无限循环效果实现与解析

源码地址:aFlyFatPig/cycleLayoutManager (github.com)

注:也能够直接引证依赖运用,详见readme.md。

跋文

本文介绍了recyclerview无限循环作用的两种不同完成办法与解析。

尽管自定义layoutManager坑点许多而且很少用的到,但了解下也会对recyclerView有更深的了解。