web-dev-qa-db-fra.com

RecyclerView avec des objets de différentes hauteurs: barre de défilement

J'ai une RecyclerView avec des objets de différentes hauteurs avec une barre de défilement. En raison de la hauteur différente des éléments, la barre de défilement change de taille, en fonction des éléments actuellement affichés (voir les captures d'écran). J'ai créé un exemple de projet qui affiche le problème ici

  1. Quelqu'un at-il eu le même problème et l'a résolu?
  2. Comment puis-je remplacer le calcul de la hauteur et de la position de la barre de défilement pour arriver à une implémentation propre?

EDIT: La position et la hauteur de la barre de défilement peuvent être contrôlées en remplaçant les options RecyclerViewscomputeVerticalScrollOffset, computeVerticalScrollRange et computeVerticalScrollExtent. . 

Le problème, à mon avis, est que RecyclerView estime la hauteur totale de tous les éléments en fonction des éléments actuellement visibles et définit la position et la hauteur de la barre de défilement en conséquence. Une façon de résoudre ce problème pourrait être de donner une meilleure estimation de la hauteur totale de tous les articles. 

 large scrollbar  small scrollbar

9
asco

Le meilleur moyen de gérer cette situation consiste peut-être à calculer la plage de la barre de défilement en fonction de la taille de chaque élément. Cela peut ne pas être pratique ou souhaitable. Au lieu de cela, voici une implémentation simple d'un RecyclerView personnalisé avec lequel vous pouvez jouer pour essayer d'obtenir ce que vous voulez. Il vous montrera comment utiliser les différentes méthodes de défilement pour contrôler la barre de défilement. Il va coller la taille du pouce à une taille initiale basée sur le nombre d'éléments affichés. L'essentiel à retenir est que la plage de défilement est arbitraire, mais que toutes les autres mesures (étendue, décalage) doivent utiliser les mêmes unités. 

Voir la documentation de computeVerticalScrollRange() .

Voici une vidéo du résultat.

 enter image description here

Mise à jour: Le code a été mis à jour pour corriger quelques problèmes: Le mouvement du pouce est moins saccadé et le pouce s'immobilise maintenant au bas de la liste lorsque la RecyclerView défile vers le bas. Il y a également quelques mises en garde qui sont données après le code.

MyRecyclerView.Java (mise à jour)

public class MyRecyclerView extends RecyclerView {
    // The size of the scroll bar thumb in our units.
    private int mThumbHeight = UNDEFINED;

    // Where the RecyclerView cuts off the views when the RecyclerView is scrolled to top.
    // For example, if 1/4 of the view at position 9 is displayed at the bottom of the RecyclerView,
    // mTopCutOff will equal 9.25. This value is used to compute the scroll offset.
    private float mTopCutoff = UNDEFINED;

    public MyRecyclerView(Context context) {
        super(context);
    }

    public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * Retrieves the size of the scroll bar thumb in our arbitrary units.
     *
     * @return Scroll bar thumb height
     */
    @Override
    public int computeVerticalScrollExtent() {
        return (mThumbHeight == UNDEFINED) ? 0 : mThumbHeight;
    }

    /**
     * Compute the offset of the scroll bar thumb in our scroll bar range.
     *
     * @return Offset in scroll bar range.
     */
    @Override
    public int computeVerticalScrollOffset() {
        return (mTopCutoff == UNDEFINED) ? 0 : (int) ((getCutoff() - mTopCutoff) * ITEM_HEIGHT);
    }

    /**
     * Computes the scroll bar range. It will simply be the number of items in the adapter
     * multiplied by the given item height. The scroll extent size is also computed since it
     * will not vary. Note: The RecyclerView must be positioned at the top or this method
     * will throw an IllegalStateException.
     *
     * @return The scroll bar range
     */
    @Override
    public int computeVerticalScrollRange() {
        if (mThumbHeight == UNDEFINED) {
            LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
            int firstCompletePositionw = lm.findFirstCompletelyVisibleItemPosition();

            if (firstCompletePositionw != RecyclerView.NO_POSITION) {
                if (firstCompletePositionw != 0) {
                    throw (new IllegalStateException(ERROR_NOT_AT_TOP_OF_RANGE));
                } else {
                    mTopCutoff = getCutoff();
                    mThumbHeight = (int) (mTopCutoff * ITEM_HEIGHT);
                }
            }
        }
        return getAdapter().getItemCount() * ITEM_HEIGHT;
    }

