web-dev-qa-db-fra.com

Présentation Android: Recyclerview horizontal dans un recyclerview vertical dans un Viewpager avec comportements de défilement

Voici l'application que j'essaie de créer avec tous les éléments décrits ci-dessous:

 Veggies app

Tout fonctionne, cependant, je souhaite que la vue de recyclage horizontale interne ne capture aucun des parchemins verticaux. Tous les défilements verticaux doivent aller vers la vue de recyclage verticale externe, et non pas vers l'horizontale, pour que le défilement vertical permette à la barre d'outils de sortir de la vue en fonction de son scrollFlag.

Lorsque je mets le doigt sur la partie "StrawBerry Plant" de la vue de recyclage et que je fais défiler vers le haut, la barre d'outils défile:

 strawberry plant

Si je mets le doigt sur la vue de défilement horizontale et que je fais défiler vers le haut, la barre d’outils ne disparaît pas du tout.

Ce qui suit est mon code de mise en page XML jusqu'à présent.

La mise en page Activity xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:id="@+id/fragment_container"
    Android:clipChildren="false">

    <Android.support.design.widget.CoordinatorLayout
        Android:orientation="vertical"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:id="@+id/container"
        >

    <Android.support.design.widget.AppBarLayout
        Android:id="@+id/appBarLayout"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content">

        <Android.support.v7.widget.Toolbar
            Android:id="@+id/toolbar"
            Android:minHeight="?attr/actionBarSize"
            Android:background="?attr/colorPrimary"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways">

        </Android.support.v7.widget.Toolbar>

        <Android.support.design.widget.TabLayout
            Android:id="@+id/sliding_tabs"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:background="?attr/colorPrimary"
            style="@style/CustomTabLayout"
            />

    </Android.support.design.widget.AppBarLayout>
    <Android.support.v4.view.ViewPager
        Android:id="@+id/viewPager"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />

    </Android.support.design.widget.CoordinatorLayout>

</FrameLayout>

La disposition "Fruits" fragment xml (qui est le code du fragment - le fragment est étiqueté dans l'image ci-dessus):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:orientation="vertical"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <ProgressBar
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:id="@+id/progressBar"
        Android:visibility="gone"
        Android:layout_centerInParent="true"
        Android:indeterminate="true"/>

<!--    <Android.support.v7.widget.RecyclerView-->
    <com.example.simon.customshapes.VerticallyScrollRecyclerView
        Android:id="@+id/main_recyclerview"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        />

</RelativeLayout>

J'ai utilisé une classe personnalisée appelée VerticallyScrollRecyclerView qui suit l'exemple de Google concernant la gestion des événements tactiles dans un groupe de vues. Son but est d'intercepter et de consommer tous les événements de défilement vertical afin qu'il puisse faire défiler la barre d'outils: http://developer.Android.com/training/gestures/viewgroup.html

Le code pour VerticallyScrollRecyclerView est ci-dessous:

public class VerticallyScrollRecyclerView extends RecyclerView {

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

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

    public VerticallyScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    ViewConfiguration vc = ViewConfiguration.get(this.getContext());
    private int mTouchSlop = vc.getScaledTouchSlop();
    private boolean mIsScrolling;
    private float startY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        // Always handle the case of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the scroll.
            mIsScrolling = false;
            startY = ev.getY();
            return super.onInterceptTouchEvent(ev); // Do not intercept touch event, let the child handle it
        }
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                Log.e("VRecView", "its moving");
                if (mIsScrolling) {
                    // We're currently scrolling, so yes, intercept the
                    // touch event!
                    return true;
                }
                // If the user has dragged her finger horizontally more than
                // the touch slop, start the scroll
                // left as an exercise for the reader
                final float yDiff = calculateDistanceY(ev.getY());
                Log.e("yDiff ", ""+yDiff);
                // Touch slop should be calculated using ViewConfiguration
                // constants.
                if (Math.abs(yDiff) > 5) {
                    // Start scrolling!
                    Log.e("Scroll", "we are scrolling vertically");
                    mIsScrolling = true;
                    return true;
                }
                break;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }


    private float calculateDistanceY(float endY) {
        return startY - endY;
    }

}

La mise en page "Favori" qui est la vue de recyclage dans la vue de recyclage verticale:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:background="@color/white"
    xmlns:app="http://schemas.Android.com/apk/res-auto">

    <TextView
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:text="Favourite"
        Android:layout_marginTop="8dp"
        Android:layout_marginBottom="8dp"
        Android:layout_marginLeft="16dp"
        Android:id="@+id/header_fav"/>

    <Android.support.v7.widget.RecyclerView
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:orientation="horizontal"
        Android:layout_below="@+id/header_fav"
        Android:id="@+id/recyclerview_fav">
    </Android.support.v7.widget.RecyclerView>

