如何将 DiffUtil.Callback 与具有页眉和页脚的 RecyclerView 一起使用?

Posted

技术标签:

【中文标题】如何将 DiffUtil.Callback 与具有页眉和页脚的 RecyclerView 一起使用?【英文标题】:How to use DiffUtil.Callback with a RecyclerView that has header and footer? 【发布时间】:2018-09-19 17:43:09 【问题描述】:

背景

我正在开发一个具有 RecyclerView 的应用程序,您可以随意上下滚动。

数据项是从服务器加载的,因此如果您即将到达底部或顶部,应用程序会获取新数据以显示在那里。

为了避免奇怪的滚动行为并停留在当前项目上,我使用 'DiffUtil.Callback' 覆盖 'getOldListSize'、'getNewListSize'、'areItemsTheSame'、'areContentsTheSame'。

我问过这个here,因为我从服务器得到的只是一个全新的项目列表,而不是与之前列表的区别。

问题

RecyclerView 不只显示数据。里面也有一些特殊物品:

由于 Internet 连接可能很慢,因此在此 RecyclerView 中有一个页眉项和一个页脚项,它们只有一个特殊的进度视图,以显示您已到达边缘并且它将很快加载。

页眉和页脚始终存在于列表中,并且不从服务器接收。它纯粹是 UI 的一部分,只是为了显示即将加载的内容。

问题是,就像其他项目一样,它需要由 DiffUtil.Callback 处理,因此对于 areItemsTheSameareContentsTheSame,如果旧页眉是新页眉,我只返回 true,而旧页脚是新的页脚:

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean 
        val oldItem = oldItems[oldItemPosition]
        val newItem = newItems[newItemPosition]
        when 
            oldItem.itemType != newItem.itemType -> return false
            oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == AgendaItem.TYPE_HEADER -> return true
            ...
        
    

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean 
        val oldItem = oldItems[oldItemPosition]
        val newItem = newItems[newItemPosition]
        return when 
            oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == ItemType.TYPE_HEADER -> true
            ...
        
    

看起来对吗?好吧,这是错误的。如果用户位于列表顶部,显示标题,并且列表更新了新项目,则标题将保持在顶部,这意味着您之前看到的项目将被新项目推开。

例子:

之前:页眉、0、1、2、3、页脚 之后:页眉、-3、-2、-1、0、1、2、3、页脚

因此,如果您停留在标题上,并且服务器向您发送了新列表,您仍然可以看到标题和新项目下方,而看不到旧项目。它会为您滚动,而不是停留在同一位置。

这是显示问题的草图。黑色矩形显示列表的可见部分。

如你所见,加载前可见部分有header和一些item,加载后仍然有header和一些item,但都是推开旧的新item。

我需要在这种情况下去掉标题,因为真正的内容在它下面。它可能会在其上方显示其他项目(或其中的一部分),而不是标题区域,但当前项目的可见位置应保持在它们所在的位置。

仅当标题显示在列表顶部时,才会出现此问题。在所有其他情况下,它都可以正常工作,因为只有普通项目显示在可见区域的顶部。

我尝试过的

我试图找到如何设置 DiffUtil.Callback 以忽略某些项目,但我认为这样的事情不存在。

我在想一些解决方法,但每个都有自己的缺点:

一个 NestedScrollView(或 RecyclerView),它将把页眉和页脚和 RecyclerView 放在中间,但这可能会导致一些滚动问题,特别是因为我已经有一个依赖于 RecyclerView 的复杂布局(折叠意见等...)。

也许在正常项目的布局中,我也可以放置页眉和页脚的布局(或者只是页眉,因为这个是有问题的)。但这对性能来说是一件坏事,因为它会毫无意义地增加额外的视图。另外,它需要我切换隐藏和查看其中的新视图。

我可以在每次有来自服务器的更新时为标头设置一个新 ID,使之前的标头消​​失了,并且在新列表的顶部有一个全新的标头。但是,如果顶部列表没有真正更新,这可能会有风险,因为标题将显示为好像它被删除然后重新添加一样。

问题

有没有办法在没有这种变通方法的情况下解决这个问题?

有没有办法告诉DiffUtil.Callback:“这些项目(页眉和页脚)不是要滚动到的真实项目,而这些项目(真实数据项目)应该是”?

【问题讨论】:

【参考方案1】:

我将尝试解释我认为可以解决您的问题的方法:

第 1 步:删除 FOOTER 和 HEADER 视图的所有代码。

第 2 步: 添加这些根据用户滚动方向在适配器中添加和删除虚拟模型项的方法:

/**
 * Adds loader item in the adapter based on the given boolean.
 */
