かなり標準的なRecyclerView
と縦型のLinearLayoutManager
があります。上部に新しい項目を挿入し続け、notifyItemInserted(0)
を呼び出しています。
リストを一番上までスクロールしたままにしたい;常に0番目の位置を表示します。
私の要件の観点から見ると、LayoutManager
の動作はアイテムの数によって異なります。
すべてのアイテムが画面に収まる一方で、見た目と動作は期待どおりです。新しいアイテムは常に上部に表示され、その下にあるすべてのアイテムがシフトされます。
ただし、いいえ。アイテムの数がRecyclerViewの境界を超えています。新しいアイテムが現在表示されているアイテムの上に追加されますが、目に見えるアイテムは残りますビュー。ユーザーは、最新の項目を表示するためにスクロールする必要があります。
この動作は、多くのアプリケーションにとって完全に理解可能で問題ありませんが、自動スクロールでユーザーを「気を散らさない」ことよりも最新のものを見ることが重要である「ライブフィード」の場合はそうではありません。
私はこの質問が RecyclerViewの先頭に新しい項目を追加する ...とほぼ同じであることを知っていますが、提案された回答はすべて単なる回避策です(それらのほとんどは確かに非常に優れています)。
私は実際にこの動作を変更する方法を探しています。アイテムの数に関係なく、LayoutManagerがまったく同じように動作するようにします。常にすべてのアイテムをシフトし(最初のいくつかの追加の場合と同じように)、アイテムのシフトを途中で止めないで、リストを上にスムーズにスクロールして補正します。
基本的に、smoothScrollToPosition
なし、RecyclerView.SmoothScroller
。 LinearLayoutManager
のサブクラス化は問題ありません。私はすでにそのコードを掘り下げていますが、これまでのところ運がないため、誰かがすでにこれに対処している場合に備えて質問することにしました。任意のアイデアをありがとう!
編集:リンクされた質問からの回答を却下する理由を明確にするために:主にアニメーションの滑らかさについて心配しています。
最初のGIFで、ItemAnimator
が他のアイテムを移動しながら新しいアイテムを追加しているところに注目してください。フェードインアニメーションと移動アニメーションはどちらも同じ長さです。しかし、スムーズスクロールで項目を「移動」しているとき、私はスクロールの速度を簡単に制御できません。デフォルトのItemAnimator
期間を使用しても、見栄えは良くありませんが、私の特定のケースでは、ItemAnimator
期間を遅くする必要があり、さらに悪化しました。
アイテムがRecyclerView
の上部に追加され、アイテムが画面に収まると、アイテムはビューホルダーにアタッチされ、RecyclerView
はアニメーションフェーズを経てアイテムを下に移動して表示します上部の新しいアイテム。
スクロールせずに新しいアイテムを表示できない場合、ビューホルダーは作成されないため、アニメーション化するものはありません。これが発生したときに新しい項目を画面に表示する唯一の方法は、ビューホルダーが作成されるようにスクロールして、ビューを画面上にレイアウトできるようにすることです。 (ビューが部分的に表示され、ビューホルダーが作成されるEdgeのケースがあるようですが、関連性がないため、この特定のインスタンスは無視します。)
したがって、問題は、2つの異なるアクション(追加されたビューのアニメーションと追加されたビューのスクロール)がユーザーに同じように見えるようにする必要があることです。基礎となるコードに飛び込んで、ビューホルダーの作成、アニメーションのタイミングなどに関して何が行われているのかを正確に把握することができます。ただし、アクションを複製できたとしても、基礎となるコードが変更されると、動作が停止する可能性があります。これはあなたが抵抗しているものです。
もう1つの方法は、RecyclerView
のゼロ位置にヘッダーを追加することです。このヘッダーが表示され、新しいアイテムが位置1に追加されると、常にアニメーションが表示されます。ヘッダーが必要ない場合は、高さをゼロにすると表示されません。次のビデオは、この手法を示しています。
これはデモのコードです。アイテムの位置0にダミーエントリを追加するだけです。ダミーエントリが好みに合わない場合は、他の方法で対処できます。 RecyclerView
にヘッダーを追加する方法を検索できます。
(スクロールバーを使用する場合、おそらくデモからわかるように誤動作します。これを100%修正するには、スクロールバーの高さと配置計算の多くを引き継ぐ必要があります。 LinearLayoutManager
のカスタムcomputeVerticalScrollOffset()
は、必要に応じてスクロールバーを最上部に配置します(ビデオは撮影後にコードが導入されました)ただし、スクロールバーは下にスクロールするとジャンプします。計算によりこの問題が処理されます。高さの異なるアイテムのコンテキストでのスクロールバーの詳細については、 このスタックオーバーフローの質問 を参照してください。)
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>
adapter.notifyDataSetChanged()
の代わりにadater.notifyItemInserted(0)
を使用してください。現在のスクロール位置が1(old zero
)の場合、これはrecylerView
をゼロ位置までスクロールします。
私にとってうまくいった唯一の解決策は、LinearLayoutManager
に対してsetReverseLayout()
とsetStackFromEnd()
を呼び出すことにより、リサイクル業者のレイアウトを逆にすることでした。
これは愚かに聞こえるかもしれませんが、RecyclerView
がリストの最後にアイテムを追加する方法は、一番上に必要なものです。この唯一のダウンサイズは、リストを逆にして、代わりに最後にアイテムを追加する必要があることです。
これは私のために働きました:
val atTop = !recycler.canScrollVertically(-1)
adapter.addToFront(item)
adapter.notifyItemInserted(0)
if (atTop) {
recycler.scrollToPosition(0)
}