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

时间:2018-04-10 07:37:33

标签: android android-recyclerview

背景

我使用的是具有RecyclerView的应用程序,您可以根据需要向上和向下滚动。

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

为了避免奇怪的滚动行为,并保持当前项目,我使用'DiffUtil.Callback',重写'getOldListSize','getNewListSize','areItemsTheSame','areContentsTheSame'。

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

问题

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
            ...
        }
    }
}
好像对吗?嗯,这是错的。如果用户位于列表的顶部,显示标题,并且列表会使用新项目进行更新,则标题将保留在顶部,这意味着您看到的先前项目将被新项目推开。< / p>

示例:

  • 之前:标题,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 个答案:

答案 0 :(得分: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步:在数据加载逻辑中使用以下方法:

在使用onScrolled()的{​​{1}}方法进行API调用时,您需要在api调用之前添加加载程序项,然后在api调用之后将其删除。使用上面给定的适配器方法。 recyclerView中的代码应如下所示:

onScrolled

现在,在api调用之后,将为您提供所需的数据。只需从列表中删除添加的加载器,就像这样:

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

最后,您需要避免在数据加载操作期间发生任何滚动,为此添加一种逻辑方法,即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()

如果您不了解其中任何一个,请告诉我。

答案 1 :(得分:0)

我想现在,我采取的解决方案就足够了。这有点奇怪,但我认为它应该有效:

每当列表在其第一个真实项目中不同时,标题项就会获得一个新ID。页脚总是具有相同的ID,因为它可以以当前工作方式移动。我甚至不需要检查它的id是否相同。对areItemsTheSame的检查就是这样:

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

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

这不是完美的解决方案,因为它并没有真正推动原始标题位于顶部,理论上它使我们“有点”同时拥有2个标题(一个被删除而另一个被添加)但是我认为这很好。

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

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