Flinging avec RecyclerView + AppBarLayout

171

J'utilise le nouveau CoordinatorLayout avec AppBarLayout et CollapsingToolbarLayout. Sous AppBarLayout, j'ai un RecyclerView avec une liste de contenu.

J'ai vérifié que le défilement fling fonctionne sur RecyclerView lorsque je fais défiler la liste de haut en bas. Cependant, je voudrais également que AppBarLayout défile en douceur pendant l'expansion.

Lorsque vous faites défiler vers le haut pour développer CollaspingToolbarLayout, le défilement s'arrête immédiatement une fois que vous retirez votre doigt de l'écran. Si vous faites défiler vers le haut dans un mouvement rapide, parfois le CollapsingToolbarLayout se replie également. Ce comportement avec le RecyclerView semble fonctionner très différemment que lors de l'utilisation d'un NestedScrollView.

J'ai essayé de définir différentes propriétés de défilement sur la vue de recyclage, mais je n'ai pas été en mesure de comprendre cela.

Voici une vidéo montrant certains des problèmes de défilement. https://youtu.be/xMLKoJOsTAM

Voici un exemple montrant le problème avec RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

Voici l'exemple original qui utilise un NestedScrollView de Chris Banes. https://github.com/chrisbanes/cheesesquare

tylerjroach
la source
Je rencontre exactement le même problème (j'utilise avec un RecyclerView). Si vous regardez une liste Google Play Store pour n'importe quelle application, elle semble se comporter correctement, il y a donc certainement une solution là-bas ...
Aneem
Hey Aneem, je sais que ce n'est pas la meilleure solution, mais j'ai commencé à expérimenter avec cette bibliothèque: github.com/ksoichiro/Android-ObservableScrollView . Surtout à cette activité pour obtenir les résultats dont j'avais besoin: FlexibleSpaceWithImageRecyclerViewActivity.java. Désolé d'avoir mal orthographié votre nom avant la modification.
Correction automatique
2
Même problème ici, j'ai fini par éviter AppBarLayout.
Renaud Cerrato
Oui. J'ai fini par obtenir exactement ce dont j'avais besoin de la bibliothèque OvservableScrollView. Je suis sûr que cela sera corrigé dans les versions futures.
tylerjroach
8
Le fling est bogué, un problème a été soulevé (et accepté).
Renaud Cerrato

Réponses:

114

La réponse de Kirill Boyarshinov était presque correcte.

Le problème principal est que le RecyclerView donne parfois une direction de lancement incorrecte, donc si vous ajoutez le code suivant à sa réponse, cela fonctionne correctement:

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

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

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }
}

J'espère que ca aide.