public void addLoader(boolean isHeader) 
    if (!isLoading()) 
        ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
        if(isHeader) 
           questions.add(0, getProgressModel());
        else 
           questions.add(getProgressModel());
        setData(dataList);
    


/**
 * Removes loader item from the UI.
 */
public void removeLoader() 
    if (isLoading() && !dataList.isEmpty()) 
        ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
        dataList.remove(getDummyModel());
        setData(questions);
    


public MessageDetail getChatItem() 
    return new Model(0, 0, 0, "", "", "")); // Here the first value is id which is set as zero.

以下是您需要确定项目是加载器项目还是实际数据项目的其余适配器逻辑:

@Override
public int getItemViewType(int position) 
    return dataList.get(position).getId() == 0 ? StaticConstants.ItemViewTypes.PROGRESS : StaticConstants.ItemViewTypes.CONTENT;

根据视图类型,您可以在适配器中添加进度条视图持有者。

第 3 步:在数据加载逻辑中使用这些方法:

recyclerViewonScrolled() 方法中进行API 调用时,您需要在api 调用之前添加一个loader 项,然后在api 调用之后将其删除。使用上面给定的适配器方法。 onScrolled 中的编码应该是这样的:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() 
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) 
            super.onScrolled(recyclerView, dx, dy);
            if (dy < 0)  //This is top scroll, so add a loader as the header.
                recyclerViewAdapter.addLoader(true);
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                if (!recyclerViewAdapter.isLoading(true)) 
                    if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() <= 2) 
                        callFetchDataApi();
                    
                
            
         else 
            if (!recyclerViewAdapter.isLoading(false)) 
                if (linearLayoutManager.findLastCompletelyVisibleItemPosition() >= linearLayoutManager.getItemCount() - 2) 
                    callFetchDataApi();
                
            
    );

现在,在 api 调用为您提供所需的数据之后。只需从列表中删除添加的加载程序,如下所示:

private void onGeneralApiSuccess(ResponseModel responseModel) 
    myStreamsDashboardAdapter.removeLoader();
    if (responseModel.getStatus().equals(SUCCESS)) 
            // Manage your pagination and other data loading logic here.
            dataList.addAll(responseModel.getDataList());
            recyclerViewAdapter.setData(dataList);
        

最后,您需要在数据加载操作期间避免任何滚动,为此添加一个逻辑方法,即isLoading() 方法。在方法onScrolled()的代码中使用:

public boolean isLoading(boolean isFromHeader) 
    if (isFromHeader) 
        return dataList.isEmpty() || dataList.get(0).getId() == 0;
     else 
        return dataList.isEmpty() || dataList.get(dataList.size() -1).getId() == 0;
    

如果你不明白这些,请告诉我。

【讨论】:

遗憾的是,我已经离开了这个。我可以给你+1的努力。如果你在 Github 上做一个样本证明它运行良好,我会接受答案。对此感到抱歉。我没时间…… 当然。不是问题。将制作样品并与您分享。【参考方案2】:

我认为目前我采取的解决方案就足够了。这有点奇怪,但我认为它应该可以工作:

每次列表的第一个真实项目不同时,标题项目都会获得一个新的 id。页脚始终具有相同的 id,因为它可以以当前的工作方式移动。我什至不需要检查它的 id 是否相同。 areItemsTheSame 的检查对他们来说是这样的:

            oldItem.agendaItemType == AgendaItem.TYPE_HEADER -> return oldItem.id == newItem.id
            oldItem.agendaItemType == AgendaItem.TYPE_FOOTER -> return true

这样,如果表头属于新的列表数据,旧的会被移除,新的会在顶部。

这不是完美的解决方案,因为它并没有真正将原始标题推到顶部,理论上它使我们“有点”同时有 2 个标题(一个被删除,一个被添加)但是我认为已经足够了。

另外,由于某种原因,我不能在页眉和页脚上使用notifyItemChanged,以防它们被更新(互联网连接改变了它的状态,所以需要单独更改页眉和页脚)。出于某种原因,只有 notifyDataSetChanged 有效。

不过,如果有更官方的方式,可能会很高兴知道。

【讨论】:

以上是关于如何将 DiffUtil.Callback 与具有页眉和页脚的 RecyclerView 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

recyclerview 中的 Diffutil,如果添加了新项目,则使其自动滚动

Android 高性能列表:RecyclerView + DiffUtil

Android 高性能列表:RecyclerView + DiffUtil

如何将模型与 GraphQL 界面上的“具有一个”关系相关联

如何将地图与具有正则表达式键的字典一起使用?

如何将底页与具有文本字段的键盘一起移动(自动对焦为真)?