Lanciare con RecyclerView + AppBarLayout


171

Sto usando il nuovo CoordinatorLayout con AppBarLayout e CollapsingToolbarLayout. Sotto AppBarLayout, ho un RecyclerView con un elenco di contenuti.

Ho verificato che lo scorrimento di Fling funziona su RecyclerView mentre sto scorrendo su e giù nell'elenco. Tuttavia, vorrei anche che AppBarLayout scorresse uniformemente durante l'espansione.

Quando si scorre verso l'alto per espandere la disposizione di CollaspingToolbar, lo scorrimento si interrompe immediatamente una volta sollevando il dito dallo schermo. Se scorri verso l'alto con un rapido movimento, a volte anche CollapsingToolbarLayout ricomprime. Questo comportamento con RecyclerView sembra funzionare in modo molto diverso rispetto a quando si utilizza un NestedScrollView.

Ho provato a impostare diverse proprietà di scorrimento sul recyclerview ma non sono stato in grado di capirlo.

Ecco un video che mostra alcuni dei problemi di scorrimento. https://youtu.be/xMLKoJOsTAM

Ecco un esempio che mostra il problema con RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

Ecco l'esempio originale che utilizza un NestedScrollView di Chris Banes. https://github.com/chrisbanes/cheesesquare


Sto riscontrando lo stesso problema esatto (sto utilizzando un RecyclerView). Se guardi un elenco di Google Play Store per qualsiasi app, sembra comportarsi correttamente, quindi c'è sicuramente una soluzione là fuori ...
Aneem

Ehi Aneem, so che questa non è la soluzione migliore, ma ho iniziato a sperimentare con questa libreria: github.com/ksoichiro/Android-ObservableScrollView . Soprattutto in questa attività per ottenere i risultati di cui avevo bisogno: FlexibleSpaceWithImageRecyclerViewActivity.java. Ci scusiamo per l'ortografia errata del nome prima della modifica.
Correzione automatica

2
Lo stesso problema qui, ho finito per evitare AppBarLayout.
Renaud Cerrato,

Sì. Ho finito per ottenere esattamente ciò di cui avevo bisogno dalla libreria OvservableScrollView. Sono sicuro che verrà risolto nelle versioni future.
Tylerjroach,

8
L'avventura è buggy, un problema è stato sollevato (e accettato).
Renaud Cerrato,

Risposte:


114

La risposta di Kirill Boyarshinov era quasi corretta.

Il problema principale è che RecyclerView a volte dà una direzione di lancio errata, quindi se aggiungi il seguente codice alla sua risposta funziona correttamente:

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;
    }
}

Spero che questo aiuti.


Mi hai salvato la giornata! Sembra funzionare perfettamente! Perché la tua risposta non è stata accettata?
Zordid,

9
se stai usando uno SwipeRefreshLayout come genitore del tuo recyclerview, aggiungi questo codice: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }prima if (target instanceof RecyclerView && velocityY < 0) {
LucasFM

1
+ 1 Analizzando questa correzione, non capisco perché Google non l'abbia ancora risolto. Il codice sembra essere abbastanza semplice.
Gaston Flores,

3
Ciao, come ottenere la stessa cosa con appbarlayout e Nestedscrollview ... Grazie in anticipo ..
Harry Sharma,

1
Non ha funzionato per me = / A proposito, non è necessario spostare la classe nel pacchetto di supporto per ottenerlo, è possibile registrare un DragCallback nel costruttore.
Augusto Carmo,

69

Sembra quello v23 aggiornamento non l'abbia ancora risolto.

Ho trovato una sorta di hack per risolverlo con il lancio. Il trucco è di ricominciare l'evento fling se il figlio principale di ScrollingView è vicino all'inizio dei dati nell'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);
    }
}

Usalo nel tuo layout in questo modo:

 <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>

EDIT: il ricominciamento dell'evento Fling ora si basa verticalScrollOffsetinvece sulla quantità di elementi sopra RecyclerView.

EDIT2: controlla la destinazione come ScrollingViewistanza dell'interfaccia anziché RecyclerView. Entrambi RecyclerViewe NestedScrollingViewimplementarlo.


Non è possibile ottenere tipi di stringa per errore
layout_behavior

L'ho provato e funziona meglio amico! ma qual è lo scopo di TOP_CHILD_FLING_THRESHOLD? e perché è 3?
Julio_oa,

@Julio_oa TOP_CHILD_FLING_THRESHOLD significa che l'evento di lancio verrà ripreso se la vista del riciclatore viene fatta scorrere sull'elemento la cui posizione è al di sotto di questo valore di soglia. Tra l'altro ho aggiornato la risposta per l'uso verticalScrollOffsetche è più generale. Ora l'evento fling verrà ricominciato quando recyclerViewsi scorre verso l'alto.
Kirill Boyarshinov,