Manolo Garcia
la source
Tu as sauvé ma journée! Semble fonctionner parfaitement! Pourquoi votre réponse n'est-elle pas acceptée?
Zordid
9
si vous utilisez un SwipeRefreshLayout comme parent de votre recyclerview, ajoutez simplement ce code: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }before if (target instanceof RecyclerView && velocityY < 0) {
LucasFM
1
+ 1 En analysant ce correctif, je ne comprends pas pourquoi Google n'a pas encore résolu ce problème. Le code semble assez simple.
Gaston Flores
3
Bonjour comment réaliser la même chose avec appbarlayout et Nestedscrollview ... Merci d'avance ..
Harry Sharma
1
Cela n'a pas fonctionné pour moi = / Au fait, vous n'avez pas besoin de déplacer la classe dans le package de support pour y parvenir, vous pouvez enregistrer un DragCallback dans le constructeur.
Augusto Carmo
69

Il semble que la v23mise à jour ne l'a pas encore corrigé.

J'ai trouvé une sorte de piratage pour le réparer en le jetant. L'astuce consiste à reprendre l'événement fling si le premier enfant de ScrollingView est proche du début des données dans Adapter.

public final class FlingBehavior extends AppBarLayout.Behavior {

    public FlingBehavior() {
    }

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

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (target instanceof ScrollingView) {
            final ScrollingView scrollingView = (ScrollingView) target;
            consumed = velocityY > 0 || scrollingView.computeVerticalScrollOffset() > 0;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }
}

Utilisez-le dans votre mise en page comme ça:

 <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="your.package.FlingBehavior">
    <!--your views here-->
 </android.support.design.widget.AppBarLayout>

MODIFIER : La reconsommation des événements Fling est désormais basée sur verticalScrollOffsetau lieu de la quantité d'éléments à partir de RecyclerView.

EDIT2: Vérifiez la cible comme ScrollingViewinstance d'interface au lieu de RecyclerView. Les deux RecyclerViewet le NestedScrollingViewmettre en œuvre.

Kirill Boyarshinov
la source
L'obtention de types de chaîne n'est pas autorisée pour l'erreur layout_behavior
Vaisakh N
Je l'ai testé et fonctionne mieux l'homme! mais quel est le but du TOP_CHILD_FLING_THRESHOLD? et pourquoi c'est 3?
Julio_oa
@Julio_oa TOP_CHILD_FLING_THRESHOLD signifie que l'événement fling serait repris si la vue du recycleur défile jusqu'à l'élément dont la position est inférieure à cette valeur de seuil. Btw j'ai mis à jour la réponse à utiliser verticalScrollOffsetqui est plus générale. À présent, l'événement de lancement sera repris lors du recyclerViewdéfilement vers le haut.
Kirill Boyarshinov
Bonjour comment réaliser la même chose avec appbarlayout et Nestedscrollview ... Merci d'avance ..
Harry Sharma
2
@Hardeep passe target instanceof RecyclerViewà target instanceof NestedScrollView, ou plus pour le cas générique en target instanceof ScrollingView. J'ai mis à jour la réponse.
Kirill Boyarshinov
15

J'ai trouvé le correctif en appliquant OnScrollingListener au recyclerView. maintenant cela fonctionne très bien. Le problème est que recyclerview a fourni une valeur de consommation incorrecte et que le comportement ne sait pas quand le recyclerview défile vers le haut.

package com.singmak.uitechniques.util.coordinatorlayout;

import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by maksing on 26/3/2016.
 */
public final class RecyclerViewAppBarBehavior extends AppBarLayout.Behavior {

    private Map<RecyclerView, RecyclerViewScrollListener> scrollListenerMap = new HashMap<>(); //keep scroll listener map, the custom scroll listener also keep the current scroll Y position.

    public RecyclerViewAppBarBehavior() {
    }

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

    /**
     *
     * @param coordinatorLayout
     * @param child The child that attached the behavior (AppBarLayout)
     * @param target The scrolling target e.g. a recyclerView or NestedScrollView
     * @param velocityX
     * @param velocityY
     * @param consumed The fling should be consumed by the scrolling target or not
     * @return
     */
    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (target instanceof RecyclerView) {
            final RecyclerView recyclerView = (RecyclerView) target;
            if (scrollListenerMap.get(recyclerView) == null) {
                RecyclerViewScrollListener recyclerViewScrollListener = new RecyclerViewScrollListener(coordinatorLayout, child, this);
                scrollListenerMap.put(recyclerView, recyclerViewScrollListener);
                recyclerView.addOnScrollListener(recyclerViewScrollListener);
            }
            scrollListenerMap.get(recyclerView).setVelocity(velocityY);
            consumed = scrollListenerMap.get(recyclerView).getScrolledY() > 0; //recyclerView only consume the fling when it's not scrolled to the top
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    private static class RecyclerViewScrollListener extends RecyclerView.OnScrollListener {
        private int scrolledY;
        private boolean dragging;
        private float velocity;
        private WeakReference<CoordinatorLayout> coordinatorLayoutRef;
        private WeakReference<AppBarLayout> childRef;
        private WeakReference<RecyclerViewAppBarBehavior> behaviorWeakReference;

        public RecyclerViewScrollListener(CoordinatorLayout coordinatorLayout, AppBarLayout child, RecyclerViewAppBarBehavior barBehavior) {
            coordinatorLayoutRef = new WeakReference<CoordinatorLayout>(coordinatorLayout);
            childRef = new WeakReference<AppBarLayout>(child);
            behaviorWeakReference = new WeakReference<RecyclerViewAppBarBehavior>(barBehavior);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            dragging = newState == RecyclerView.SCROLL_STATE_DRAGGING;
        }

        public void setVelocity(float velocity) {
            this.velocity = velocity;
        }

        public int getScrolledY() {
            return scrolledY;
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            scrolledY += dy;

            if (scrolledY <= 0 && !dragging && childRef.get() != null && coordinatorLayoutRef.get() != null && behaviorWeakReference.get() != null) {
                //manually trigger the fling when it's scrolled at the top
                behaviorWeakReference.get().onNestedFling(coordinatorLayoutRef.get(), childRef.get(), recyclerView, 0, velocity, false);
            }
        }
    }
}
Mak Sing
la source
Merci pour votre message. J'ai essayé toutes les réponses sur cette page et d'après mon expérience, c'est la réponse la plus efficace. Mais, le RecylerView dans ma disposition défile en interne avant que l'AppBarLayout ait défilé hors de l'écran si je ne fais pas défiler le RecyclerView avec suffisamment de force. En d'autres termes, lorsque je fais défiler le RecyclerView avec suffisamment de force, l'AppBar fait défiler l'écran sans que le RecyclerView ne défile en interne, mais lorsque je ne fais pas défiler le RecyclerView avec suffisamment de force, le RecyclerView défile en interne avant que l'AppbarLayout ait défilé hors de l'écran. Savez-vous ce qui cause cela?
Micah Simmons
Le recyclerview reçoit toujours des événements tactiles, c'est pourquoi il défile toujours, le comportement de onNestedFling s'animerait pour faire défiler l'appbarLayout en même temps. Vous pouvez peut-être essayer de remplacer onInterceptTouch dans le comportement pour changer cela. Pour moi, le comportement actuel est acceptable d'après ce que je vois. (je ne sais pas si nous voyons la même chose)
Mak Sing
@MakSing est vraiment utile avec CoordinatorLayoutet ViewPagerconfiguration merci beaucoup pour cette solution très attendue. Veuillez écrire un GIST pour le même afin que d'autres développeurs puissent également en bénéficier. Je partage également cette solution. Merci encore.
Nitin Misra
1
@MakSing Off toutes les solutions, cela fonctionne le mieux pour moi. J'ai ajusté la vitesse transmise au onNestedFling un peu de vitesse * 0.6f ... semble lui donner un meilleur flux.
saberrider
Travaille pour moi. @MakSing Dans la méthode onScrolled, vous devez appeler onNestedFling de AppBarLayout.Behavior et non de RecyclerViewAppBarBehavior? Cela me semble un peu étrange.
Anton Malmygin
13

Il a été corrigé depuis la conception du support 26.0.0.

compile 'com.android.support:design:26.0.0'
Xiaozou
la source
2
Cela doit augmenter. Ceci est décrit ici au cas où quelqu'un serait intéressé par les détails.
Chris Dinon
1
Maintenant, il semble y avoir un problème avec la barre d'état, où lorsque vous faites défiler la barre d'état vers le bas, la barre d'état descend un peu avec le défilement ... c'est super ennuyeux!
boîte du
2
@Xiaozou J'utilise 26.1.0 et j'ai toujours des problèmes de flinging. Le lancer rapide entraîne parfois un mouvement opposé (la vitesse du mouvement est opposée / fausse comme on peut le voir dans la méthode onNestedFling). Reproduit dans Xiaomi Redmi Note 3 et Galaxy S3
dor506
@ dor506 stackoverflow.com/a/47298312/782870 Je ne sais pas si nous avons le même problème lorsque vous dites le résultat du mouvement opposé. Mais j'ai posté une réponse ici. J'espère que ça aide :)
vida
5

Il s'agit d'une version fluide de Google Support Design AppBarLayout. Si vous utilisez AppBarLayout, vous saurez qu'il a un problème avec fling.

compile "me.henrytao:smooth-app-bar-layout:<latest-version>"

Voir la bibliothèque ici .. https://github.com/henrytao-me/smooth-app-bar-layout

Mansukh Ahir
la source
4

C'est un bug de recyclage. Il est censé être corrigé dans la v23.1.0.

regardez https://code.google.com/p/android/issues/detail?id=177729

entrez la description de l'image ici

dupengtao
la source
9
v23.4.0 - Toujours pas corrigé
Arthur
3
Toujours pas corrigé dans la v25.1.0
0xcaff
v25.3.1, semble toujours mauvais.
Bawa
1
C'est enfin réglé v26.0.1!
Micer
2

Ceci est ma mise en page et le parchemin Il fonctionne comme il se doit.

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:id="@+id/container">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbarLayout"
    android:layout_height="192dp"
    android:layout_width="match_parent">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/ctlLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"
        app:contentScrim="?attr/colorPrimary"
        app:layout_collapseMode="parallax">

        <android.support.v7.widget.Toolbar
            android:id="@+id/appbar"
            android:layout_height="?attr/actionBarSize"
            android:layout_width="match_parent"
            app:layout_scrollFlags="scroll|enterAlways"
            app:layout_collapseMode="pin"/>

    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/catalogueRV"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</android.support.design.widget.CoordinatorLayout>
Luis Pe
la source
2

Ma solution jusqu'à présent, basée sur les réponses de Mak Sing et Manolo Garcia .

Ce n'est pas totalement parfait. Pour l'instant, je ne sais pas comment recalculer une vitesse valide pour éviter un effet étrange: la barre d'applications peut s'étendre plus rapidement que la vitesse de défilement. Mais l'état avec une barre d'applications étendue et une vue de recycleur défilée ne peut pas être atteint.

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

import java.lang.ref.WeakReference;

public class FlingAppBarLayoutBehavior
        extends AppBarLayout.Behavior {

    // The minimum I have seen for a dy, after the recycler view stopped.
    private static final int MINIMUM_DELTA_Y = -4;

    @Nullable
    RecyclerViewScrollListener mScrollListener;

    private boolean isPositive;

    public FlingAppBarLayoutBehavior() {
    }

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

    public boolean callSuperOnNestedFling(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            float velocityX,
            float velocityY,
            boolean consumed) {
        return super.onNestedFling(
                coordinatorLayout,
                child,
                target,
                velocityX,
                velocityY,
                consumed
        );
    }

    @Override
    public boolean onNestedFling(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            float velocityX,
            float velocityY,
            boolean consumed) {

        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }

        if (target instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) target;

            if (mScrollListener == null) {
                mScrollListener = new RecyclerViewScrollListener(
                        coordinatorLayout,
                        child,
                        this
                );
                recyclerView.addOnScrollListener(mScrollListener);
            }

            mScrollListener.setVelocity(velocityY);
        }

        return super.onNestedFling(
                coordinatorLayout,
                child,
                target,
                velocityX,
                velocityY,
                consumed
        );
    }

    @Override
    public void onNestedPreScroll(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            int dx,
            int dy,
            int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    private static class RecyclerViewScrollListener
            extends RecyclerView.OnScrollListener {

        @NonNull
        private final WeakReference<AppBarLayout> mAppBarLayoutWeakReference;

        @NonNull
        private final WeakReference<FlingAppBarLayoutBehavior> mBehaviorWeakReference;

        @NonNull
        private final WeakReference<CoordinatorLayout> mCoordinatorLayoutWeakReference;

        private int mDy;

        private float mVelocity;

        public RecyclerViewScrollListener(
                @NonNull CoordinatorLayout coordinatorLayout,
                @NonNull AppBarLayout child,
                @NonNull FlingAppBarLayoutBehavior barBehavior) {
            mCoordinatorLayoutWeakReference = new WeakReference<>(coordinatorLayout);
            mAppBarLayoutWeakReference = new WeakReference<>(child);
            mBehaviorWeakReference = new WeakReference<>(barBehavior);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                if (mDy < MINIMUM_DELTA_Y
                        && mAppBarLayoutWeakReference.get() != null
                        && mCoordinatorLayoutWeakReference.get() != null
                        && mBehaviorWeakReference.get() != null) {

                    // manually trigger the fling when it's scrolled at the top
                    mBehaviorWeakReference.get()
                            .callSuperOnNestedFling(
                                    mCoordinatorLayoutWeakReference.get(),
                                    mAppBarLayoutWeakReference.get(),
                                    recyclerView,
                                    0,
                                    mVelocity, // TODO find a way to recalculate a correct velocity.
                                    false
                            );

                }
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            mDy = dy;
        }

        public void setVelocity(float velocity) {
            mVelocity = velocity;
        }

    }

}
Zxcv
la source
Vous pouvez obtenir la vitesse actuelle d'un recyclerView (à partir de 25.1.0) en utilisant la réflexion: Field viewFlingerField = recyclerView.getClass().getDeclaredField("mViewFlinger"); viewFlingerField.setAccessible(true); Object flinger = viewFlingerField.get(recyclerView); Field scrollerField = flinger.getClass().getDeclaredField("mScroller"); scrollerField.setAccessible(true); ScrollerCompat scroller = (ScrollerCompat) scrollerField.get(flinger); velocity = Math.signum(mVelocity) * Math.abs(scroller.getCurrVelocity());
Nicholas
2

