将RecyclerView项目插入零位 - 始终保持滚动到顶部

时间:2018-05-11 13:40:38

标签: android android-layout android-recyclerview

我有一个标准RecyclerView非常标准LinearLayoutManager。我一直在顶部插入新项目,我正在呼叫notifyItemInserted(0)

我希望列表保持滚动到顶部;始终显示第0个位置。

从我的要求来看,LayoutManager根据项目数量的不同表现。

虽然所有项目都适合屏幕,但它的外观和行为与我预期的一样:新项目始终显示在顶部并将所有内容移到其下方。

Behavior with few items: Addition shifts other items down, first (newest) item is always visible

但是,一旦没有。项目超出RecyclerView的范围,新项目添加到当前可见的项目,但可见项目仍在视图中。用户必须滚动才能看到最新的项目。

Behavior with many items: New items are added above the top boundary, user has to scroll to reveal them

对于许多应用程序来说,这种行为是完全可以理解的,但不适用于“实时提要”,其中看到最近的事情比使用自动滚动“不分散”用户更重要。

我知道这个问题几乎与Adding new item to the top of the RecyclerView重复了......但是所有提出的答案都只是变通方法(大多数都非常好,不可否认)。

我正在寻找一种方法来实际改变这种行为。我希望LayoutManager 完全相同,无论项目数量。我希望它总是移动所有项目(就像它为前几个添加项目所做的那样),而不是在某个时刻停止移动项目,并通过平滑滚动列表来补偿。

基本上,没有smoothScrollToPosition,没有RecyclerView.SmoothScroller。子类化LinearLayoutManager很好。我已经挖掘了它的代码,但到目前为止没有任何运气,所以我决定询问以防有人已经处理过这个问题。谢谢你的任何想法!

编辑:澄清为什么我会从链接问题中解答答案:主要是我关注动画的平滑性。

请注意,在第一个GIF中,ItemAnimator在添加新项目时移动其他项目,淡入和移动动画的持续时间相同。但是当我通过平滑滚动“移动”项目时,我无法轻松控制滚动的速度。即使使用默认ItemAnimator个持续时间,这看起来也不太好,但在我的特定情况下,我甚至需要减慢ItemAnimator持续时间,这会使情况变得更糟:

Insert "fixed" with smooth scroll + ItemAnimator durations increased

5 个答案:

答案 0 :(得分:4)

当一个项目被添加到RecyclerView的顶部并且该项目可以放到屏幕上时,该项目将附加到视图持有者,并且RecyclerView经历动画阶段以将项目移动到在顶部显示新项目。

如果在不滚动的情况下无法显示新项目,则不会创建视图保持器,因此无法设置动画。当发生这种情况时,将新项目放到屏幕上的唯一方法是滚动,这会导致创建视图持有者,以便可以在屏幕上显示视图。 (似乎有一个边缘情况,部分显示视图并创建视图持有者,但我将忽略此特定实例,因为它不是密切关系。)

因此,问题在于必须使两个不同的动作,即添加视图的动画和添加视图的滚动,以使用户看起来相同。我们可以深入了解底层代码,并确定在视图持有者创建,动画计时等方面究竟发生了什么。但是,即使我们可以复制操作,如果底层代码发生更改,它也可能会中断。这就是你要抵制的。

另一种方法是在RecyclerView的零位添加标题。当显示此标题并将新项目添加到位置1时,您将始终看到动画。如果您不想要标题,则可以将其设置为零高度并且不会显示。以下视频显示了这种技术:

[video]

这是演示的代码。它只是在项目的第0位添加一个虚拟条目。如果虚拟条目不符合您的喜好,还有其他方法可以解决这个问题。您可以搜索向RecyclerView添加标题的方法。

(如果你确实使用了滚动条,那么它会出现错误,你可以从演示中看出来。要修复这个100%,你必须接管很多滚动条高度和位置计算。 computeVerticalScrollOffset() LinearLayoutManager负责在适当的时候将滚动条放在顶部。(视频拍摄后引入了代码。)但是滚动条会在向下滚动时跳转。更好的放置计算会照顾这个问题。有关不同高度项的上下文中有关滚动条的更多信息,请参阅this Stack Overflow question。)