</RelativeLayout>

Cela me dérange depuis un moment et je n'ai pas réussi à trouver une solution. Est-ce que quelqu'un sait comment résoudre ce problème? 

5 points à Griffindor pour la réponse correcte et bien sûr, des points de réputation sur SO.

53
Simon

Solution testée:

Tout ce dont vous avez besoin est d’appeler mInnerRecycler.setNestedScrollingEnabled(false); sur votre RecyclerViews intérieur 


Explication :

RecyclerView prend en charge le défilement imbriqué introduit dans API 21 via l'implémentation de l'interface NestedScrollingChild. Il s'agit d'une fonctionnalité intéressante lorsque vous avez une vue de défilement à l'intérieur d'une autre qui défile dans la même direction et que vous souhaitez faire défiler la View intérieure uniquement lorsqu'elle est activée. 

Dans tous les cas, RecyclerView par défaut appelle RecyclerView.setNestedScrollingEnabled(true); sur lui-même lors de l'initialisation. Maintenant, revenons au problème, puisque vos deux RecyclerViews sont dans le même ViewPager qui a le AppBarBehavior, le CoordinateLayout doit décider quel parchemin doit répondre lorsque vous faites défiler votre RecyclerView intérieur; lorsque le défilement imbriqué de votre RecyclerView intérieur est activé, il obtient le focus du défilement et le CoordinateLayout choisit de répondre à son défilement par-dessus le défilement de RecyclerView extérieur. Le fait est que, puisque vos RecyclerViews internes ne défilent pas verticalement, il n'y a pas de changement de défilement vertical (du point de vue de CoordinateLayout), et s'il n'y a pas de changement, le AppBarLayout ne change pas non plus.

Dans votre cas, étant donné que vos RecyclerViews internes défilent dans une direction différente, vous pouvez le désactiver, ce qui permet au CoordinateLayout de ne pas tenir compte de son défilement et de répondre au défilement de RecyclerView extérieur.


Remarquer :

L'attribut xml Android:nestedScrollingEnabled="boolean" n'est pas destiné à être utilisé avec RecyclerView et toute tentative d'utilisation de Android:nestedScrollingEnabled="false" aboutira à un Java.lang.NullPointerException. Par conséquent, vous devrez au moins le faire en code. 

97
Ari

Je suis un peu en retard mais cela fonctionnera certainement pour les autres personnes confrontées au même problème

 mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                int action = e.getAction();
               // Toast.makeText(getActivity(),"HERE",Toast.LENGTH_SHORT).show();
                switch (action) {
                    case MotionEvent.ACTION_POINTER_UP:
                        rv.getParent().requestDisallowInterceptTouchEvent(true);

                        break;
                }
                return false;
            }
2
Shahid Sarwar

Solution testée, utilisez une NestedScrollView() personnalisée.

Code:

public class CustomNestedScrollView extends NestedScrollView {
    public CustomNestedScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
         // Explicitly call computeScroll() to make the Scroller compute itself
            computeScroll();
        }
        return super.onInterceptTouchEvent(ev);
    }
}
2
Anuj

essayer

public OuterRecyclerViewAdapter(List<Item> items) {
    //Constructor stuff
    viewPool = new RecyclerView.RecycledViewPool();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    //Create viewHolder etc
    holder.innerRecyclerView.setRecycledViewPool(viewPool);

}

recylerview interne utilisera le même viewpool et ce sera plus lisse

1
Abhilash Das

Je vous suggère d'ajouter la vue de recyclage horizontale à l'intérieur de fragments tels que l'application Google.

0
Daniel Nyamasyo

si quelqu'un cherche toujours, essayez ceci:

private val Y_BUFFER = 10
private var preX = 0f
private var preY = 0f

mView.rv.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
        override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {

            }

        override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
                when (e.action) {
                    MotionEvent.ACTION_DOWN -> rv.parent.requestDisallowInterceptTouchEvent(true)
                    MotionEvent.ACTION_MOVE -> {
                        if (Math.abs(e.x - preX) > Math.abs(e.y - preY)) {
                            rv.parent.requestDisallowInterceptTouchEvent(true)
                        } else if (Math.abs(e.y - preY) > Y_BUFFER) {
                            rv.parent.requestDisallowInterceptTouchEvent(false)
                        }

                    }
                }
                preX = e.x
                preY = e.y
                return false
            }

            override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
            }
        })

il vérifie si le défilement horizontal est en cours, puis n'autorise pas le parent à gérer l'événement

0
Ahmed na