Dans mon cas, j'obtenais le problème où le lancer RecyclerViewne le faisait pas défiler en douceur, ce qui le bloquait.

C'était parce que, pour une raison quelconque, j'avais oublié que j'avais mis mon RecyclerViewdans unNestedScrollView .

C'est une erreur stupide, mais il m'a fallu un certain temps pour comprendre ...

Farbod Salamat-Zadeh
la source
1

J'ajoute une vue de 1dp de hauteur à l'intérieur de l'AppBarLayout et cela fonctionne beaucoup mieux. Ceci est ma mise en page.

  <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="com.spof.spof.app.UserBeachesActivity">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.Toolbar
        android:id="@+id/user_beaches_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_alignParentTop="true"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/WhiteTextToolBar"
        app:layout_scrollFlags="scroll|enterAlways" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp" />
</android.support.design.widget.AppBarLayout>


<android.support.v7.widget.RecyclerView
    android:id="@+id/user_beaches_rv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

Jachumbelechao Unto Mantekilla
la source
Cela ne fonctionne que si vous faites défiler vers le haut. Pas quand vous faites défiler vers le bas
Arthur
Pour moi fonctionne bien dans les deux sens. Avez-vous ajouté la vue 1dp dans l'appbarlayout?. Je ne l'ai testé que dans android lollipop et kitkat.
Jachumbelechao Unto Mantekilla
Eh bien, j'utilise également CollapsingToolbarLayout qui enveloppe la barre d'outils. J'ai mis la vue 1dp à l'intérieur. C'est un peu comme ça AppBarLayout -> CollapsingToolbarLayout -> Toolbar + 1dp view
Arthur
Je ne sais pas si cela fonctionne bien avec le CollapsingToolbarLayout. Je n'ai testé qu'avec ce code. Avez-vous essayé de placer la vue 1dp en dehors de CollapsingToolbarLayout?
Jachumbelechao Unto Mantekilla
Oui. Le défilement vers le haut fonctionne, le défilement vers le bas n'étend pas la barre d'outils.
Arthur
1

