如何创建一个循环(无尽的)RecyclerView?

Posted

技术标签:

【中文标题】如何创建一个循环(无尽的)RecyclerView?【英文标题】:How do I create a circular (endless) RecyclerView? 【发布时间】:2015-09-24 01:18:58 【问题描述】:

我正在尝试让我的 RecyclerView 循环回到我列表的开头。

我在整个互联网上进行了搜索,并设法检测到我何时到达了列表的末尾,但是我不确定从哪里开始。

这是我目前用来检测列表末尾的(找到here):

 @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) 

        visibleItemCount = mLayoutManager.getChildCount();
        totalItemCount = mLayoutManager.getItemCount();
        pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();

        if (loading) 
            if ( (visibleItemCount+pastVisiblesItems) >= totalItemCount) 
                loading = false;
                Log.v("...", ""+visibleItemCount);
            
        
 

当滚动到末尾时,我希望视图在从列表顶部显示数据时可见,或者当滚动到列表顶部时,我会从列表底部显示数据。

例如:

查看1 查看2 查看3 查看4 查看5

查看5 查看1 查看2 查看3 查看4

【问题讨论】:

@DamianKozlak RecyclerView 与 ListView 不同 @ChadBingham 当然不是,但是可以使用与ListView 中相同的解决方案来解决此问题。 ListViewRecyclerView 适配器都有 getCount()getItem() 方法。 您找到解决方案了吗?我正在解决同样的问题,目前我正在考虑编写一个从 LinearLayoutManager 扩展的自定义 LayoutManager。你有没有实现过你的目标? 是的,Sebastian 下面的回答对我有用。这是我找到的最简单的解决方案。 【参考方案1】:

没有办法让它无限,但有办法让它看起来像无限。

    在您的适配器中覆盖 getCount() 以返回像 Integer.MAX_VALUE 这样的大内容:

    @Override
    public int getCount() 
        return Integer.MAX_VALUE;
    
    

    getItem()getView() 位置除以实际项目编号:

    @Override
    public Fragment getItem(int position) 
        int positionInList = position % fragmentList.size();
        return fragmentList.get(positionInList);
    
    

    最后,将当前项目设置在中间的某个位置(否则,它只会在向下的方向上无穷无尽)。

    // scroll to middle item
    recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2);
    

【讨论】:

@anhtuannd 使用 MAX_INT / 2 或其他任何内容作为起始位置。 很好的解决方案!我添加了代码以便更好地理解。 :) 好主意!一项改进是在 scrollTo() 调用之后显示相同的第一项。可能是:int number = Integer.MAX_VALUE / items.size() / 2; scrollToPosition(number * items.size()); 我不明白这是怎么工作的,首先我们在谈论 Recyclerview 适配器,getItem / getView 是从哪里来的。 @geniushkg 这实际上更简单。您不需要覆盖任何方法,只需在 onBindViewHolder() 中使用 it = position % itemList.size() 的另一个位置(例如 realPosition)。【参考方案2】:

我为这个问题找到的其他解决方案运行良好,但我认为在回收器视图的 getCount() 方法中返回 Integer.MAX_VALUE 可能存在一些内存问题。

要解决此问题,请重写 getItemCount() 方法,如下所示:

@Override
public int getItemCount() 
    return itemList == null ? 0 : itemList.size() * 2;

现在无论您使用位置从列表中获取项目,在下面使用

position % itemList.size()

现在将 scrollListener 添加到您的回收站视图中

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() 
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy)  
        super.onScrolled(recyclerView, dx, dy);
        int firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition();
        if (firstItemVisible != 0 && firstItemVisible % itemList.size() == 0) 
            recyclerView.getLayoutManager().scrollToPosition(0);
        
    
);

最后开始自动滚动,调用下面的方法

public void autoScroll() 
    final Handler handler = new Handler();
    final Runnable runnable = new Runnable() 
        @Override
        public void run() 
            recyclerView.scrollBy(2, 0);
            handler.postDelayed(this, 0);
        
    ;
    handler.postDelayed(runnable, 0);

【讨论】:

