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

标签 android android-recyclerview

背景

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

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

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

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

问题

RecyclerView 不仅要显示数据。里面也有一些特别的东西:

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

页眉页脚始终存在于列表中,不会从服务器接收。它纯粹是 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 的复杂布局( View 折叠等...... )。
  • 也许在普通项目的布局中,我也可以放置页眉和页脚的布局(或者只是页眉,因为这是有问题的)。但这对性能来说是一件坏事,因为它会毫无意义地增加额外的 View 。另外,它需要我切换隐藏和查看其中的新 View 。
  • 每次有来自服务器的更新时,我都可以为 header 设置一个新的 ID,就像以前的 header 消​​失了一样,并且在新列表的顶部有一个全新的 header 。但是,在顶部列表没有真正更新的情况下,这可能有风险,因为标题将显示为好像它已被删除然后重新添加。

  • 问题

    没有这种变通办法,有没有办法解决这个问题?

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

    最佳答案

    我将尝试解释我所看到的问题的解决方案:

    第一步:删除 FOOTER 和 HEADER View 的所有代码。

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

    根据 View 类型,您可以在适配器中添加进度条 View 支架。

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

    onScrolled() 中进行 API 调用时recyclerView的方法,需要在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;
        }
    }
    

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

    关于android - 如何将 DiffUtil.Callback 与具有页眉和页脚的 RecyclerView 一起使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49747957/

    相关文章:

    android - 在屏幕方向更改时恢复对象

    安卓内核版本名称

    android - 在 BLE Android 中写入后无法获得读取特性

    android - 异步图像下载到 StaggeredGridLayout

    android - recyclerview 内的多行编辑文本未正确滚动

    android - 如何使用express编写recyclerview拖放的测试用例

    java - 如何在 Android Studio 中创建自定义图像按钮?

    android - 如何将 Floating Action Button 带出应用程序并使其作为可见、可移动和可点击的后台服务运行?

    android - 应为 BEGIN_ARRAY 但在第 1 行第 26 列路径 $.result 中为 STRING

    java - 通过 AdapterPosition 获取 RecyclerView 的 View