Déjà des solutions assez populaires ici, mais après avoir joué avec elles, j'ai trouvé une solution plutôt simple qui a bien fonctionné pour moi. Ma solution garantit également que le AppBarLayoutn'est étendu que lorsque le contenu déroulant atteint le sommet, un avantage par rapport aux autres solutions ici.

private int mScrolled;
private int mPreviousDy;
private AppBarLayout mAppBar;

myRecyclerView.addOnScrollListener(new OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mScrolled += dy;
            // scrolled to the top with a little more velocity than a slow scroll e.g. flick/fling.
            // Adjust 10 (vertical change of event) as you feel fit for you requirement
            if(mScrolled == 0 && dy < -10 && mPrevDy < 0) {
                mAppBar.setExpanded(true, true);
            }
            mPreviousDy = dy;
    });
Rossco
la source
Qu'est-ce que mPrevDy
ARR.s
1

La réponse acceptée n'a pas fonctionné pour moi parce que j'avais à l' RecyclerViewintérieur un SwipeRefreshLayoutet un ViewPager. Il s'agit de la version améliorée qui recherche un RecyclerViewdans la hiérarchie et devrait fonctionner pour n'importe quelle mise en page:

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

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

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (!(target instanceof RecyclerView) && velocityY < 0) {
            RecyclerView recycler = findRecycler((ViewGroup) target);
            if (recycler != null){
                target = recycler;
            }
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    @Nullable
    private RecyclerView findRecycler(ViewGroup container){
        for (int i = 0; i < container.getChildCount(); i++) {
            View childAt = container.getChildAt(i);
            if (childAt instanceof RecyclerView){
                return (RecyclerView) childAt;
            }
            if (childAt instanceof ViewGroup){
                return findRecycler((ViewGroup) childAt);
            }
        }
        return null;
    }
}
Dmide
la source
1

