如何在具有页眉和页脚的RecyclerView中使用DiffUtil.Callback?

7

背景

我在开发一个应用程序,其中有一个可以上下滚动的RecyclerView。

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

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

我在这里询问过关于此的问题,因为我从服务器获取的只是一个全新的项目列表,而不是与先前列表的差异。

问题

RecyclerView不仅有要显示的数据,还有一些特殊的项目:

由于互联网连接可能很慢,因此在这个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、页脚

因此,如果您停留在标题处,服务器向您发送新列表,则仍会看到标题,下面是新项目,而不会看到旧项目。它会为您滚动而不是停留在同一位置。

这里有一个草图展示了这个问题。黑色矩形表示列表的可见部分。

enter image description here

正如您所见,在加载之前,可见部分有标题和一些项目,在加载之后,它仍然有标题和一些项目,但那些是推开旧项目的新项目。
在这种情况下,我需要标题消失,因为真正的内容在它下面。除了标题区域外,它可能会显示其他项目(或其中的一部分),但当前项目的可见位置应保持不变。
此问题仅在列表顶部显示标题时才会出现。在所有其他情况下,它都可以正常工作,因为只有普通项目显示在可见区域的顶部。
我尝试了一些方法,例如找到如何设置DiffUtil.Callback以忽略某些项目,但我认为这样的方法不存在。
我考虑了一些解决方法,但每种方法都有其缺点:
  • 一个NestedScrollView(或RecyclerView),它将容纳头部和底部以及中间的RecyclerView,但这可能会导致一些滚动问题,特别是由于我已经有一个依赖于RecyclerView的复杂布局(视图折叠等)。

  • 也许在普通项目的布局中,我也可以放置页眉和页脚的布局(或者只是页眉,因为这个是有问题的)。但这对性能来说是不好的,因为它会为无用的视图充气额外的视图。此外,它要求我切换新视图的隐藏和查看。

  • 每当有来自服务器的更新时,我都可以为页眉设置一个新ID,使其好像之前的页眉已经消失了,并且在新列表的顶部有一个全新的页眉。然而,在没有真正更新列表顶部的情况下,这可能是有风险的,因为页眉将被显示为已删除,然后重新添加。

问题

有没有办法解决这个问题,而不需要这样的变通方法?

有没有一种方法可以告诉 DiffUtil.Callback:“这些项(头和尾)不是真正的滚动项,而这些项(真实数据项)应该是”?
2个回答

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;
}

根据视图类型,您可以在适配器中添加进度条视图持有者。
第三步:在数据加载逻辑中使用这些方法:
recyclerViewonScrolled()方法中调用API时,在API调用之前需要添加一个加载器项目,然后在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上提供一个能够证明它运行良好的示例,我会接受这个答案。对此感到抱歉。我没有时间... - android developer
当然,没问题。我会制作一个样本并与您分享。 - Ashutosh Tiwari

0

我认为目前采取的解决方案已经足够了。虽然有点奇怪,但我认为它应该能够工作:

每当列表中的第一个真实项不同时,标题项都会获得一个新的ID。页脚始终具有相同的ID,因为它可以按照当前的方式移动。我甚至不需要检查它的ID是否相同。对于它们,areItemsTheSame的检查如下:

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

这样,如果标题属于新的列表数据,则旧数据将被删除,新数据将位于顶部。

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

此外,由于某种原因,我无法在仅更新页眉和页脚的情况下使用notifyItemChanged(互联网连接更改其状态,因此需要单独更改页眉和页脚)。出于某种原因,只有notifyDataSetChanged有效。

不过,如果有更官方的方法,那就很好了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接