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

这篇具有很好参考价值的文章主要介绍了Android 无限循环RecyclerView的完美实现方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Android 无限循环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,直接返回如下就行。

	@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;
        }
        //preLayout主要支持动画,直接跳过
        if (state.isPreLayout()) {
            return;
        }
        //将视图分离放入scrap缓存中,以准备重新对view进行排版
        detachAndScrapAttachedViews(recycler);

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

            autualWidth += width;
            //如果当前布局过的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.左右滑动的时候,填充子view
        int travl = fill(dx, recycler, state);
        if (travl == 0) {
            return 0;
        }

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

        //3.回收已经离开界面的
        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) {
                //向左滚动,移除一个左边不在内容里的view
                if (view.getRight() < 0) {
                    removeAndRecycleView(view, recycler);
                    Log.d(TAG, "循环: 移除 一个view  childCount=" + getChildCount());
                }
            } else {
                //向右滚动,移除一个右边不在内容里的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,如下:

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

访问源码请点我文章来源地址https://www.toymoban.com/news/detail-819409.html

自定义竖向LayoutManager
import android.view.View;
import android.view.ViewGroup;

import androidx.recyclerview.widget.RecyclerView;

/***
 * 无限竖向滚动的LayoutManager
 * 1.要求RecyclerView在布局中必须控制高度为准确值/match_parent
 */
public class LooperVerticalLayoutManager extends RecyclerView.LayoutManager {
    private static final String TAG = "LooperLayoutManager";
    private boolean looperEnable = true;

    public LooperVerticalLayoutManager() {
    }

    public void setLooperEnable(boolean looperEnable) {
        this.looperEnable = looperEnable;
    }

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

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

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0) {
            return;
        }
        //preLayout主要支持动画,直接跳过
        if (state.isPreLayout()) {
            return;
        }
        //将视图分离放入scrap缓存中,以准备重新对view进行排版
        detachAndScrapAttachedViews(recycler);

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

            autualHeight += height;
            //如果当前布局过的itemView的宽度总和大于RecyclerView的宽,则不再进行布局
            if (autualHeight > getHeight()) {
                break;
            }
        }
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //1.左右滑动的时候,填充子view
        int travl = fill(dy, recycler, state);
        if (travl == 0) {
            return 0;
        }

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

        //3.回收已经离开界面的
        recyclerHideView(dy, recycler, state);
        return travl;
    }

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

            if (firstView.getTop() >= 0) {
                View scrap = null;
                if (firstPos == 0) {
                    if (looperEnable) {
                        scrap = recycler.getViewForPosition(getItemCount() - 1);
                    } else {
                        dy = 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, 0, firstView.getTop() - height,
                        width, firstView.getTop());
            }
        }
        return dy;
    }

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

到了这里,关于Android 无限循环RecyclerView的完美实现方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android PopupWindow+RecyclerView 实现二级联动筛选

    这篇文章主要的功能是利用 PopupWindow 和  RecyclerView 实现条件筛选包括二级联动筛选,主要是仿小红书里的筛选功能而写的一个 Demo 效果如下,代码通俗易懂,保姆级教程 这里我模拟实际接口返回的数据而准备的数据源,在工程目录下新建 assets 资源文件,在新建一个JsonDat

    2024年02月13日
    浏览(63)
  • Android 实现 RecyclerView下拉刷新,SwipeRefreshLayout上拉加载

    上拉、下拉的效果图如下: 使用步骤 1、在清单文件中添加依赖 implementation ‘com.android.support:recyclerview-v7:27.1.1’ implementation “androidx.swiperefreshlayout:swiperefreshlayout:1.0.0” 2、main布局 item.xml footview.xml(底部提示) 2、MyAdapter 3、MainActivity实现

    2024年02月13日
    浏览(37)
  • Android的RecyclerView实现列表拖动(移动顺序)交换数据位置

    1.先看效果图 这是拖动前的图片 这是拖动列表改变位置后的图片 这里放上视频演示 RecyclerView实现列表拖动交换数据位置 2.主活动MainActivity2类代码如下

    2024年02月12日
    浏览(42)
  • 如何在 Android 应用中使用 RecyclerView 实现一个列表显示,并实现点击事件?

    首先,需要在项目的 build.gradle 文件中添加 RecyclerView 的依赖: 接下来,在布局文件中添加 RecyclerView: 接着,需要创建一个 Adapter 类,用于将数据绑定到 RecyclerView 上,如下所示: 在 onBindViewHolder() 方法中,我们可以将数据绑定到 ViewHolder 中的视图上。 需要注意的是,在 V

    2024年02月05日
    浏览(41)
  • Android RecyclerView实现购物车功能(完善详解篇-保姆级教程)

    购物车实现图片: 首先新建model 随便一个名字 ,例如ShoppingCart 功能一: RecyclerView布局的实现 ①创建MainActivity ②在MainActivity布局中添加RecyclerView组件 ◼ 布局位置在layout中如图位置: recyclerview如图中间部分: (注:其他布局可自己通过拖动组件实现) ◼ activity_main.xml的布局

    2024年02月04日
    浏览(75)
  • 【Android】RecyclerView实现列表中的Item之间设置间距的一种方式

    RecyclerView 的 Item 默认没有间距是因为 RecyclerView 是一个高度自定义的控件,它的目标是提供一个高效灵活的列表展示,并且适应各种不同的布局需求。 为了让开发者能够充分自定义列表项的布局和样式,RecyclerView 没有默认设置项来添加 item 之间的间距。这样设计的好处是,

    2024年02月13日
    浏览(40)
  • Android kotlin 实现仿淘宝RecyclerView和对应下的指示器功能

    指示器样式 , 第二个gif是用模拟器的,gif有小问题,第三个截图没问题 在 app 的 build.gradle 在添加以下代码 1、 implementation \\\'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.6\\\' ,这个里面带的适配器,直接调用就即可 这依赖包还需要得到要添加,在 Project 的 build.gradle 在添加以下代码

    2024年02月09日
    浏览(30)
  • Android Studio 控制台中文乱码,解决方案都在这里了,完美解决

    Android Studio 如果不进行配置的话,运行程序时控制台中文乱码问题会非常严重,甚至影响我们对信息的获取和程序的跟踪。 通过历年的开发经验,在本文中我总结出四点用于解决控制台中文乱码问题的方法,希望有助于大家。 注意 :下面根据我日常工作的经验总结,排序的

    2023年04月08日
    浏览(68)
  • Android RecyclerView之最基本使用教程完整示例(列表/分隔线/点击事件响应/水波纹特效等实现)

    前面几篇文章我们学了Listview的一些基本使用和面向实用优化性的一些进阶自定义用法。这篇文章开始学习RecyclerView,还是先从一个最简单的示例开始。 本篇文章我们将用RecyclerView实现如下效果图(实现一个带分隔线有点击事件的列表,类似Listview ArrayAdapter的示例)  Listvi

    2024年02月08日
    浏览(38)
  • Android RecyclerView实现选中Item变色的最精简高效实现(绝不会出现点击时其它item偶尔也被选中现象)

    效果:  核心代码如下: 代码超精简,试一下您就知道了。跟网上其它的教程不同,绝不会出现点击时其它item偶尔也被同步选中改变颜色导致乱串的现象

    2024年02月11日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包