Réponse: il est corrigé dans la bibliothèque de support v26

mais la v26 a un problème lors du lancement. Parfois, AppBar rebondit à nouveau même si le fling n'est pas trop difficile.

Comment supprimer l'effet de rebond sur la barre d'applications?

Si vous rencontrez le même problème lors de la mise à jour pour prendre en charge la version 26, voici le résumé de cette réponse .

Solution : étendez le comportement par défaut d'AppBar et bloquez l'appel pour onNestedPreScroll () et onNestedScroll () d'AppBar.Behavior lorsque AppBar est touché alors que NestedScroll ne s'est pas encore arrêté.

vida
la source
0

Julian Os a raison.

La réponse de Manolo Garcia ne fonctionne pas si la vue de recyclage est inférieure au seuil et défile. Vous devez comparer le offsetde la vue de recyclage et le velocity to the distance, pas la position de l'élément.

J'ai fait la version java en faisant référence au code kotlin de Julian et en soustrayant la réflexion.

public final class FlingBehavior extends AppBarLayout.Behavior {

    private boolean isPositive;

    private float mFlingFriction = ViewConfiguration.getScrollFriction();

    private float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
    private final float INFLEXION = 0.35f;
    private float mPhysicalCoeff;

    public FlingBehavior(){
        init();
    }

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

