Flingowanie z RecyclerView + AppBarLayout

171

Używam nowego CoordinatorLayout z AppBarLayout i CollapsingToolbarLayout. Poniżej AppBarLayout mam RecyclerView z listą zawartości.

Sprawdziłem, że przewijanie fling działa w RecyclerView, gdy przewijam listę w górę iw dół. Chciałbym jednak również, aby AppBarLayout płynnie przewijał się podczas rozwijania.

Podczas przewijania w górę w celu rozwinięcia CollaspingToolbarLayout przewijanie zatrzymuje się natychmiast po podniesieniu palca z ekranu. Jeśli przewiniesz w górę w szybkim ruchu, czasami CollapsingToolbarLayout również zwija się ponownie. Wydaje się, że to zachowanie z RecyclerView działa znacznie inaczej niż w przypadku korzystania z NestedScrollView.

Próbowałem ustawić różne właściwości przewijania w widoku recyklingu, ale nie byłem w stanie tego rozgryźć.

Oto film przedstawiający niektóre problemy z przewijaniem. https://youtu.be/xMLKoJOsTAM

Oto przykład przedstawiający problem z RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

Oto oryginalny przykład wykorzystujący NestedScrollView od Chrisa Banesa. https://github.com/chrisbanes/cheesesquare

tylerjroach
źródło
Mam dokładnie ten sam problem (używam z RecyclerView). Jeśli spojrzysz na listę sklepów Google Play dla dowolnej aplikacji, wydaje się, że zachowuje się ona poprawnie, więc na pewno jest tam rozwiązanie ...
Aneem
Hej Aneem, wiem, że to nie jest najlepsze rozwiązanie, ale zacząłem eksperymentować z tą biblioteką: github.com/ksoichiro/Android-ObservableScrollView . Szczególnie w tym działaniu, aby osiągnąć wyniki, których potrzebowałem: FlexibleSpaceWithImageRecyclerViewActivity.java. Przepraszamy za błędną pisownię twojego nazwiska przed edycją. Autokorekta ..
tylerjroach
2
Ten sam problem, ostatecznie uniknąłem AppBarLayout.
Renaud Cerrato
Tak. Skończyło się na tym, że otrzymałem dokładnie to, czego potrzebowałem, z biblioteki OvservableScrollView. Jestem pewien, że zostanie to naprawione w przyszłych wersjach.
tylerjroach
8
Plik zawiera błędy, problem został zgłoszony (i zaakceptowany).
Renaud Cerrato

Odpowiedzi:

114

Odpowiedź Kirilla Boyarshinova była prawie poprawna.

Głównym problemem jest to, że RecyclerView czasami podaje nieprawidłowy kierunek rzucania, więc jeśli dodasz następujący kod do jego odpowiedzi, działa on poprawnie:

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

Mam nadzieję, że to pomoże.

