web-dev-qa-db-ja.com

ゼロ位置に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

この動作は、多くのアプリケーションにとって完全に理解可能で問題ありませんが、自動スクロールでユーザーを「気を散らさない」ことよりも最新のものを見ることが重要である「ライブフィード」の場合はそうではありません。


私はこの質問が RecyclerViewの先頭に新しい項目を追加する ...とほぼ同じであることを知っていますが、提案された回答はすべて単なる回避策です(それらのほとんどは確かに非常に優れています)。

私は実際にこの動作を変更する方法を探しています。アイテムの数に関係なく、LayoutManagerがまったく同じように動作するようにします。常にすべてのアイテムをシフトし(最初のいくつかの追加の場合と同じように)、アイテムのシフトを途中で止めないで、リストを上にスムーズにスクロールして補正します。

基本的に、smoothScrollToPositionなし、RecyclerView.SmoothScrollerLinearLayoutManagerのサブクラス化は問題ありません。私はすでにそのコードを掘り下げていますが、これまでのところ運がないため、誰かがすでにこれに対処している場合に備えて質問することにしました。任意のアイデアをありがとう!


編集:リンクされた質問からの回答を却下する理由を明確にするために:主にアニメーションの滑らかさについて心配しています。

最初のGIFで、ItemAnimatorが他のアイテムを移動しながら新しいアイテムを追加しているところに注目してください。フェードインアニメーションと移動アニメーションはどちらも同じ長さです。しかし、スムーズスクロールで項目を「移動」しているとき、私はスクロールの速度を簡単に制御できません。デフォルトのItemAnimator期間を使用しても、見栄えは良くありませんが、私の特定のケースでは、ItemAnimator期間を遅くする必要があり、さらに悪化しました。

Insert "fixed" with smooth scroll + ItemAnimator durations increased

17
oli.G

アイテムがRecyclerViewの上部に追加され、アイテムが画面に収まると、アイテムはビューホルダーにアタッチされ、RecyclerViewはアニメーションフェーズを経てアイテムを下に移動して表示します上部の新しいアイテム。

スクロールせずに新しいアイテムを表示できない場合、ビューホルダーは作成されないため、アニメーション化するものはありません。これが発生したときに新しい項目を画面に表示する唯一の方法は、ビューホルダーが作成されるようにスクロールして、ビューを画面上にレイアウトできるようにすることです。 (ビューが部分的に表示され、ビューホルダーが作成されるEdgeのケースがあるようですが、関連性がないため、この特定のインスタンスは無視します。)

したがって、問題は、2つの異なるアクション(追加されたビューのアニメーションと追加されたビューのスクロール)がユーザーに同じように見えるようにする必要があることです。基礎となるコードに飛び込んで、ビューホルダーの作成、アニメーションのタイミングなどに関して何が行われているのかを正確に把握することができます。ただし、アクションを複製できたとしても、基礎となるコードが変更されると、動作が停止する可能性があります。これはあなたが抵抗しているものです。

もう1つの方法は、RecyclerViewのゼロ位置にヘッダーを追加することです。このヘッダーが表示され、新しいアイテムが位置1に追加されると、常にアニメーションが表示されます。ヘッダーが必要ない場合は、高さをゼロにすると表示されません。次のビデオは、この手法を示しています。

[video]

これはデモのコードです。アイテムの位置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>
14
Cheticamp

adapter.notifyDataSetChanged()の代わりにadater.notifyItemInserted(0)を使用してください。現在のスクロール位置が1(old zero)の場合、これはrecylerViewをゼロ位置までスクロールします。

0
user7487242

私にとってうまくいった唯一の解決策は、LinearLayoutManagerに対してsetReverseLayout()setStackFromEnd()を呼び出すことにより、リサイクル業者のレイアウトを逆にすることでした。

これは愚かに聞こえるかもしれませんが、RecyclerViewがリストの最後にアイテムを追加する方法は、一番上に必要なものです。この唯一のダウンサイズは、リストを逆にして、代わりに最後にアイテムを追加する必要があることです。

0
Gnzlt

これは私のために働きました:

val atTop = !recycler.canScrollVertically(-1)

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

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