RecyclerView无限循环效果实现及示例解析

目录
  • 前言
  • 有现成的轮子吗?
    • 1、修改adpter和数据映射实现
    • 2、自定义layoutManager
  • 后记

前言

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

熟悉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的方法更好。

其实我倒不这么觉得。

事实上,这种方法已经可以很好地满足大部分无限循环的场景,并且由于它依然沿用了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 &lt; 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // (不限制layout个数/还有剩余空间) 并且 有剩余数据
    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主要干了两件事:

  • 循环调用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效果:

日志打印:

可以看到,页面上共有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来实现布局。

/**
 * 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) &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;
    }
}

最终效果:

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

注:也可以直接引用依赖使用,详见readme.md。

后记

本文介绍了recyclerview无限循环效果的两种不同实现方法与解析。

虽然自定义layoutManager坑点很多并且很少用的到,但了解下也会对recyclerView有更深的理解。

以上就是RecyclerView无限循环效果实现及示例解析的详细内容,更多关于RecyclerView无限循环的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android 源码浅析RecyclerView Adapter

    目录 引言 源码分析 RecyclerViewDataObserver AdapterDataObservable Adapter AdapterHelper notifyDataSetChanged 最后 引言 在使用 RecyclerView 时 Adapter 也是必备的,在对其进行增删改操作时会用到以下方法: recyclerView.setAdapter(adapter) adapter.notifyItemInserted(index) adapter.notifyItemChang

  • Android 源码浅析RecyclerView ItemAnimator

    目录 前言 源码分析 前置基础 ItemHolderInfo InfoRecord ViewInfoStore ProcessCallback 动画处理 dispatchLayoutStep3 dispatchLayoutStep1 dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3 总结 动画执行 ItemAnimator SimpleItemAnimator DefaultItemAnimator 最后 前言 在这个系列博客

  • RecyclerView 源码浅析测量 布局 绘制 预布局

    目录 前言 onMeasure onLayout onDraw dispatchLayoutStep1.2.3 dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3 mAttachedScrap 和 mChangedScrap 预布局 最后 前言 上一篇博客内容对 RecyclerView 回收复用机制相关源码进行了分析,本博客从自定义 View 三大流程 measure.layout.draw 的角度继续对 RecyclerVi

  • Android 实现通知消息水平播放、无限循环效果

    今天我们来实现一个简单的效果,通知消息无限循环播放,先看效果图: 这个效果也很常见,实现的方法也有很多,我是使用RecyclerView来实现的,觉得还是挺不错的,就写下来分享给大家. 下面先看我们的布局文件main.xml,里面主要是一个RecyclerView: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.andr

  • Vue实现一种简单的无限循环滚动动画的示例

    本文主要介绍了Vue实现一种简单的无限循环滚动动画的示例,分享给大家,具体如下: 先看实现效果: 这种类似轮播的效果,通常可以使用轮播的方案解决,只不过相对于我要分享的方案来说,轮播实现还是要复杂些的. Vue提供了一种过渡动画transition-group,这里我便是利用的这个效果 // template <transition-group name="list-complete" tag="div"> <div v-for="v i

  • IOS实现图片轮播无限循环效果

    本文接着上篇文章进行叙述讲解,主要为大家分享了图片轮播无限循环效果的实现方法,具体内容如下 之前说到第一个问题,ScrollView移动到最后一张图片时无法移动了,这是因为ScrollView已经移动到最后,而图片又是依次排列,自然也就无法移动. 解决办法是,我们换一个思路实现图片轮播效果,ScrollView上只放三个ImageView,屏幕始终显示中间的ImageView,左边和右边的ImageView分别代表前一张图片和后一张图片,屏幕移动的时候,中间的ImageView变化,同时左右两边

  • Android实现ViewPager无限循环效果(二)

    本文实例为大家分享了Android实现ViewPager无限循环效果的第二种方式,供大家参考,具体内容如下 原理:在Adapter中将getCount设置为无限大 package com.xiaomai.myproject.demo; import android.os.Bundle; import android.support.v4.view.ViewPager; import android.view.ViewGroup; import android.widget.ImageView;

  • Unity ScrollView实现无限循环效果

    本文实例为大家分享了Unity ScrollView实现无限循环效果的具体代码,供大家参考,具体内容如下 在Unity引擎中ScrollView组件是一个使用率比较高的组件,该组件能上下或者左右拖动的UI列表,背包.展示多个按钮等情况的时候会用到,在做排行榜类似界面时,item非常多,可能有几百个,一次创建这么多GameObject是非常卡的.为此,使用只创建可视区一共显示的个数,加上后置准备个数. 由于ScrollView有两种滚动方式,水平滚动或者垂直滚动,所以我创建了ScrollBase基

  • Android TV 3D卡片无限循环效果

    TV 3D卡片无限循环效果,供大家参考,具体内容如下 ##前言 1.需求:实现3个卡片实现无限循环效果:1-2-3-1-2-3-1-,而且要实现3D效果:中间突出,两侧呈角度显示 2.Viewpager实现方式 (1) LoopViewpager,有兴趣的同学可以去github上看一下. (2) 通过定义一个item的个数Integer,MAX,然后设置初始位置为:Integer,MAX/2. 以上方式如果简单的加载图片这种方式还可取,由于需求3个界面内部控件比较多,在加上需要实现自定义的的3D

  • Android ViewPager实现无限循环效果

    最近项目里有用到ViewPager来做广告运营位展示,看到现在很多APP的广告运营位都是无限循环的,所以就研究了一下这个功能的实现. 先看看效果 从一个方向上一直滑动,么有滑到尽头的感觉,具体是怎么实现的呢?看下面的思路. 实现思路 此处画了一幅图来表达实现无限循环的思路,即在数据起始位置前插入最后一项数据,在最后一项数据后插入第一项数据,当滑动到此处时,更新页面的索引位置就ok了 . 代码实现 这个方法用于数据处理,其中mediaList是原始数据,newMediaList是处理完的数据,mM

  • Android ViewPager导航小圆点实现无限循环效果

    之前用View Pager做了一个图片切换的推荐栏(就类似与淘宝.头条客户端顶端的推荐信息栏),利用View Pager很快就能实现,但是一次无意间使用淘宝APP的时候,突然发现它的效果和我做的还不一样,淘宝APP的推荐栏可以左右无限循环切换,而ViewPager自身其实并没有支持这个功能. 其实实现这个无限循环不难,只需要在数据源的首尾各添加一张多余的图片,在onPagerChangeListener()中监听position<1和position>(总数据条目-1)就可以了.另外一点需要注

  • Android实现ViewPager无限循环效果(一)

    本文实例为大家分享了Android实现ViewPager无限循环的具体代码,供大家参考,具体内容如下 方式一: 实现原理: 假设有3张图片,分别是1,2,3,那么就创建5张图片,这5张图片的顺序为:3,1,2,3,1,其中1,2,3为我们要实现滑动的图片,最左面的3和最右面的1是我们另外添加的图片,开始时,显示图片1,当图片向左滑动依次为1,2,3,当从第3张图片继续向左滑动,会出现我们多添加的图片1,这时,将当前的index设置为真正的图片1所在的位置. package com.xiaomai

  • Android实现轮播图无限循环效果

    本文实例为大家分享了Android轮播图无限循环的具体代码,供大家参考,具体内容如下 实现无限循环 在getCount()方法中,返回一个很大的值,Integer.MAX_VALUE 在instantiateItem()方法中,获取当前View的索引时,进行取于操作,传递进来的int position是个非常大的数,对他进行求余数 在destroyItem()方法中,同样 在onPageSelected()监听方法中,对传递进来的索引进行取于 反向的无限循环 调用ViewPager对象的setC

随机推荐