Manolo Garcia
źródło
Uratowałeś mi dzień! Wygląda na to, że działa absolutnie dobrze! Dlaczego twoja odpowiedź nie została zaakceptowana?
Zordid,
9
jeśli używasz SwipeRefreshLayout jako elementu nadrzędnego swojego widoku recyklingu, po prostu dodaj ten kod: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }przed if (target instanceof RecyclerView && velocityY < 0) {
LucasFM
1
+ 1 Analizując tę ​​poprawkę, nie rozumiem. Dlaczego Google jeszcze tego nie naprawił. Kod wydaje się dość prosty.
Gaston Flores
3
Witam, jak osiągnąć to samo z appbarlayout i Nestedscrollview ... Z góry dziękuję ..
Harry Sharma
1
U mnie to nie zadziałało = / Nawiasem mówiąc, nie musisz przenosić klasy do pakietu wsparcia, aby to osiągnąć, możesz zarejestrować DragCallback w konstruktorze.
Augusto Carmo
69

Wygląda na to, że v23aktualizacja jeszcze tego nie naprawiła.

Znalazłem coś w rodzaju hacka, aby to naprawić, rzucając w dół. Sztuczka polega na tym, aby ponownie wykorzystać zdarzenie rzucające, jeśli górny element podrzędny ScrollingView znajduje się blisko początku danych w adapterze.

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

Użyj go w swoim układzie w ten sposób:

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

EDYCJA: Ponowne wykorzystanie wydarzeń Rzut jest teraz oparte na verticalScrollOffsetliczbie przedmiotów, które znajdują się na wierzchu RecyclerView.

EDIT2: Sprawdź cel jako ScrollingViewinstancję interfejsu zamiast RecyclerView. Obie RecyclerViewi NestedScrollingViewwdrożyć to.

Kirill Boyarshinov
źródło
Pobieranie typów łańcuchów nie jest dozwolone w przypadku błędu
layout_behavior
Przetestowałem to i działa lepiej człowieku! ale jaki jest cel TOP_CHILD_FLING_THRESHOLD? i dlaczego jest 3?
Julio_oa
@Julio_oa TOP_CHILD_FLING_THRESHOLD oznacza, że ​​zdarzenie fling zostanie ponownie wykorzystane, jeśli widok recyklera zostanie przewinięty do elementu, którego pozycja jest poniżej tej wartości progowej. Btw zaktualizowałem odpowiedź, verticalScrollOffsetktóra jest bardziej ogólna. Teraz rzucane wydarzenie zostanie ponownie wykorzystane po recyclerViewprzewinięciu do góry.
Kirill Boyarshinov
Witam, jak osiągnąć to samo z appbarlayout i Nestedscrollview ... Z góry dziękuję ..
Harry Sharma
2
@Trudna zmiana target instanceof RecyclerViewna target instanceof NestedScrollViewlub więcej dla ogólnej wielkości liter na target instanceof ScrollingView. Zaktualizowałem odpowiedź.
Kirill Boyarshinov
15

Znalazłem poprawkę, stosując OnScrollingListener do pliku recyclinglerView. teraz działa bardzo dobrze. Problem polega na tym, że recykling podał nieprawidłową zużytą wartość, a zachowanie nie wie, kiedy widok recyklingu jest przewijany do góry.

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
źródło
Dzięki za twój post. Wypróbowałem wszystkie odpowiedzi na tej stronie i z mojego doświadczenia wynika, że ​​jest to najskuteczniejsza odpowiedź. Ale RecylerView w moim układzie przewija się wewnętrznie, zanim AppBarLayout przewinie się poza ekran, jeśli nie przewiniesz RecyclerView z wystarczającą siłą. Innymi słowy, gdy przewijam RecyclerView z wystarczającą siłą, AppBar przewija ekran bez wewnętrznego przewijania RecyclerView, ale gdy nie przewijam RecyclerView z wystarczającą siłą, RecyclerView przewija się wewnętrznie, zanim AppbarLayout przewinie się poza ekran. Czy wiesz, co jest tego przyczyną?
Micah Simmons
Recyclerview nadal odbiera zdarzenia dotykowe, dlatego nadal przewija się, zachowanie naNestedFling będzie animowane, aby przewijać appbarLayout w tym samym czasie. Może możesz spróbować zastąpić onInterceptTouch w zachowaniu, aby to zmienić. Dla mnie obecne zachowanie jest akceptowalne z tego, co widzę. (nie jestem pewien, czy widzimy to samo)
Mak Sing
@MakSing jest naprawdę pomocny CoordinatorLayouti ViewPagerkonfiguracja, dziękuję bardzo za to najbardziej oczekiwane rozwiązanie. Proszę napisać GIST dla tego samego, aby inni deweloperzy również mogli z niego skorzystać. Udostępniam również to rozwiązanie. Dzięki jeszcze raz.
Nitin Misra
1
@MakSing Off wszystkie rozwiązania, to działa najlepiej dla mnie. Dostosowałem prędkość przekazaną do onNestedFling trochę velocity * 0.6f ... wydaje się, że daje to ładniejszy przepływ.
saberrider
Pracuje dla mnie. @MakSing Czy w metodzie onScrolled musisz wywołać onNestedFling z AppBarLayout.Behavior, a nie z RecyclerViewAppBarBehavior? Wydaje mi się to trochę dziwne.
Anton Malmygin
13

Zostało to naprawione od czasu wsparcia projektu 26.0.0.

compile 'com.android.support:design:26.0.0'
Xiaozou
źródło
2
To musi wzrosnąć. Jest to opisane tutaj, na wypadek gdyby ktoś był zainteresowany szczegółami.
Chris Dinon
1
Teraz wydaje się, że występuje problem z paskiem stanu, w którym podczas przewijania w dół pasek stanu obniża się nieco wraz z przewijaniem ... to bardzo irytujące!
ramka
2
@Xiaozou Używam 26.1.0 i nadal mam problemy z wyrzucaniem. Szybki rzut czasami powoduje przeciwny ruch (prędkość ruchu jest przeciwna / zła, jak widać w metodzie onNestedFling). Powielono go w Xiaomi Redmi Note 3 i Galaxy S3
dor506
@ dor506 stackoverflow.com/a/47298312/782870 Nie jestem pewien, czy mamy ten sam problem, gdy mówisz o przeciwnym wyniku ruchu. Ale zamieściłem odpowiedź tutaj. Mam nadzieję, że to pomoże :)
vida
4

