当前位置: 首页 > 编程笔记 >

Android无限循环RecyclerView的完美实现方案

海翼
2023-03-14
本文向大家介绍Android无限循环RecyclerView的完美实现方案,包括了Android无限循环RecyclerView的完美实现方案的使用技巧和注意事项,需要的朋友参考一下

背景

项目中要实现横向列表的无限循环滚动,自然而然想到了RecyclerView,但我们常用的RecyclerView是不支持无限循环滚动的,所以就需要一些办法让它能够无限循环。

方案选择

方案1 对Adapter进行修改

网上大部分博客的解决方案都是这种方案,对Adapter做修改。具体如下

首先,让 Adapter 的 getItemCount() 方法返回 Integer.MAX_VALUE,使得position数据达到很大很大;

其次,在 onBindViewHolder() 方法里对position参数取余运算,拿到position对应的真实数据索引,然后对itemView绑定数据

最后,在初始化RecyclerView的时候,让其滑动到指定位置,如 Integer.MAX_VALUE/2,这样就不会滑动到边界了,如果用户一根筋,真的滑动到了边界位置,再加一个判断,如果当前索引是0,就重新动态调整到初始位置

这个方案是挺简单,但并不完美。一是对我们的数据和索引做了计算操作,二是如果滑动到边界,再动态调整到中间,会有一个不明显的卡顿操作,使得滑动不是很顺畅。所以,直接看方案二。

方案2 自定义LayoutManager,修改RecyclerView的布局方式

这个算得上是一劳永逸的解决方案了,也是我今天要详细介绍的方案。我们都知道,RecyclerView的数据绑定是通过Adapter来处理的,而排版方式以及View的回收控制等,则是通过LayoutManager来实现的,因此我们直接修改itemView的排版方式就可以实现我们的目标,让RecyclerView无限循环。

自定义LayoutManager

1.创建自定义LayoutManager

首先,自定义 LooperLayoutManager 继承自 RecyclerView.LayoutManager,然后需要实现抽象方法 generateDefaultLayoutParams(),这个方法的作用是给 itemView 设置默认的LayoutParams,直接返回如下就行。

public class LooperLayoutManager extends RecyclerView.LayoutManager {
    @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT);
  }
}

2.打开滚动开关

接着,对滚动方向做处理,重写canScrollHorizontally()方法,打开横向滚动开关。注意我们是实现横向无限循环滚动,所以实现此方法,如果要对垂直滚动做处理,则要实现canScrollVertically()方法。

  @Override
  public boolean canScrollHorizontally() {
    return true;
  }

3.对RecyclerView进行初始化布局

好了,以上两部是基础工作,接下来,重写 onLayoutChildren() 方法,开始对itemView初始化布局。

  @Override
  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0) {
      return;
    }
    //标注1.如果当前时准备状态,直接返回
    if (state.isPreLayout()) {
      return;
    }
    //标注2.将视图分离放入scrap缓存中,以准备重新对view进行排版
    detachAndScrapAttachedViews(recycler);

    int autualWidth = 0;
    for (int i = 0; i < getItemCount(); i++) {
      //标注3.初始化,将在屏幕内的view填充
      View itemView = recycler.getViewForPosition(i);
      addView(itemView);
      //标注4.测量itemView的宽高
      measureChildWithMargins(itemView, 0, 0);
      int width = getDecoratedMeasuredWidth(itemView);
      int height = getDecoratedMeasuredHeight(itemView);
      //标注5.根据itemView的宽高进行布局
      layoutDecorated(itemView, autualWidth, 0, autualWidth + width, height);

      autualWidth += width;
      //标注6.如果当前布局过的itemView的宽度总和大于RecyclerView的宽,则不再进行布局
      if (autualWidth > getWidth()) {
        break;
      }
    }
  }

onLayoutChildren() 方法顾名思义,就是对所有的 itemView 进行布局,一般会在初始化和调用 Adapter 的 notifyDataSetChanged() 方法时调用。代码思路已经注释的很清楚了,其中有几个方法需要简单提下:

标注2处 detachAndScrapAttachedViews(recycler) 方法会将所有的 itemView 从View树中全部detach,然后放入scrap缓存中。了解过RecyclerView的同学应该知道,RecyclerView是有一个二级缓存的,一级缓存是 scrap 缓存,二级缓存是 recycler 缓存,其中从View树上detach的View会放入scrap缓存里,调用removeView()删除的View会放入recycler缓存中。

标注3处 recycler.getViewForPosition(i) 方法会从缓存中拿到对应索引的 itemView,这个方法内部会先从 scrap 缓存中取 itemView,如果没有则从 recycler 缓存中取,如果还没有则调用 adapter 的 onCreateViewHolder() 去创建 itemView。