    private void init(){
        final float ppi = BaseApplication.getInstance().getResources().getDisplayMetrics().density * 160.0f;
        mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                * 39.37f // inch/meter
                * ppi
                * 0.84f; // look and feel tuning
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {

        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            RecyclerView recyclerView = (RecyclerView) target;

            double distance = getFlingDistance((int) velocityY);
            if (distance < recyclerView.computeVerticalScrollOffset()) {
                consumed = true;
            } else {
                consumed = false;
            }
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    public double getFlingDistance(int velocity){
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    private double getSplineDeceleration(int velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }

}
정성민
la source
ne peut pas resloveBaseApplication
ARR.s
@ ARR.s désolé, il vous suffit de le remplacer par votre contexte comme ci-dessous.
정성민
VOTRE_CONTEXTE.getResources (). GetDisplayMetrics (). Densité * 160.0f;
정성민
0

En référence à Google issue tracker , il a été corrigé avec la version Android 26.0.0-beta2 de la bibliothèque de support

Veuillez mettre à jour votre bibliothèque de support Android version 26.0.0-beta2.

Si un problème persiste, veuillez le signaler à l' outil de suivi des problèmes de Google qu'ils rouvriront pour examen.

Prags
la source
0

Ajouter une autre réponse ici car celles ci-dessus ne répondaient pas complètement à mes besoins ou ne fonctionnaient pas très bien. Celui-ci est en partie basé sur des idées répandues ici.

Alors que fait celui-ci?

Scénario fling vers le bas: si l'AppBarLayout est réduit, il laisse le RecyclerView se balancer sans rien faire. Sinon, il réduit l'AppBarLayout et empêche le RecyclerView de faire son fling. Dès qu'il est réduit (jusqu'à ce que la vitesse donnée l'exige) et s'il reste de la vitesse, le RecyclerView est projeté avec la vitesse d'origine moins ce que l'AppBarLayout vient de consommer.

Scénario vers le haut: si le décalage de défilement du RecyclerView n'est pas nul, il est projeté avec la vitesse d'origine. Dès que cela est terminé et s'il reste de la vitesse (c'est-à-dire que le RecyclerView défile jusqu'à la position 0), l'AppBarLayout est étendu au point que la vitesse d'origine moins les demandes juste consommées. Sinon, l'AppBarLayout est étendu au point que la vitesse d'origine l'exige.

AFAIK, c'est le comportement indendu.

Il y a beaucoup de réflexion en jeu, et c'est assez coutume. Aucun problème n'a encore été trouvé. Il est également écrit en Kotlin, mais le comprendre ne devrait pas poser de problème. Vous pouvez utiliser le plugin IntelliJ Kotlin pour le compiler en bytecode -> et le décompiler en Java. Pour l'utiliser, placez-le dans le package android.support.v7.widget et définissez-le comme le comportement de CoordinatorLayout.LayoutParams d'AppBarLayout dans le code (ou ajoutez le constructeur xml applicable ou quelque chose)

/*
 * Copyright 2017 Julian Ostarek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.v7.widget

import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.widget.ScrollerCompat
import android.view.View
import android.widget.OverScroller

class SmoothScrollBehavior(recyclerView: RecyclerView) : AppBarLayout.Behavior() {
    // We're using this SplineOverScroller from deep inside the RecyclerView to calculate the fling distances
    private val splineOverScroller: Any
    private var isPositive = false

    init {
        val scrollerCompat = RecyclerView.ViewFlinger::class.java.getDeclaredField("mScroller").apply {
            isAccessible = true
        }.get(recyclerView.mViewFlinger)
        val overScroller = ScrollerCompat::class.java.getDeclaredField("mScroller").apply {
            isAccessible = true
        }.get(scrollerCompat)
        splineOverScroller = OverScroller::class.java.getDeclaredField("mScrollerY").apply {
            isAccessible = true
        }.get(overScroller)
    }

    override fun onNestedFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float, consumed: Boolean): Boolean {
        // Making sure the velocity has the correct sign (seems to be an issue)
        var velocityY: Float
        if (isPositive != givenVelocity > 0) {
            velocityY = givenVelocity * - 1
        } else velocityY = givenVelocity

        if (velocityY < 0) {
            // Decrement the velocity to the maximum velocity if necessary (in a negative sense)
            velocityY = Math.max(velocityY, - (target as RecyclerView).maxFlingVelocity.toFloat())

            val currentOffset = (target as RecyclerView).computeVerticalScrollOffset()
            if (currentOffset == 0) {
                super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
                return true
            } else {
                val distance = getFlingDistance(velocityY.toInt()).toFloat()
                val remainingVelocity = - (distance - currentOffset) * (- velocityY / distance)
                if (remainingVelocity < 0) {
                    (target as RecyclerView).addOnScrollListener(object : RecyclerView.OnScrollListener() {
                        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                                recyclerView.post { recyclerView.removeOnScrollListener(this) }
                                if (recyclerView.computeVerticalScrollOffset() == 0) {
                                    super@SmoothScrollBehavior.onNestedFling(coordinatorLayout, child, target, velocityX, remainingVelocity, false)
                                }
                            }
                        }
                    })
                }
                return false
            }
        }
        // We're not getting here anyway, flings with positive velocity are handled in onNestedPreFling
        return false
    }

    override fun onNestedPreFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float): Boolean {
        // Making sure the velocity has the correct sign (seems to be an issue)
        var velocityY: Float
        if (isPositive != givenVelocity > 0) {
            velocityY = givenVelocity * - 1
        } else velocityY = givenVelocity

        if (velocityY > 0) {
            // Decrement to the maximum velocity if necessary
            velocityY = Math.min(velocityY, (target as RecyclerView).maxFlingVelocity.toFloat())

            val topBottomOffsetForScrollingSibling = AppBarLayout.Behavior::class.java.getDeclaredMethod("getTopBottomOffsetForScrollingSibling").apply {
                isAccessible = true
            }.invoke(this) as Int
            val isCollapsed = topBottomOffsetForScrollingSibling == - child.totalScrollRange

            // The AppBarlayout is collapsed, we'll let the RecyclerView handle the fling on its own
            if (isCollapsed)
                return false

            // The AppbarLayout is not collapsed, we'll calculate the remaining velocity, trigger the appbar to collapse and fling the RecyclerView manually (if necessary) as soon as that is done
            val distance = getFlingDistance(velocityY.toInt())
            val remainingVelocity = (distance - (child.totalScrollRange + topBottomOffsetForScrollingSibling)) * (velocityY / distance)

            if (remainingVelocity > 0) {
                (child as AppBarLayout).addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
                    override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
                        // The AppBarLayout is now collapsed
                        if (verticalOffset == - appBarLayout.totalScrollRange) {
                            (target as RecyclerView).mViewFlinger.fling(velocityX.toInt(), remainingVelocity.toInt())
                            appBarLayout.post { appBarLayout.removeOnOffsetChangedListener(this) }
                        }
                    }
                })
            }

            // Trigger the expansion of the AppBarLayout
            super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
            // We don't let the RecyclerView fling already
            return true
        } else return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)
    }

    override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout?, target: View?, dx: Int, dy: Int, consumed: IntArray?) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed)
        isPositive = dy > 0
    }

    private fun getFlingDistance(velocity: Int): Double {
        return splineOverScroller::class.java.getDeclaredMethod("getSplineFlingDistance", Int::class.javaPrimitiveType).apply {
            isAccessible = true
        }.invoke(splineOverScroller, velocity) as Double
    }

}
Julian Os
la source
Comment le paramétrer?
ARR.s
0

c'est ma solution dans mon projet.
arrêtez simplement le mScroller lorsque vous obtenez Action_Down

xml:

    <android.support.design.widget.AppBarLayout
        android:id="@+id/smooth_app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:elevation="0dp"
        app:layout_behavior="com.sogou.groupwenwen.view.topic.FixAppBarLayoutBehavior">

FixAppBarLayoutBehavior.java:

    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
        if (ev.getAction() == ACTION_DOWN) {
            Object scroller = getSuperSuperField(this, "mScroller");
            if (scroller != null && scroller instanceof OverScroller) {
                OverScroller overScroller = (OverScroller) scroller;
                overScroller.abortAnimation();
            }
        }

        return super.onInterceptTouchEvent(parent, child, ev);
    }

    private Object getSuperSuperField(Object paramClass, String paramString) {
        Field field = null;
        Object object = null;
        try {
            field = paramClass.getClass().getSuperclass().getSuperclass().getDeclaredField(paramString);
            field.setAccessible(true);
            object = field.get(paramClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return object;
    }

//or check the raw file:
//https://github.com/shaopx/CoordinatorLayoutExample/blob/master/app/src/main/java/com/spx/coordinatorlayoutexample/util/FixAppBarLayoutBehavior.java
shaopx
la source
0

pour androidx,

Si votre fichier manifeste a une ligne android: hardwareAccelerated = "false", supprimez-la.

Jetwiz
la source