To błąd dotyczący recyklingu. Ma to zostać naprawione w wersji 23.1.0.

spójrz na https://code.google.com/p/android/issues/detail?id=177729

wprowadź opis obrazu tutaj

dupengtao
źródło
9
Wersja 23.4.0 - Nadal nie naprawiono
Arthur
3
Nadal nie naprawiono w wersji 25.1.0
0xcaff
Wersja 25.3.1, nadal wygląda źle.
Bawa
1
W końcu zostało naprawione v26.0.1!
Micer
2

To jest mój układ i zwój. Działa tak, jak powinien.

<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
źródło
2

Moje dotychczasowe rozwiązanie na podstawie odpowiedzi Mak Sing i Manolo Garcia .

To nie jest do końca idealne. Na razie nie wiem, jak przeliczyć prędkość walidową, aby uniknąć dziwnego efektu: pasek aplikacji może rozszerzać się szybciej niż prędkość przewijania. Ale stan z rozszerzonym paskiem aplikacji i przewijanym widokiem recyklera nie jest dostępny.

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
źródło
Możesz uzyskać aktualną prędkość recyklera (od 25.1.0), korzystając z odbicia: 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

W moim przypadku pojawił się problem polegający na rzucaniu pliku RecyclerView że nie przewijało go płynnie, co powodowało, że utknęło.

To dlatego, że z jakiegoś powodu zapomniałem, że włożyłem RecyclerViewplikNestedScrollView .

To głupi błąd, ale zajęło mi trochę czasu, zanim to rozgryzłem ...

Farbod Salamat-Zadeh
źródło
1

Dodaję widok wysokości 1 dp wewnątrz AppBarLayout i wtedy działa znacznie lepiej. To jest mój układ.

  <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
źródło
Działa tylko wtedy, gdy przewiniesz w górę. Ale nie kiedy przewijasz w dół
Arthur
U mnie działa dobrze w obu kierunkach. Czy dodałeś widok 1dp wewnątrz appbarlayout? Przetestowałem to tylko w Android Lollipop i KitKat.
Jachumbelechao Unto Mantekilla
Cóż, używam również CollapsingToolbarLayout, który otacza pasek narzędzi. Umieściłem w nim widok 1dp. To trochę tak, jak ten AppBarLayout -> CollapsingToolbarLayout -> Pasek narzędzi + widok 1 dp
Arthur
Nie wiem, czy działa dobrze z CollapsingToolbarLayout. Testowałem tylko z tym kodem. Czy próbowałeś umieścić widok 1dp poza CollapsingToolbarLayout?
Jachumbelechao Unto Mantekilla
Tak. Przewijanie w górę działa, przewijanie w dół nie powoduje rozwinięcia paska narzędzi.
Arthur
1

Już tutaj kilka dość popularnych rozwiązań, ale po zabawie z nimi wymyśliłem raczej prostsze rozwiązanie, które działało dobrze. Moje rozwiązanie zapewnia również, że AppBarLayoutjest on rozszerzany tylko wtedy, gdy przewijalna zawartość osiąga szczyt, co stanowi przewagę nad innymi rozwiązaniami tutaj.

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
źródło
co to jest mPrevDy
ARR.s
1

Przyjęta odpowiedź nie działała dla mnie, ponieważ miałem RecyclerViewwewnątrz a SwipeRefreshLayouti ViewPager. To jest ulepszona wersja, która szuka RecyclerVieww hierarchii i powinna działać dla każdego układu:

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
źródło
1