    /**
     * Determine where the RecyclerVIew display cuts off the list of views. The range is
     * zero through (getAdapter().getItemCount() - 1) inclusive.
     *
     * @return The position in the RecyclerView where the displayed views are cut off. If the
     * bottom view is partially displayed, this will be a fractional number.
     */
    private float getCutoff() {
        LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
        int lastVisibleItemPosition = lm.findLastVisibleItemPosition();

        if (lastVisibleItemPosition == RecyclerView.NO_POSITION) {
            return 0f;
        }

        View view = lm.findViewByPosition(lastVisibleItemPosition);
        float fractionOfView;

        if (view.getBottom() < getHeight()) { // last visible position is fully visible
            fractionOfView = 0f;
        } else { // last view is cut off and partially displayed
            fractionOfView = (float) (getHeight() - view.getTop()) / (float) view.getHeight();
        }
        return lastVisibleItemPosition + fractionOfView;
    }

    private static final int ITEM_HEIGHT = 1000; // Arbitrary, make largish for smoother scrolling
    private static final int UNDEFINED = -1;
    private static final String ERROR_NOT_AT_TOP_OF_RANGE
            = "RecyclerView must be positioned at the top of its range.";
}

Mises en garde Les problèmes suivants devront peut-être être résolus en fonction de la mise en œuvre.

L'exemple de code ne fonctionne que pour le défilement vertical. L'exemple de code suppose également que le contenu de RecyclerView est statique. Toute mise à jour des données sauvegardant la RecyclerView peut entraîner des problèmes de défilement. Si des modifications sont apportées qui affectent la hauteur de toute vue affichée sur le premier plein écran de la variable RecyclerView, le défilement sera désactivé. Les modifications ci-dessous qui fonctionneront probablement bien. Cela est dû à la manière dont le code calcule le décalage de défilement.

Pour déterminer la valeur de base du décalage de défilement (variable mTopCutOff), vous devez faire défiler RecyclerView vers le haut la première fois que computeVerticalScrollRange() est appelé afin de pouvoir mesurer les vues; sinon, le code s'arrêtera avec une "IllegalStateException". Cela est particulièrement gênant lors d’un changement d’orientation si la RecyclerView fait défiler la liste. Une solution simple consiste à empêcher la restauration de la position de défilement afin que celle-ci revienne par défaut au sommet lors d’un changement d’orientation.

(Ce qui suit n'est probablement pas la meilleure solution ...)} _

var lm: LinearLayoutManager = object : LinearLayoutManager(this) {
    override fun onRestoreInstanceState(state: Parcelable?) {
        // Don't restore
    }
}

J'espère que ça aide. (Au fait, votre MCVE a rendu cela beaucoup plus facile.)

5
Cheticamp

Utilisez les positions d'élément comme métrique de progression du défilement. Cela rendra votre indicateur de défilement un peu nerveux, mais au moins, il restera de taille fixe.

Il existe plusieurs implémentations d'indicateurs de défilement personnalisés pour RecyclerView. La plupart des doubles comme des défileurs rapides.

Voici ma propre implémentation , basée sur bibliothèque RecyclerViewFastScroller . Fondamentalement, il faut créer une sous-classe View personnalisée, qui sera animée, de la même manière que ScrollView et DrawerLayout:

  • Enregistrer le décalage actuel
  • Pendant l'animation, décalage de la position du pouce Voir via les appels View#offset*
  • Pendant la mise en page, définissez la position en fonction du décalage actuel.

Vous ne voulez probablement pas commencer à apprendre toute cette magie maintenant, utilisez simplement une bibliothèque existante à défilement rapide (RecyclerViewFastScroller ou un de ses clones).

1
user1643723

Si je ne me trompe pas, l'attribut Android:scollBarSize="Xdp" devrait fonctionner pour vous. Ajoutez-le à votre RecyclerView xml.

De cette façon, vous décidez de la taille et elle restera fixe.

0
RED_