web-dev-qa-db-fra.com

Insertion d'éléments RecyclerView à la position zéro - restez toujours en défilement vers le haut

J'ai un RecyclerView assez standard avec un LinearLayoutManager vertical. Je continue d'insérer de nouveaux éléments en haut et j'appelle notifyItemInserted(0).

Je veux que la liste reste défilée vers le haut ; pour toujours afficher la 0ème position.

Du point de vue de mon exigence, le LayoutManager se comporte différemment en fonction du nombre d'éléments.

Bien que tous les éléments tiennent sur l'écran, il ressemble et se comporte comme je m'y attendais: le nouvel élément apparaît toujours en haut et déplace tout en dessous.

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

Cependant, dès que le non. d'éléments dépasse les limites de RecyclerView, de nouveaux éléments sont ajoutés au-dessus de l'élément actuellement visible, mais les éléments visibles restent dans vue . L'utilisateur doit faire défiler pour voir l'élément le plus récent.

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

Ce comportement est parfaitement compréhensible et très bien pour de nombreuses applications, mais pas pour un "flux en direct", où voir la chose la plus récente est plus important que "ne pas distraire" l'utilisateur avec des défilements automatiques.


Je sais que cette question est presque un doublon de Ajout d'un nouvel élément en haut de RecyclerView ... mais toutes les réponses proposées sont de simples solutions de contournement (la plupart d'entre eux sont assez bons, certes).

Je cherche un moyen de réellement changer ce comportement . Je veux que le LayoutManager agisse exactement de la même manière, quel que soit le nombre d'éléments . Je veux qu'il décale toujours tous les éléments (tout comme pour les premiers ajouts), qu'il ne s'arrête pas de déplacer les éléments à un moment donné et qu'il compense en faisant défiler la liste en haut.

Fondamentalement, pas de smoothScrollToPosition, pas de RecyclerView.SmoothScroller. Le sous-classement LinearLayoutManager est très bien. Je suis déjà en train de fouiller dans son code, mais sans succès jusqu'à présent, j'ai donc décidé de demander au cas où quelqu'un l'aurait déjà traité. Merci pour toutes les idées!


EDIT: Pour clarifier pourquoi j'écarte les réponses à la question liée: Je suis surtout préoccupé par la fluidité de l'animation.

Remarquez dans le premier GIF où ItemAnimator déplace d'autres éléments tout en ajoutant le nouveau, les animations de fondu et de déplacement ont la même durée. Mais lorsque je "déplace" les éléments par un défilement fluide, je ne peux pas facilement contrôler la vitesse du défilement . Même avec les durées ItemAnimator par défaut, cela ne semble pas aussi bon, mais dans mon cas particulier, j'ai même dû ralentir les durées ItemAnimator, ce qui est encore pire:

Insert "fixed" with smooth scroll + ItemAnimator durations increased

17
oli.G

Lorsqu'un élément est ajouté en haut de RecyclerView et que l'élément peut tenir sur l'écran, l'élément est attaché à un support de vue et RecyclerView subit une phase d'animation pour déplacer les éléments vers le bas pour les afficher le nouvel élément en haut.

Si le nouvel élément ne peut pas être affiché sans défilement, un support de vue n'est pas créé, il n'y a donc rien à animer. La seule façon d'obtenir le nouvel élément à l'écran lorsque cela se produit est de faire défiler ce qui provoque la création du support de vue afin que la vue puisse être présentée à l'écran. (Il semble y avoir un cas Edge où la vue est partiellement affichée et un support de vue est créé, mais j'ignorerai cette instance particulière car elle n'est pas pertinente.)

Ainsi, le problème est que deux actions différentes, l'animation d'une vue ajoutée et le défilement d'une vue ajoutée, doivent être faites pour avoir la même apparence pour l'utilisateur. Nous pourrions plonger dans le code sous-jacent et comprendre exactement ce qui se passe en termes de création de vues, de synchronisation d'animation, etc. Mais, même si nous pouvons dupliquer les actions, il peut se casser si le code sous-jacent change. C'est à cela que vous résistez.

Une alternative consiste à ajouter un en-tête à la position zéro du RecyclerView. Vous verrez toujours l'animation lorsque cet en-tête est affiché et de nouveaux éléments sont ajoutés à la position 1. Si vous ne voulez pas d'en-tête, vous pouvez le rendre à hauteur zéro et il ne s'affichera pas. La vidéo suivante montre cette technique:

[video]

Ceci est le code de la démo. Il ajoute simplement une entrée fictive à la position 0 des articles. Si une entrée factice n'est pas à votre goût, il existe d'autres façons d'aborder cela. Vous pouvez rechercher des moyens d'ajouter des en-têtes à RecyclerView.

(Si vous utilisez une barre de défilement, elle se comportera mal comme vous pouvez probablement le voir dans la démo. Pour corriger ce 100%, vous devrez prendre en charge une grande partie de la hauteur de la barre de défilement et du calcul du placement. La computeVerticalScrollOffset() personnalisée pour le LinearLayoutManager prend soin de placer la barre de défilement en haut le cas échéant. (Le code a été introduit après la vidéo prise.) La barre de défilement, cependant, saute lors du défilement vers le bas. Un meilleur calcul de placement résoudrait ce problème. Voir cette question Stack Overflow pour plus d'informations sur les barres de défilement dans le contexte des éléments de hauteur variable.)

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

Utilisez adapter.notifyDataSetChanged() au lieu de adater.notifyItemInserted(0). Cela fera défiler recylerView jusqu'à la position zéro si la position de défilement actuelle est une (old zero).

0
user7487242

La seule solution qui a fonctionné pour moi a été d'inverser la disposition du recycleur en appelant setReverseLayout() et setStackFromEnd() sur son LinearLayoutManager.

Cela peut sembler stupide, mais la façon dont RecyclerView gère l'ajout d'éléments à la fin de la liste est ce dont vous avez besoin en haut. La seule réduction de taille est que vous devez inverser votre liste et commencer à ajouter des éléments à la fin.

0
Gnzlt

Cela a fonctionné pour moi:

val atTop = !recycler.canScrollVertically(-1)

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

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