Odpowiedź: Naprawiono to w bibliotece wsparcia v26

ale v26 ma jakiś problem z rzucaniem. Czasami AppBar odbija się ponownie, nawet jeśli rzucanie nie jest zbyt trudne.

Jak usunąć efekt odbijania na pasku aplikacji?

Jeśli napotkasz ten sam problem podczas aktualizacji do obsługi wersji 26, oto podsumowanie tej odpowiedzi .

Rozwiązanie : Rozszerz domyślne zachowanie AppBar i zablokuj wywołanie funkcji AppBar.Behavior onNestedPreScroll () i onNestedScroll (), gdy zostanie dotknięty AppBar, gdy NestedScroll nie został jeszcze zatrzymany.

vida
źródło
0

Julian Os ma rację.

Odpowiedź Manolo Garcia nie działa, jeśli widok recyklingu jest poniżej progu i przewija się. Musisz porównać widok offsetrecyklingu i velocity to the distance, a nie pozycję przedmiotu.

Stworzyłem wersję java, odwołując się do kodu kotlin Juliana i odejmując odbicie.

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

}
정성민
źródło
nie można resloveBaseApplication
ARR.s
@ ARR.s przepraszamy, po prostu zastąp go swoim kontekstem, jak poniżej.
정성민
YOUR_CONTEXT.getResources (). GetDisplayMetrics (). Density * 160.0f;
정성민
0

W odniesieniu do narzędzia do śledzenia problemów Google błędów , został on naprawiony w bibliotece wsparcia w wersji 26.0.0-beta2 dla systemu Android

Zaktualizuj bibliotekę obsługi Androida do wersji 26.0.0-beta2.

Jeśli jakikolwiek problem będzie się powtarzał, zgłoś go do narzędzia Google do śledzenia problemów , które ponownie otworzą w celu zbadania.

Prags
źródło
0

Dodanie tutaj kolejnej odpowiedzi, ponieważ powyższe albo nie spełniły całkowicie moich potrzeb, albo nie działały zbyt dobrze. Ten jest częściowo oparty na pomysłach, które tu rozprzestrzeniają.

Więc co to robi?

Scenariusz w dół: jeśli AppBarLayout jest zwinięty, pozwala RecyclerView na samodzielne uruchamianie bez wykonywania jakichkolwiek czynności. W przeciwnym razie zwija AppBarLayout i uniemożliwia RecyclerView wykonanie jego zrzutu. Gdy tylko zostanie zwinięty (do punktu, którego wymaga dana prędkość) i jeśli pozostanie prędkość, RecyclerView zostanie wyrzucony z pierwotną prędkością minus to, co AppBarLayout właśnie zużyło podczas zwijania.

Scenariusz w górę: Jeśli przesunięcie przewijania RecyclerView jest różne od zera, zostanie wyrzucony z oryginalną prędkością. Jak tylko to się zakończy i jeśli nadal pozostanie prędkość (tj. RecyclerView przewinie się do pozycji 0), AppBarLayout zostanie rozszerzony do punktu, w którym pierwotna prędkość pomniejszona o właśnie zużyte wymagania. W przeciwnym razie AppBarLayout zostanie rozszerzony do punktu wymaganego przez pierwotną prędkość.

AFAIK, to jest wcięcie.

Jest w tym dużo refleksji i jest to dość zwyczajowe. Nie znaleziono jeszcze żadnych problemów. Jest również napisany w języku Kotlin, ale zrozumienie tego nie powinno stanowić problemu. Możesz użyć wtyczki IntelliJ Kotlin, aby skompilować go do kodu bajtowego -> i zdekompilować z powrotem do Javy. Aby go użyć, umieść go w pakiecie android.support.v7.widget i ustaw jako zachowanie CoordinatorLayout.LayoutParams w AppBarLayout w kodzie (lub dodaj odpowiedni konstruktor xml lub coś w tym stylu)

/*
 * 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
źródło
Jak to ustawić?
ARR.s
0

to jest moje rozwiązanie w moim projekcie.
po prostu zatrzymaj mScroller po otrzymaniu 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
źródło
0

dla androidx,

Jeśli plik manifestu zawiera wiersz android: hardwareAccelerated = "false", usuń go.

Jetwiz
źródło