标注5处 layoutDecorated() 方法会对 itemView 进行布局排版,这里可以看出来,我们是根据宽依次往父容器的右边排下去,直到下一个 itemView的顶点位置超过了RecyclerView 的宽度。

4.对RecyclerView进行滚动和回收itemView处理

对RecyclerView的子item进行排版布局后,运行一下效果就会出现了,不过这时候我们滑动列表会发现滑动后变成空白了,所以就该对滑动操作进行处理了。

前面说过,我们打开了横向滚动的开关,所以对应的,我们要重写 scrollHorizontallyBy()方法进行横向滑动操作。

@Override
  public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //标注1.横向滑动的时候,对左右两边按顺序填充itemView
    int travl = fill(dx, recycler, state);
    if (travl == 0) {
      return 0;
    }

    //2.滑动
    offsetChildrenHorizontal(-travl);

    //3.回收已经不可见的itemView
    recyclerHideView(dx, recycler, state);
    return travl;
  }

可以看到,滑动逻辑很简单,总结为三步:

  • 横向滑动的时候,对左右两边按顺序填充itemView
  • 滑动itemView
  • 回收已经不可见的itemView

下面一步一步介绍:

首先第一步,滑动的时候调用自定义的 fill() 方法,对左右两边进行填充。还没忘了,我们是来实现循环滑动的,所以这一步尤其重要,先看代码:

  /**
   * 左右滑动的时候,填充
   */
  private int fill(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (dx > 0) {
      //标注1.向左滚动
      View lastView = getChildAt(getChildCount() - 1);
      if (lastView == null) {
        return 0;
      }
      int lastPos = getPosition(lastView);
      //标注2.可见的最后一个itemView完全滑进来了,需要补充新的
      if (lastView.getRight() < getWidth()) {
        View scrap = null;
        //标注3.判断可见的最后一个itemView的索引,
        // 如果是最后一个,则将下一个itemView设置为第一个,否则设置为当前索引的下一个
        if (lastPos == getItemCount() - 1) {
          if (looperEnable) {
            scrap = recycler.getViewForPosition(0);
          } else {
            dx = 0;
          }
        } else {
          scrap = recycler.getViewForPosition(lastPos + 1);
        }
        if (scrap == null) {
          return dx;
        }
        //标注4.将新的itemViewadd进来并对其测量和布局
        addView(scrap);
        measureChildWithMargins(scrap, 0, 0);
        int width = getDecoratedMeasuredWidth(scrap);
        int height = getDecoratedMeasuredHeight(scrap);
        layoutDecorated(scrap,lastView.getRight(), 0,
            lastView.getRight() + width, height);
        return dx;
      }
    } else {
      //向右滚动
      View firstView = getChildAt(0);
      if (firstView == null) {
        return 0;
      }
      int firstPos = getPosition(firstView);

      if (firstView.getLeft() >= 0) {
        View scrap = null;
        if (firstPos == 0) {
          if (looperEnable) {
            scrap = recycler.getViewForPosition(getItemCount() - 1);
          } else {
            dx = 0;
          }
        } else {
          scrap = recycler.getViewForPosition(firstPos - 1);
        }
        if (scrap == null) {
          return 0;
        }
        addView(scrap, 0);
        measureChildWithMargins(scrap,0,0);
        int width = getDecoratedMeasuredWidth(scrap);
        int height = getDecoratedMeasuredHeight(scrap);
        layoutDecorated(scrap, firstView.getLeft() - width, 0,
            firstView.getLeft(), height);
      }
    }
    return dx;
  }

代码是有点长,不过逻辑很清晰。首先分为两部分,往左填充或是往右填充,dx为将要滑动的距离,如果 dx > 0,则是往左边滑动,则需要判断右边的边界,如果最后一个itemView完全显示出来后,在右边填充一个新的itemView。
看标注3,往右边填充的时候需要检测当前最后一个可见itemView的索引,如果索引是最后一个,则需要新填充的itemView为第0个,这样就可以实现往左边滑动时候无限循环了。然后将需要新填充的itemView进行测量布局操作,将填充进去了。

同理,往右滑动的逻辑跟往左滑动相似,就不一一再阐述了。

第二步:填充完新的itemView后,就开始进行滑动了,这里直接调用 LayoutManager 的 offsetChildrenHorizontal() 方法滑动-travl 距离,travl 是通过fill方法计算出来的,通常情况下都为 dx,只有当滑动到最后一个itemView,并且循环滚动开关没有打开的时候才为0,也就是不滚动了。

    //2.滚动
    offsetChildrenHorizontal(travl * -1);