<强> MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private TheAdapter mAdapter;
    private final ArrayList<String> mItems = new ArrayList<>();
    private int mItemCount = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    LinearLayoutManager layoutManager =
        new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
            @Override
            public int computeVerticalScrollOffset(RecyclerView.State state) {
                if (findFirstCompletelyVisibleItemPosition() == 0) {
                    // Force scrollbar to top of range. When scrolling down, the scrollbar
                    // will jump since RecyclerView seems to assume the same height for
                    // all items.
                    return 0;
                } else {
                    return super.computeVerticalScrollOffset(state);
                }
            }
        };
        recyclerView.setLayoutManager(layoutManager);

        for (mItemCount = 0; mItemCount < 6; mItemCount++) {
            mItems.add(0, "Item # " + mItemCount);
        }

        // Create a dummy entry that is just a placeholder.
        mItems.add(0, "Dummy item that won't display");
        mAdapter = new TheAdapter(mItems);
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    public void onClick(View view) {
        // Always at to position #1 to let animation occur.
        mItems.add(1, "Item # " + mItemCount++);
        mAdapter.notifyItemInserted(1);
    }
}

<强> TheAdapter.java

class TheAdapter extends RecyclerView.Adapter<TheAdapter.ItemHolder> {
    private ArrayList<String> mData;

    public TheAdapter(ArrayList<String> data) {
        mData = data;
    }

    @Override
    public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;

        if (viewType == 0) {
            // Create a zero-height view that will sit at the top of the RecyclerView to force
            // animations when items are added below it.
            view = new Space(parent.getContext());
            view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
        } else {
            view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.list_item, parent, false);
        }
        return new ItemHolder(view);
    }

    @Override
    public void onBindViewHolder(final ItemHolder holder, int position) {
        if (position == 0) {
            return;
        }
        holder.mTextView.setText(mData.get(position));
    }

    @Override
    public int getItemViewType(int position) {
        return (position == 0) ? 0 : 1;
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    public static class ItemHolder extends RecyclerView.ViewHolder {
        private TextView mTextView;

        public ItemHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.textView);
        }
    }
}

<强> activity_main.xml中

<android.support.constraint.ConstraintLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:text="Button"
        android:onClick="onClick"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

<强> list_item.xml

<LinearLayout 
    android:id="@+id/list_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:orientation="horizontal">

    <View
        android:id="@+id/box"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="16dp"
        android:background="@android:color/holo_green_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/box"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="TextView" />

</LinearLayout>

答案 1 :(得分:1)

我还在显示项目的实时供稿,添加新项目时,它在第一个项目之前。但是在回收者视图中,我需要向上滚动才能看到新项目。

要解决该问题,请向适配器添加一个RecyclerView.AdapterDataObserver()并覆盖onItemRangeInserted()。添加新数据后,请检查数据是否位于位置0上,并且回收站视图位于顶部(如果要在列表中滚动,则不想自动滚动到第一个位置)。

示例:

adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        super.onItemRangeInserted(positionStart, itemCount);
        if (positionStart == 0 && positionStart == layoutManager.findFirstCompletelyVisibleItemPosition()) {
            layoutManager.scrollToPosition(0);
        }
    }
});

对于其他类型的适配器,例如ListAdapterPagedListAdapter,此解决方案对我来说效果很好。 首先,我想使用the accepted solution的类似实现,在其中添加一个笨拙的第一项,但是在PagedListAdapter中,由于列表是不可变的,所以这是不可能的。

答案 2 :(得分:0)

使用adapter.notifyDataSetChanged()代替adater.notifyItemInserted(0)。如果当前滚动位置是一个(recylerView),它将old zero滚动到零位置。

答案 3 :(得分:0)

这对我有用:

val atTop = !recycler.canScrollVertically(-1)

adapter.addToFront(item)
adapter.notifyItemInserted(0)

if (atTop) {
    recycler.scrollToPosition(0)
}

答案 4 :(得分:0)

对我而言唯一有效的解决方案是通过在回收站setReverseLayout()上调用setStackFromEnd()LinearLayoutManager来反转回收站的布局。

这听起来可能很愚蠢,但是RecyclerView处理列表末尾添加项的方式是您最需要的。唯一的缩水是您必须反转列表并开始在末尾添加项目。