我的这个方法只重复了一次,例如我输入了虚拟数据1,2,3,4,它只显示1,2,3,4,1,2,3,4,而不是无限循环,我做错了什么? @BeeingJk 在 getItemCount 函数中,我们返回列表中的两次元素,这就是为什么它只重复两次。尝试返回列表中元素数量的 4-5 倍,然后它应该可以正常工作。 感谢@NikhilGupta,我已经弄清楚了,因为我的数据长度太短以至于firstItemVisible % itemList.size() == 0 永远不会发生,是的,返回 3 次使它工作,但我不能保证它会一直在工作,所以我在启用自动滚动之前将总数据长度与 RecyclerView 宽度进行比较,到目前为止找不到更简单的方法 我无法从头循环到结尾,但它正在从结尾循环到开头。有什么方法可以让我朝任何一个方向走。 我需要从哪里调用 autoScroll() 方法请帮忙。【参考方案3】:

我创建了一个LoopingLayoutManager 来解决这个问题。

它无需修改适配器即可工作,从而具有更大的灵活性和可重用性。

它功能齐全,支持:

垂直和水平方向 LTR 和 RTL 两个方向以及 LTR 和 RTL 的 ReverseLayout 用于查找项目和位置的公共函数 以编程方式滚动的公共函数 Snap Helper 支持 辅助功能(TalkBack 和语音访问)支持

它托管在 maven Central 上,这意味着您只需将其作为依赖项添加到您的 build.gradle 文件中:

dependencies 
    implementation 'com.github.beksomega:loopinglayout:0.3.1'

并将您的 LinearLayoutManager 更改为 LoopingLayoutManager

它有一套 132 个单元测试,这让我确信它是稳定的,但如果你发现任何错误,请在 github 上提出问题!

我希望这会有所帮助!

【讨论】:

这很好,但我注意到了一个问题,即项目在滚动时会跳转位置。 感谢您试用我的项目@DaveThomas!我也非常感谢您报告有关它的错误。我从来没有见过这个问题,所以我不能立即给你建议。但是,如果您填写 issue,我也许可以找到它。 对我来说,推荐的图书馆就像一个魅力。我很感激它也支持LinearSnapHelper 现在我看到LoopingLayoutManager 的问题在于它多次布置/渲染相同的View 实例。乍一看,它看起来很棒,但如果需要更新项目,这种方法不适用于场景。 onClick。在这种情况下,行为是未指定的(有点混乱),因为未单击的项目也会更新其 UI。发生这种情况是因为这些项目共享相同的 View 实例。从编码的角度来看,上述Integer.MAX_VALUE % 2 方法可能不是最干净的方法,但适用于这种情况。【参考方案4】:

修改了@afanit的解决方案,防止在反向滚动时无限滚动暂时停止(由于等待第0项完全可见,这使得可滚动内容在scrollToPosition()被调用之前用完):

val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstItemPosition != 1 && firstItemPosition % items.size == 1) 
    layoutManager.scrollToPosition(1)
 else if (firstItemPosition == 0) 
    layoutManager.scrollToPositionWithOffset(items.size, -recyclerView.computeHorizontalScrollOffset())

注意computeHorizontalScrollOffset() 的使用,因为我的布局管理器是水平的。

另外,我发现getItemCount() 的最小返回值是items.size + 3。永远不会到达位置大于此的项目。

【讨论】:

【参考方案5】:

除了上面的解决方案。 对于双方的无尽回收者视图,您应该添加类似的内容:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() 
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) 
            super.onScrolled(recyclerView, dx, dy)
            val firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition()
            if (firstItemVisible != 1 && firstItemVisible % songs.size == 1) 
                linearLayoutManager.scrollToPosition(1)
            
            val firstCompletelyItemVisible = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
            if (firstCompletelyItemVisible == 0) 
                linearLayoutManager.scrollToPositionWithOffset(songs.size, 0)
            
        
    )

并升级您的 getItemCount() 方法:

@Override
public int getItemCount() 
        return itemList == null ? 0 : itemList.size() * 2 + 1;

它的工作方式类似于无限向下滚动,但是是双向的。很高兴为您提供帮助!

【讨论】:

它没有得到最后一个项目之后的第一个项目,它只是滚动回到顶部【参考方案6】:

