web-dev-qa-db-fra.com

Défilement lisse NestedScrollView et Horizontal RecyclerView

J'ai un unique nidedcrollview vertical qui contient un tas de recyclerview avec une configuration de layoutmanager horizontale. L'idée est assez similaire à l'apparence du nouveau Google Play Store. Je suis capable de le rendre fonctionnel mais ce n'est pas lisse du tout. Voici les problèmes:

1) L'élément de recyclage horizontal horizontal ne parvient pas à intercepter l'événement tactile la plupart du temps, même si je tape dessus correctement. La vue de défilement semble avoir la priorité pour la plupart des mouvements. Il m'est difficile d'accroche le mouvement horizontal. Cet UX est frustrant car je dois essayer plusieurs fois avant que cela fonctionne. Si vous consultez le Play Store, il est capable d’intercepter l’événement tactile et il fonctionne parfaitement. J'ai remarqué que, dans le Play Store, de nombreux écrans de recyclage horizontaux étaient organisés de cette manière. Pas de scrollview.

2) La hauteur des vues de recyclage horizontales doit être définie manuellement et il n’existe pas de moyen facile de calculer la hauteur des éléments enfants.

Voici la mise en page que j'utilise:

<Android.support.v4.widget.NestedScrollView
    Android:id="@+id/scroll"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:clipToPadding="false"
    Android:background="@color/dark_bgd"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:orientation="vertical">

        <LinearLayout
            Android:id="@+id/main_content_container"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:visibility="gone"
            tools:visibility="gone"
            Android:orientation="vertical">                

                <Android.support.v7.widget.RecyclerView
                    Android:id="@+id/starring_list"
                    Android:paddingLeft="@dimen/spacing_major"
                    Android:paddingRight="@dimen/spacing_major"
                    Android:layout_width="match_parent"
                    Android:layout_height="180dp" />

Ce modèle d’interface utilisateur est très basique et très probablement utilisé dans de nombreuses applications différentes. J'ai lu beaucoup d'OS où les gens disent que c'est une mauvaise idée de mettre une liste dans une liste, mais c'est un modèle d'interface utilisateur très courant et moderne utilisé partout. Pensez à Netflix comme interface avec une série de listes de défilement horizontales une liste verticale. N'y a-t-il pas un moyen en douceur d'y parvenir?

Exemple d'image du magasin:

 Google Play Store

17
falc0nit3

Le problème de défilement régulier est donc résolu maintenant. Cela était dû à un bogue de NestedScrollView dans la bibliothèque de support technique de la conception (actuellement 23.1.1).

Vous pouvez en savoir plus sur le problème et sur la solution simple ici: https://code.google.com/p/Android/issues/detail?id=194398

En bref, une fois que vous avez effectué une analyse, nestedscrollview n'a pas enregistré d'en-tête sur le composant scroller; un événement supplémentaire, 'ACTION_DOWN', a donc été nécessaire pour que le parent nestedscrollview ne puisse plus intercepter les événements suivants. Ainsi, si vous avez essayé de faire défiler votre liste d'enfants (ou viewpager), après un fling, le premier contact libère la liaison NSV parent et les contacts suivants fonctionnent. Cela rendait l'UX vraiment mauvais.

Il faut essentiellement ajouter cette ligne à l'événement ACTION_DOWN du NSV:

computeScroll();

Voici ce que j'utilise:

public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;

public MyNestedScrollView(Context context) {
    super(context);
    init(context);
}

private void init(Context context) {
    ViewConfiguration config = ViewConfiguration.get(context);
    slop = config.getScaledEdgeSlop();
}

public MyNestedScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}


private float xDistance, yDistance, lastX, lastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            xDistance = yDistance = 0f;
            lastX = ev.getX();
            lastY = ev.getY();

            // This is very important line that fixes 
           computeScroll();


            break;
        case MotionEvent.ACTION_MOVE:
            final float curX = ev.getX();
            final float curY = ev.getY();
            xDistance += Math.abs(curX - lastX);
            yDistance += Math.abs(curY - lastY);
            lastX = curX;
            lastY = curY;

            if (xDistance > yDistance) {
                return false;
            }
    }


    return super.onInterceptTouchEvent(ev);
}

}

