提问者:小点点

如何直观地保持在相同的滚动位置,在未知数量的RecyclerView数据更改(插入,删除,更新)?


如果你的回收视图得到了新的项目,最好使用通知fyItemRangeInserted,以及每个项目唯一、稳定的id,这样它就可以很好地动画,而不会改变你看到的太多:

如您所见,列表中的第一个项目“0”,当我在它之前添加更多项目时,它保持在同一个位置,好像什么都没有改变。

这是一个很好的解决方案,当您在其他任何地方插入项目时,它也适用于其他情况。

然而,它并不适合所有情况。有时,我从外面得到的只是:“这是一个新的项目列表,有些是新的,有些是相同的,有些已经更新/删除”。

正因为如此,我不能再使用通知ItemRangeInserted了,因为我不知道添加了多少。

问题是,如果我使用通知数据设置更改,滚动会发生变化,因为当前项目之前的项目数量发生了变化。

这意味着您当前查看的项目将在视觉上移至一边:

正如你现在看到的,当我在第一个项目之前添加更多项目时,他们会将其向下推。

我希望当前可查看的项目将尽可能多地保留,优先级为顶部的项目(在这种情况下为0)。

对用户来说,除了一些可能的最终情况(删除了当前项目和之后的项目,或者以某种方式更新了当前项目),他不会注意到当前项目以上的任何内容。看起来好像我使用了通知fyItemRangeInserted

我试图保存当前滚动状态或位置,然后恢复它,如图所示,但没有一个解决方案解决了这个问题。

这是我POC尝试的项目:

class MainActivity : AppCompatActivity() {
    val listItems = ArrayList<ListItemData>()
    var idGenerator = 0L
    var dataGenerator = 0

    class ListItemData(val data: Int, val id: Long)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            val inflater = LayoutInflater.from(this@MainActivity)
            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false)) {}
            }

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
                val textView = holder!!.itemView as TextView
                val item = listItems[position]
                textView.text = "item: ${item.data}"
            }

            override fun getItemId(position: Int): Long = listItems[position].id

            override fun getItemCount(): Int = listItems.size
        }
        adapter.setHasStableIds(true)
        recyclerView.adapter = adapter
        for (i in 1..30)
            listItems.add(ListItemData(dataGenerator++, idGenerator++))
        addItemsFromTopButton.setOnClickListener {
            for (i in 1..5) {
                listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
            }
            //this is a good insertion, when we know how many items were added
            adapter.notifyItemRangeInserted(0, 5)
            //this is a bad insertion, when we don't know how many items were added
//            adapter.notifyDataSetChanged()
        }


    }
}


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.user.recyclerviewadditionwithoutscrollingtest.MainActivity">


    <Button
        android:id="@+id/addItemsFromTopButton" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
        android:text="add items to top" app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/recyclerView"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp"
        android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
        android:layout_marginTop="8dp" android:orientation="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

是否可以通知适配器各种更改,但让它停留在完全相同的地方?

如果可以,当前查看的项目将保留,或根据需要删除/更新。

当然,物品的id将保持唯一和稳定,但可悲的是,细胞大小可能彼此不同。

编辑:我找到了一个部分解决方案。它的工作原理是获取哪个视图位于顶部,获取其项目(将其保存在viewHolder中)并尝试滚动到它。不过,这存在多个问题:

  1. 如果项目被删除,我将不得不以某种方式滚动到下一个,依此类推。我认为在真正的应用程序中,我可以做到这一点。不知道是否有更好的方法。
  2. 目前它通过列表来获取项目,但也许在真正的应用程序中我可以优化它。
  3. 因为它只是滚动到项目,如果把它放在RecyclerView的顶部边缘,所以如果你滚动了一点以部分显示它,它会移动一点:

这是新代码:

class MainActivity : AppCompatActivity() {
    val listItems = ArrayList<ListItemData>()
    var idGenerator = 0L
    var dataGenerator = 0

    class ListItemData(val data: Int, val id: Long)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val adapter = object : RecyclerView.Adapter<ViewHolder>() {
            val inflater = LayoutInflater.from(this@MainActivity)
            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
                return ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false))
            }

            override fun onBindViewHolder(holder: ViewHolder, position: Int) {
                val textView = holder.itemView as TextView
                val item = listItems[position]
                textView.text = "item: ${item.data}"
                holder.listItem = item
            }

            override fun getItemId(position: Int): Long = listItems[position].id

            override fun getItemCount(): Int = listItems.size
        }
        adapter.setHasStableIds(true)
        recyclerView.adapter = adapter
        for (i in 1..30)
            listItems.add(ListItemData(dataGenerator++, idGenerator++))
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        addItemsFromTopButton.setOnClickListener {
            for (i in 1..5) {
                listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
            }
            val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
            val holder = recyclerView.findViewHolderForAdapterPosition(firstVisibleItemPosition) as ViewHolder
            adapter.notifyDataSetChanged()
            val listItemToGoTo = holder.listItem
            for (i in 0..listItems.size) {
                val cur = listItems[i]
                if (listItemToGoTo === cur) {
                    layoutManager.scrollToPositionWithOffset(i, 0)
                    break
                }
            }
            //TODO think what to do if the item wasn't found
        }


    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var listItem: ListItemData? = null
    }
}

共1个答案

匿名用户

我将使用DiffUtilapi来解决这个问题。DiffUtil旨在采用“之前”和“之后”列表(可以像您想要的那样相似或不同),并将为您计算需要通知适配器的各种插入、删除等。

使用DiffUtil最大的也是几乎唯一的挑战是定义要使用的DiffUtil. Callback。对于您的概念验证应用程序,我认为事情会很容易。请原谅Java的代码;我知道您最初是用静态编程语言发布的,但我对静态编程语言并不像对Java那样熟悉。

这是一个我认为适用于您的应用程序的回调:

private static class MyCallback extends DiffUtil.Callback {

    private List<ListItemData> oldItems;
    private List<ListItemData> newItems;

    @Override
    public int getOldListSize() {
        return oldItems.size();
    }

    @Override
    public int getNewListSize() {
        return newItems.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldItems.get(oldItemPosition).id == newItems.get(newItemPosition).id;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldItems.get(oldItemPosition).data == newItems.get(newItemPosition).data;
    }
}

以下是你如何在应用程序中使用它(在java/kotlin伪代码中):

addItemsFromTopButton.setOnClickListener {
    MyCallback callback = new MyCallback();
    callback.oldItems = new ArrayList<>(listItems);

    // modify listItems however you want... add, delete, shuffle, etc

    callback.newItems = new ArrayList<>(listItems);
    DiffUtil.calculateDiff(callback).dispatchUpdatesTo(adapter);
}

我制作了自己的小应用程序来测试这一点:每次按下按钮都会添加20个项目,打乱列表,然后删除10个项目。这是我观察到的:

  • 当“之前”列表中的第一个可见项也存在于“之后”列表中时…
    • 当它后面有足够的项目填满屏幕时,它就留在原地。
    • 当没有的时候,回收站视图滚动到底部