[英]Inserting RecyclerView items at zero position - always stay scrolled to top
I have a pretty standard RecyclerView
with a vertical LinearLayoutManager
.我有一个非常标准的RecyclerView
和一个垂直的LinearLayoutManager
。 I keep inserting new items at the top and I'm calling notifyItemInserted(0)
.我一直在顶部插入新项目,我正在调用notifyItemInserted(0)
。
I want the list to stay scrolled to the top ;我希望列表保持滚动到顶部; to always display the 0th position.始终显示第 0 个位置。
From my requirement's point of view, the LayoutManager
behaves differently based on the number of items.从我的需求的角度来看, LayoutManager
行为会根据项目的数量而有所不同。
While all items fit on the screen, it looks and behaves as I expect: The new item always appears on top and shifts everything below it.虽然所有项目都适合屏幕,但它的外观和行为都符合我的预期:新项目总是出现在顶部并移动它下面的所有内容。
However, as soon as the no.然而,一旦没有。 of items exceeds the RecyclerView's bounds, new items are added above the currently visible one, but the visible items stay in view .的项目超出了 RecyclerView 的边界,新项目添加到当前可见项目之上,但可见项目保持在视图中。 The user has to scroll to see the newest item.用户必须滚动才能看到最新的项目。
This behavior is totally understandable and fine for many applications, but not for a "live feed", where seeing the most recent thing is more important than "not distracting" the user with auto-scrolls.对于许多应用程序来说,这种行为是完全可以理解的并且很好,但对于“实时提要”则不然,在这种情况下,查看最新内容比“不分散”自动滚动用户的注意力更重要。
I know this question is almost a duplicate of Adding new item to the top of the RecyclerView ... but all of the proposed answers are mere workarounds (most of them quite good, admittedly).我知道这个问题几乎是将新项目添加到 RecyclerView 顶部的重复......但所有建议的答案都只是解决方法(不可否认,其中大多数都很好)。
I'm looking for a way to actually change this behavior .我正在寻找一种方法来真正改变这种行为。 I want the LayoutManager to act exactly the same, no matter the number of items .我希望 LayoutManager 的行为完全相同,无论项目数量如何。 I want it to always shift all of the items (just like it does for the first few additions), not to stop shifting items at some point, and compensate by smooth-scrolling the list to the top.我希望它始终移动所有项目(就像它对前几个添加所做的那样),而不是在某个时候停止移动项目,并通过将列表平滑滚动到顶部来进行补偿。
Basically, no smoothScrollToPosition
, no RecyclerView.SmoothScroller
.基本上,没有smoothScrollToPosition
,没有RecyclerView.SmoothScroller
。 Subclassing LinearLayoutManager
is fine.继承LinearLayoutManager
很好。 I'm already digging through its code, but without any luck so far, so I decided to ask in case someone already dealt with this.我已经在挖掘它的代码,但到目前为止没有任何运气,所以我决定问问是否有人已经处理过这个问题。 Thanks for any ideas!感谢您的任何想法!
EDIT: To clarify why I'm dismissing answers from the linked question: Mostly I'm concerned about animation smoothness.编辑:澄清为什么我不理会链接问题的答案:主要是我担心动画平滑度。
Notice in the first GIF where ItemAnimator
is moving other items while adding the new one, both fade-in and move animations have the same duration.请注意,在第一个 GIF 中, ItemAnimator
在添加新项目的同时移动其他项目,淡入和移动动画具有相同的持续时间。 But when I'm "moving" the items by smooth scrolling, I cannot easily control the speed of the scroll .但是当我通过平滑滚动“移动”项目时,我无法轻松控制滚动速度。 Even with default ItemAnimator
durations, this doesn't look as good, but in my particular case, I even needed to slow down the ItemAnimator
durations, which makes it even worse:即使使用默认的ItemAnimator
持续时间,这看起来也不那么好,但在我的特殊情况下,我什至需要减慢ItemAnimator
持续时间,这使得情况更糟:
Although I wrote this answer and this is the accepted solution, I suggest a look at the other later answers to see if they work for you before attempting this.尽管我写了这个答案并且这是公认的解决方案,但我建议在尝试此之前查看其他稍后的答案,看看它们是否适合您。
When an item is added to the top of the RecyclerView
and the item can fit onto the screen, the item is attached to a view holder and RecyclerView
undergoes an animation phase to move items down to display the new item at the top.当一个项目被添加到RecyclerView
的顶部并且该项目可以适应屏幕时,该项目被附加到一个视图持有者并且RecyclerView
经历一个动画阶段来向下移动项目以在顶部显示新项目。
If the new item cannot be displayed without scrolling, a view holder is not created so there is nothing to animate.如果不滚动就无法显示新项目,则不会创建视图持有者,因此没有任何动画。 The only way to get the new item onto the screen when this happens is to scroll which causes the view holder to be created so the view can be laid out on the screen.发生这种情况时将新项目放到屏幕上的唯一方法是滚动,这会导致创建视图持有者,以便可以在屏幕上布置视图。 (There does seem to be an edge case where the view is partially displayed and a view holder is created, but I will ignore this particular instance since it is not germane.) (似乎确实存在局部显示视图并创建视图持有者的边缘情况,但我将忽略此特定实例,因为它不是密切相关的。)
So, the issue is that two different actions, animation of an added view and scrolling of an added view, must be made to look the same to the user.因此,问题是两个不同的动作,添加视图的动画和添加视图的滚动,必须让用户看起来相同。 We could dive into the underlying code and figure out exactly what is going on in terms of view holder creation, animation timing, etc. But, even if we can duplicate the actions, it can break if the underlying code changes.我们可以深入研究底层代码,并确切地弄清楚在视图持有者创建、动画计时等方面发生了什么。但是,即使我们可以复制这些动作,如果底层代码发生变化,它也可能会中断。 This is what you are resisting.这就是你在抗拒的。
An alternative is to add a header at position zero of the RecyclerView
.另一种方法是在RecyclerView
零位置添加标题。 You will always see the animation when this header is displayed and new items are added to position 1. If you don't want a header, you can make it zero height and it will not display.当显示此标题并将新项目添加到位置 1 时,您将始终看到动画。如果您不想要标题,您可以将其设置为零高度,它不会显示。 The following video shows this technique:以下视频展示了这种技术:
This is the code for the demo.这是演示的代码。 It simply adds a dummy entry at position 0 of the items.它只是在项目的位置 0 添加一个虚拟条目。 If a dummy entry is not to your liking, there are other ways to approach this.如果您不喜欢虚拟条目,还有其他方法可以解决此问题。 You can search for ways to add headers to RecyclerView
.您可以搜索将标题添加到RecyclerView
。
(If you do use a scrollbar, it will misbehave as you can probably tell from the demo. To fix this 100%, you will have to take over a lot of the scrollbar height and placement computation. The custom computeVerticalScrollOffset()
for the LinearLayoutManager
takes care of placing the scrollbar at the top when appropriate. (Code was introduced after video taken.) The scrollbar, however, jumps when scrolling down. A better placement computation would take care of this problem. See this Stack Overflow question for more information on scrollbars in the context of varying height items.) (如果你确实使用了滚动条,它会像你从演示中看到的那样行为不端。要修复这个 100%,你将不得不接管大量的滚动条高度和位置计算。用于LinearLayoutManager
的自定义computeVerticalScrollOffset()
负责在适当的时候将滚动条放置在顶部。(在拍摄视频后引入了代码。)然而,滚动条在向下滚动时会跳转。更好的放置计算可以解决这个问题。有关更多信息,请参阅此堆栈溢出问题在不同高度项目的上下文中的滚动条上。)
MainActivity.java主活动.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活动_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 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>
I'm also displaying a live feed of items, and when a new item is added, it's before the first item.我还显示项目的实时提要,当添加新项目时,它位于第一个项目之前。 But in the recycler view, I need to scroll up to see the new item.但是在回收站视图中,我需要向上滚动才能看到新项目。
To solve the problem, add to the Adapter a RecyclerView.AdapterDataObserver()
and override the onItemRangeInserted()
.要解决此问题,请向适配器添加RecyclerView.AdapterDataObserver()
并覆盖onItemRangeInserted()
。 When new data is added, check if the data is on at position 0, and the recycler view was on top (You don't want to autoscroll to first position if you were scrolling in the list).添加新数据时,检查数据是否位于位置 0,并且回收器视图位于顶部(如果您在列表中滚动,则不想自动滚动到第一个位置)。
Exemple :例子:
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);
}
}
});
This solution worked fine for me, with different type of Adapter, like ListAdapter
and PagedListAdapter
.这个解决方案对我来说很好用,有不同类型的适配器,比如ListAdapter
和PagedListAdapter
。 I firstly wanted to use a similar implementation of the accepted solution , where you add a dumb first item, but in PagedListAdapter
it's impossible as list are immutable.我首先想使用已接受解决方案的类似实现,在其中添加一个愚蠢的第一项,但在PagedListAdapter
这是不可能的,因为列表是不可变的。
This worked for me:这对我有用:
val atTop = !recycler.canScrollVertically(-1)
adapter.addToFront(item)
adapter.notifyItemInserted(0)
if (atTop) {
recycler.scrollToPosition(0)
}
The only solution that worked for me was to reverse the recycler's layout by calling setReverseLayout()
and setStackFromEnd()
on its LinearLayoutManager
.唯一对我setReverseLayout()
解决方案是通过在LinearLayoutManager
上调用setReverseLayout()
和setStackFromEnd()
来反转回收器的布局。
This might sound stupid, but the way RecyclerView
handles adding items at the end of the list is what you need at the top.这听起来可能很愚蠢,但RecyclerView
处理在列表末尾添加项目的方式正是您在顶部需要的。 The only downsize of this is that you'd have to reverse your list and start adding items to the end instead.唯一的缩小是您必须反转您的列表并开始将项目添加到末尾。
Use adapter.notifyDataSetChanged()
instead of adater.notifyItemInserted(0)
.使用adapter.notifyDataSetChanged()
而不是adater.notifyItemInserted(0)
。 This will scroll recylerView
to zero position if current scroll position is one( old zero
).如果当前滚动位置为 1( old zero
),这会将recylerView
滚动到零位置。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.