Ciao, come ottenere la stessa cosa con appbarlayout e Nestedscrollview ... Grazie in anticipo ..
Harry Sharma,

2
@Hardeep cambia target instanceof RecyclerViewin target instanceof NestedScrollView, o più per il caso generico in target instanceof ScrollingView. Ho aggiornato la risposta.
Kirill Boyarshinov l'

15

Ho trovato la correzione applicando OnScrollingListener a recyclerView. ora funziona molto bene. Il problema è che recyclerview ha fornito il valore consumato errato e il comportamento non è a conoscenza del momento in cui il recyclerview viene spostato verso l'alto.

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);
            }
        }
    }
}

Grazie per il tuo post. Ho provato tutte le risposte su questa pagina e nella mia esperienza questa è la risposta più efficace. Tuttavia, RecylerView nel mio layout scorre internamente prima che AppBarLayout abbia fatto scorrere lo schermo fuori se non faccio scorrere RecyclerView con forza sufficiente. In altre parole, quando scorro il RecyclerView con una forza sufficiente, l'AppBar scorre fuori dallo schermo senza che il RecyclerView scorra internamente, ma quando non faccio scorrere il RecyclerView con una forza sufficiente, il RecyclerView scorre internamente prima che AppbarLayout abbia fatto scorrere lo schermo. Sai cosa lo sta causando?
Micah Simmons,

Il recyclerview continua a ricevere eventi tattili ed è per questo che scorre ancora, il comportamento su NestedFling si animerebbe per scorrere l'appLayout allo stesso tempo. Forse puoi provare a sovrascrivere onInterceptTouch nel comportamento per cambiarlo. Per me l'attuale comportamento è accettabile da quello che vedo. (non sono sicuro se stiamo vedendo la stessa cosa)
Fa cantare il

@MakSing è davvero utile con l' installazione CoordinatorLayoute ViewPagergrazie mille per questa soluzione più attesa. Si prega di scrivere un GIST per lo stesso in modo che anche altri sviluppatori possano trarne beneficio. Condivido anche questa soluzione. Grazie ancora.
Nitin Misra,

1
@MakSing Off di tutte le soluzioni, questo funziona meglio per me. Ho regolato la velocità assegnata a onNestedFling un po 'di velocità * 0,6f ... sembra dare un flusso più piacevole ad esso.
saberrider

Per me va bene. @MakSing Nel metodo onScrolled devi chiamare onNestedFling di AppBarLayout.Behavior e non di RecyclerViewAppBarBehavior? Mi sembra un po 'strano.
Anton Malmygin,

13

È stato risolto dalla progettazione del supporto 26.0.0.

compile 'com.android.support:design:26.0.0'

2
Questo deve salire. Questo è descritto qui nel caso qualcuno fosse interessato ai dettagli.
Chris Dinon,

1
Ora sembra esserci un problema con la barra di stato, in cui quando si scorre verso il basso la barra di stato si abbassa un po 'con lo scorrimento ... è super fastidioso!
scatola

2
@Xiaozou Sto usando 26.1.0 e ho ancora problemi con il lancio. L'avvio rapido a volte provoca un movimento opposto (la velocità del movimento è opposta / errata, come si può vedere nel metodo onNestedFling). Riprodotto in Xiaomi Redmi Note 3 e Galaxy S3
dor506,

@ dor506 stackoverflow.com/a/47298312/782870 Non sono sicuro se abbiamo lo stesso problema quando si dice contrario conseguenza il movimento. Ma ho pubblicato una risposta qui. Spero che sia d'aiuto :)
vida,



2

Questo è il mio layout e la pergamena Funziona come dovrebbe.

<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>

2

La mia soluzione finora, basata sulle risposte di Mak Sing e Manolo Garcia .

Non è del tutto perfetto. Per ora non so come ricalcolare una velocità valida per evitare uno strano effetto: l'appbar può espandersi più velocemente della velocità di scorrimento. Ma non è possibile raggiungere lo stato con una barra delle applicazioni espansa e una vista riciclata a scorrimento.

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;
        }

    }

}

È possibile ottenere la velocità corrente di un recyclerView (a partire dal 25.1.0) usando la riflessione: 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

Nel mio caso, stavo ottenendo il problema in cui il lanciare il RecyclerView non lo faceva scorrere senza intoppi, facendolo rimanere bloccato.

Questo perché, per qualche ragione, avevo dimenticato che avevo messo la mia RecyclerViewin unNestedScrollView .

È un errore sciocco, ma mi ci è voluto un po 'per capirlo ...


1

Aggiungo una vista di 1dp di altezza all'interno di AppBarLayout e poi funziona molto meglio. Questo è il mio layout.

  <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" />


Funziona solo se scorri verso l'alto. Non quando scorri verso il basso però
Arthur,