我在使用 Glide 和其他 API 时遇到了 OOM 问题,并使用受这篇文章启发的 Duplicate End Caps 为 ios 构建创建了这个实现。

可能看起来令人生畏,但实际上它只是复制 RecyclerView 类并更新 RecyclerView 适配器中的两个方法。它所做的只是一旦它到达了端盖,它就会快速地向适配器的 ViewHolders 的两端进行无动画转换,以允许连续的循环转换。

http://iosdevelopertips.com/user-interface/creating-circular-and-infinite-uiscrollviews.html

class CyclingRecyclerView(
    context: Context,
    attrs: AttributeSet?
) : RecyclerView(context, attrs) 
    // --------------------- Instance Variables ------------------------
    private val onScrollListener = object : RecyclerView.OnScrollListener() 

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) 
            // The total number of items in our RecyclerView
            val itemCount = adapter?.itemCount ?: 0

            // Only continue if there are more than 1 item, otherwise, instantly return
            if (itemCount <= 1) return

            // Once the scroll state is idle, check what position we are in and scroll instantly without animation
            if (newState == SCROLL_STATE_IDLE) 
                // Get the current position
                val pos = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()

                // If our current position is 0,
                if (pos == 0) 
                    Log.d("AutoScrollingRV", "Current position is 0, moving to $itemCount - 1 when item count is $itemCount")
                    scrollToPosition(itemCount - 2)
                 else if (pos == itemCount - 1) 
                    Log.d("AutoScrollingRV", "Current position is $itemCount - 1, moving to 1 when item count is $itemCount")
                    scrollToPosition(1)
                 else 
                    Log.d("AutoScrollingRV", "Curren position is $pos")
                
            
        
    

    init 
        addOnScrollListener(onScrollListener)
    

对于适配器,只需确保更新 2 个方法,就我而言,viewModels 只是我的数据结构,其中包含我发送到我的 ViewHolders 的数据

override fun getItemCount(): Int = if (viewModels.size > 1) viewModels.size + 2 else viewModels.size

在 ViewHolder 上,您只需检索调整后的索引数据

    override fun onBindViewHolder(holder: ImageViewHolder, position: Int) 
        val adjustedPos: Int =
            if (viewModels.size > 1) 
                when (position) 
                    0 -> viewModels.lastIndex
                    viewModels.size + 1 -> 0
                    else -> position - 1
                
             else 
                position
            
        holder.bind(viewModels[adjustedPos])
    

以前的实现让我很受伤哈哈,似乎只是添加大量的项目,这是一个很大的问题,当您遇到带有 Integer.MAX_VALUE 嵌套 RecyclerView 的多张卡片时。这种方法解决了 OOM 的所有问题,因为它只需要创建 2 和 ViewHolders。

【讨论】:

【参考方案7】:

两边都有无尽的recyclerView

在你的回收站视图中添加 onScrollListener

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() 

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) 
            super.onScrolled(recyclerView, dx, dy);

            int firstItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
            if (firstItemVisible != 1 && firstItemVisible % itemList.size() == 1) 
                ((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPosition(1);
            
            int firstCompletelyItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (firstCompletelyItemVisible == 0)
            

            if (firstItemVisible != RecyclerView.NO_POSITION
                    && firstItemVisible== recyclerView.getAdapter().getItemCount()%itemList.size() - 1)
            
                ((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(itemList.size() + 1, 0);
            
        
    );

在您的适配器中覆盖 getItemCount 方法

@Override
public int getItemCount()

    return itemList == null ? 0 : itemList.size() * 2 + 1;

【讨论】:

以上是关于如何创建一个循环(无尽的)RecyclerView?的主要内容,如果未能解决你的问题,请参考以下文章

无尽的recyclerview,多种视图类型,OnscrollListener 后崩溃

java 无尽的RecyclerView OnScrollListener

如果循环一个数据框并在该循环内创建新列,它会是无穷无尽的吗?

像素风Roguelike策略卡牌《循环英雄》:无尽循环中的成长与取舍

如何使用光标和循环显示来自 sqlite 的片段的 recyclerview

无尽的包含循环[重复]