第三步:回收已经不可见的itemView。只有对不可见的itemView进行回收,才能做到回收利用,防止内存爆增。

  /**
   * 回收界面不可见的view
   */
  private void recyclerHideView(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    for (int i = 0; i < getChildCount(); i++) {
      View view = getChildAt(i);
      if (view == null) {
        continue;
      }
      if (dx > 0) {
        //标注1.向左滚动,移除左边不在内容里的view
        if (view.getRight() < 0) {
          removeAndRecycleView(view, recycler);
          Log.d(TAG, "循环: 移除 一个view childCount=" + getChildCount());
        }
      } else {
        //标注2.向右滚动,移除右边不在内容里的view
        if (view.getLeft() > getWidth()) {
          removeAndRecycleView(view, recycler);
          Log.d(TAG, "循环: 移除 一个view childCount=" + getChildCount());
        }
      }
    }

  }

代码也很简单,遍历所有添加进 RecyclerView 里的item,然后根据 itemView 的顶点位置进行判断,移除不可见的item。移除 itemView 调用 removeAndRecycleView(view, recycler) 方法,会对移除的item进行回收,然后存入 RecyclerView 的缓存里。

至此,一个可以实现左右无限循环的LayoutManager就实现了,调用方式跟通常我们用RrcyclerView没有任何区别,只需要给 RecyclerView 设置 LayoutManager 时指定我们的LayoutManager,如下:

    recyclerView.setAdapter(new MyAdapter());
    LooperLayoutManager layoutManager = new LooperLayoutManager();
    layoutManager.setLooperEnable(true);
    recyclerView.setLayoutManager(layoutManager);

访问源码请点我

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。

 类似资料:
  • 本文向大家介绍Android ViewPager实现无限循环的实例,包括了Android ViewPager实现无限循环的实例的使用技巧和注意事项,需要的朋友参考一下 Android ViewPager实现无限循环的实例 ViewPager自身并不支持左右无限循环的功能,这里就提供一种方案让Android ViewPager实现左右无限循环的功能,这里记录下: 用于显示的mViews,比数据源mL

  • 本文向大家介绍Android ViewPager实现无限循环效果,包括了Android ViewPager实现无限循环效果的使用技巧和注意事项,需要的朋友参考一下 最近项目里有用到ViewPager来做广告运营位展示,看到现在很多APP的广告运营位都是无限循环的,所以就研究了一下这个功能的实现。 先看看效果 从一个方向上一直滑动,么有滑到尽头的感觉,具体是怎么实现的呢?看下面的思路。 实现思路 此

  • 本文向大家介绍Android实现ViewPager无限循环效果(二),包括了Android实现ViewPager无限循环效果(二)的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了Android实现ViewPager无限循环效果的第二种方式,供大家参考,具体内容如下 原理:在Adapter中将getCount设置为无限大 代码: act_loopviewpager.xml 以上就是本文

  • 本文向大家介绍Android实现ViewPager无限循环效果(一),包括了Android实现ViewPager无限循环效果(一)的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了Android实现ViewPager无限循环的具体代码,供大家参考,具体内容如下 方式一: 实现原理: 假设有3张图片,分别是1,2,3,那么就创建5张图片,这5张图片的顺序为:3,1,2,3,1,其中1,2

  • 本文向大家介绍Android Viewpager实现无限循环轮播图,包括了Android Viewpager实现无限循环轮播图的使用技巧和注意事项,需要的朋友参考一下 在网上找了很多viewpager实现图片轮播的,但是大多数通过以下方式在PagerAdapter的getCount()返回一个无限大的数,来实现 伪无限 虽然通过这种方式是能达到效果,但是从严格意义上来说并不是真正的无限。 假如有五

  • 我想实现一个无限循环数据集 如您所见,这里的主要挑战是方法。如果我在那里放一个足够大的数字,比如1 如果我在那里放一个小数字,比如1或BATCH_SIZE,训练循环中采样的“数据”将定期重复。这不是我想要的,因为我想产生新的数据 我猜过度使用内存的罪魁祸首是堆栈中的某个地方,缓存了一堆东西。随便看看Python的一面,我就不知道在哪里了。 有人能告诉我什么是实现我想要的最好的方法吗?(使用Data

  • 本文向大家介绍Android编程实现ListView内容无限循环显示的方法,包括了Android编程实现ListView内容无限循环显示的方法的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了Android编程实现ListView内容无限循环显示的方法。分享给大家供大家参考,具体如下: 其实要达到无限循环显示,主要就是实现继承Adapter的类。 我这里用到的是BaseAdapter 在on

  • 本文向大家介绍Android 使用viewpager实现无限循环(定时+手动),包括了Android 使用viewpager实现无限循环(定时+手动)的使用技巧和注意事项,需要的朋友参考一下 循环轮播的方法有两种,一种是使用定时器另外一种是使用手指拨动,相比较而言,定时器实现循环播放比较容易,只要在定时器的消息里加上简单几段代码即可,下面分别就这两种方法给大家详解,具体详情请看下文吧。 但如何在手