Per me funziona bene in entrambe le direzioni. Hai aggiunto la vista 1dp all'interno dell'appbarlayout ?. L'ho provato solo su Android Lollipop e Kitkat.
Jachumbelechao Unto Mantekilla,

Bene, sto anche usando CollapsingToolbarLayout che avvolge la barra degli strumenti. Ho inserito la vista 1dp al suo interno. È un po 'come questo AppBarLayout -> CollapsingToolbarLayout -> Toolbar + 1dp view
Arthur

Non so se funziona bene con CollapsingToolbarLayout. Ho provato solo con questo codice. Hai provato a mettere la vista 1dp al di fuori di CollapsingToolbarLayout?
Jachumbelechao Unto Mantekilla,

Sì. Scorri verso l'alto funziona, lo scorrimento verso il basso non espande la barra degli strumenti.
Arthur,

1

Già alcune soluzioni piuttosto popolari qui, ma dopo aver giocato con loro ho trovato una soluzione piuttosto semplice che ha funzionato bene per me. La mia soluzione assicura anche che AppBarLayoutsi espanda solo quando il contenuto scorrevole raggiunge il massimo, un vantaggio rispetto ad altre soluzioni qui.

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;
    });

cos'è mPrevDy
ARR.s il

1

La risposta accettata non ha funzionato per me perché avevo RecyclerViewall'interno di a SwipeRefreshLayoute a ViewPager. Questa è la versione migliorata che cerca un RecyclerViewnella gerarchia e dovrebbe funzionare per qualsiasi layout:

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;
    }
}

1

Risposta: è stato risolto nella libreria di supporto v26

ma v26 ha qualche problema nel lanciare. A volte, AppBar rimbalza di nuovo anche se lanciare non è troppo difficile.

Come rimuovo l'effetto di rimbalzo sulla barra delle applicazioni?

Se riscontri lo stesso problema durante l'aggiornamento per supportare la v26, ecco il riepilogo di questa risposta .

Soluzione : estendi il comportamento predefinito di AppBar e blocca la chiamata per onNestedPreScroll () e onNestedScroll () di AppBar.Behavior quando AppBar viene toccato mentre NestedScroll non si è ancora arrestato.


0

Julian Os ha ragione.

La risposta di Manolo Garcia non funziona se il recyclerview è al di sotto della soglia e scorre. È necessario confrontare il valore offsetdi recyclerview e il velocity to the distance, non la posizione dell'articolo.

Ho realizzato la versione java facendo riferimento al codice kotlin di julian e sottraendo la riflessione.

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));
    }

}

impossibile resloveBaseApplication
ARR.s

@ ARR.s scusate, basta sostituirlo con il contesto come di seguito.
정성민

YOUR_CONTEXT.getResources (). GetDisplayMetrics (). Densità * 160.0f;
정성민



0

L'aggiunta di un'altra risposta qui come quelle sopra non ha soddisfatto completamente le mie esigenze o non ha funzionato molto bene. Questo è parzialmente basato su idee diffuse qui.

Quindi cosa fa questo?

Lancio verso il basso dello scenario: se AppBarLayout è compresso, consente a RecyclerView di lanciarsi da solo senza fare nulla. In caso contrario, comprime AppBarLayout e impedisce a RecyclerView di eseguire il lancio. Non appena è collassato (fino al punto richiesto dalla velocità data) e se è rimasta la velocità, RecyclerView viene lanciato con la velocità originale meno ciò che l'AppBarLayout ha appena consumato collassando.

Lancio verso l'alto dello scenario: se l'offset di scorrimento del RecyclerView non è zero, viene lanciato con la velocità originale. Non appena ciò è terminato e se rimane ancora la velocità (ovvero il RecyclerView è passato alla posizione 0), AppBarLayout si espande fino al punto in cui la velocità originale meno le richieste appena consumate. Altrimenti, AppBarLayout viene espanso fino al punto richiesto dalla velocità originale.

AFAIK, questo è il comportamento indotto.

C'è molta riflessione ed è piuttosto personalizzato. Nessun problema riscontrato ancora. È anche scritto in Kotlin, ma capire che non dovrebbe essere un problema. È possibile utilizzare il plug-in IntelliJ Kotlin per compilarlo in bytecode -> e decompilarlo in Java. Per usarlo, inseriscilo nel pacchetto android.support.v7.widget e impostalo come codice CoordinatorLayout del comportamento AppBarLayout nel codice (o aggiungi il costruttore xml applicabile o qualcosa del genere)

/*
 * 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
    }

}

Come impostarlo?
ARR.s

0

questa è la mia soluzione nel mio progetto.
basta arrestare mScroller quando si ottiene 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

0

per androidx,

Se il tuo file manifest ha una linea android: hardwareAccelerated = "false", eliminalo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.