Utilisez cette classe à la place de nestedscrollview dans le fichier xml et les listes enfant doivent intercepter et gérer correctement les événements tactiles. 

Ouf, il y a en fait beaucoup de bugs comme ceux-ci qui me donnent envie d'abandonner complètement la bibliothèque de support de conception et de la revoir quand elle sera plus mature. 

19
falc0nit3

J'ai réussi à faire défiler horizontalement un parent à défilement vertical avec ViewPager:

<Android.support.v4.widget.NestedScrollView

    ...

    <Android.support.v4.view.ViewPager
        Android:id="@+id/pager_known_for"
        Android:layout_width="match_parent"
        Android:layout_height="350dp"
        Android:minHeight="350dp"
        Android:paddingLeft="24dp"
        Android:paddingRight="24dp"
        Android:clipToPadding="false"/>

classe publique UniversityKnownForPagerAdapter étend PagerAdapter {

public UniversityKnownForPagerAdapter(Context context) {
    mContext = context;
    mInflater = LayoutInflater.from(mContext);
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);

    ...

    container.addView(rootView);

    return rootView;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View)object);
}

@Override
public int getCount() {
    return 4;
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return (view == object);
}

Seul problème: vous devez fournir une hauteur fixe au téléavertisseur de vue.

0
Guillaume Imbert

Depuis que la solution falc0nit3 ne fonctionne plus (actuellement le projet utilise la version 28.0.0 de la bibliothèque de support), j’en ai trouvé une autre. 

La raison de l’arrière-plan du problème est toujours la même, la vue défilante mange l’événement en renvoyant true au second tap, là où elle ne devrait pas, car naturellement le second tap sur la vue fling arrête le défilement et peut être utilisé avec le prochain événement move commencer en face de scroll Le problème est reproduit comme avec NestedScrollView comme avec RecyclerView. Ma solution consiste à arrêter le défilement manuellement avant que la vue native puisse l’intercepter dans onInterceptTouchEvent. Dans ce cas, l'événement ACTION_DOWN ne sera pas consommé, car il a déjà été arrêté. 

Donc, pour NestedScrollView:

class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
    NestedScrollView(context, attrs) {

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev)
        }
        return super.onInterceptTouchEvent(ev)
    }
}

Pour RecyclerView:

class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
    RecyclerView(context, attrs) {

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        if (e.actionMasked == MotionEvent.ACTION_DOWN) {
            this.stopScroll()
        }
        return super.onInterceptTouchEvent(e)
    }

}

Malgré la solution pour RecyclerView, la lecture est facile, mais pour NestedScrollView, c'est un peu compliqué. Malheureusement, il n’existe aucun moyen clair d’arrêter le défilement manuel dans le widget, la seule responsabilité étant de gérer le défilement (omg). Je suis intéressant dans la méthode abortAnimatedScroll(), mais c'est privé. Il est possible d’utiliser la réflexion pour la contourner, mais pour moi le mieux est d’appeler la méthode, qui appelle abortAnimatedScroll() elle-même . Regardez le traitement onTouchEvent de ACTION_DOWN:

 /*
 * If being flinged and user touches, stop the fling. isFinished
 * will be false if being flinged.
 */
if (!mScroller.isFinished()) {
    Log.i(TAG, "abort animated scroll");
    abortAnimatedScroll();
}

En gros, l'arrêt de fling est géré dans cette méthode, mais un peu plus tard, nous devons l'appeler pour corriger le bogue.

Malheureusement, pour cette raison, nous ne pouvons pas simplement créer OnTouchListener et le définir à l'extérieur, de sorte que seul l'héritage réponde aux exigences

0
Beloo