Lanzar con RecyclerView + AppBarLayout

171

Estoy usando el nuevo CoordinatorLayout con AppBarLayout y CollapsingToolbarLayout. Debajo de AppBarLayout, tengo un RecyclerView con una lista de contenido.

He verificado que el desplazamiento de volteo funciona en RecyclerView cuando me desplazo hacia arriba y hacia abajo en la lista. Sin embargo, también me gustaría que AppBarLayout se desplace suavemente durante la expansión.

Al desplazarse hacia arriba para expandir CollaspingToolbarLayout, el desplazamiento se detiene inmediatamente una vez que levanta el dedo de la pantalla. Si se desplaza hacia arriba en un movimiento rápido, a veces el CollapsingToolbarLayout vuelve a contraerse también. Este comportamiento con RecyclerView parece funcionar de manera muy diferente que cuando se usa un NestedScrollView.

Intenté establecer diferentes propiedades de desplazamiento en la vista del reciclador, pero no he podido resolver esto.

Aquí hay un video que muestra algunos de los problemas de desplazamiento. https://youtu.be/xMLKoJOsTAM

Aquí hay un ejemplo que muestra el problema con RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

Aquí está el ejemplo original que usa un NestedScrollView de Chris Banes. https://github.com/chrisbanes/cheesesquare

tylerjroach
fuente
Estoy experimentando este mismo problema exacto (lo estoy usando con un RecyclerView). Si miras una lista de la tienda de Google Play para cualquier aplicación, parece que se comporta correctamente, por lo que definitivamente hay una solución por ahí ...
Aneem
Hola Aneem, sé que esta no es la mejor solución, pero comencé a experimentar con esta biblioteca: github.com/ksoichiro/Android-ObservableScrollView . Especialmente en esta actividad para lograr los resultados que necesitaba: FlexibleSpaceWithImageRecyclerViewActivity.java. Perdón por escribir mal su nombre antes de la edición. Autocorrección ..
tylerjroach
2
El mismo problema aquí, terminé evitando AppBarLayout.
Renaud Cerrato
Sí. Terminé obteniendo exactamente lo que necesitaba de la biblioteca OvservableScrollView. Estoy seguro de que se solucionará en futuras versiones.
tylerjroach
8
El lanzamiento es defectuoso, se ha planteado un problema (y aceptado).
Renaud Cerrato

Respuestas:

114

La respuesta de Kirill Boyarshinov fue casi correcta.

El problema principal es que RecyclerView a veces da una dirección de lanzamiento incorrecta, por lo que si agrega el siguiente código a su respuesta, funciona correctamente:

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

Espero que esto ayude.

Manolo Garcia
fuente
¡Salvaste mi día! Parece estar funcionando absolutamente bien! ¿Por qué no se acepta su respuesta?
Zordid
9
si está utilizando un SwipeRefreshLayout como padre de su vista de reciclador, simplemente agregue este código: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }antes if (target instanceof RecyclerView && velocityY < 0) {
LucasFM
1
+ 1 Analizando esta solución, no entiendo por qué Google aún no ha solucionado esto. El código parece ser bastante simple.
Gastón Flores
3
Hola, cómo lograr lo mismo con appbarlayout y Nestedscrollview ... Gracias de antemano ...
Harry Sharma
1
No funcionó para mí = / Por cierto, no necesita mover la clase al paquete de soporte para lograrlo, puede registrar un DragCallback en el constructor.
Augusto Carmo
69

Parece que v23 actualización aún no lo solucionó.

He encontrado una especie de truco para arreglarlo con el lanzamiento. El truco consiste en reanudar el evento de lanzamiento si el hijo superior de ScrollingView está cerca del comienzo de los datos en el Adaptador.

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

Úselo en su diseño así:

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

EDITAR: La reanudación de eventos de lanzamiento ahora se basa en verticalScrollOffsetlugar de la cantidad de elementos desde la parte superior deRecyclerView .

EDIT2: Verifique el destino como ScrollingViewinstancia de interfaz en lugar de RecyclerView. Ambos RecyclerViewe NestedScrollingViewimplementarlo.

Kirill Boyarshinov
fuente
Obtener tipos de cadena no está permitido por error de
layout_behavior
Lo probé y funciona mejor hombre! pero, ¿cuál es el propósito de TOP_CHILD_FLING_THRESHOLD? y por que son 3?
Julio_oa
@Julio_oa TOP_CHILD_FLING_THRESHOLD significa que el evento de lanzamiento se reconsideraría si la vista del reciclador se desplaza al elemento cuya posición está por debajo de este valor umbral. Por cierto, actualicé la respuesta para usar, verticalScrollOffsetque es más general. Ahora el evento de lanzamiento se reanudará cuando recyclerViewse desplace hacia arriba.
Kirill Boyarshinov
Hola, cómo lograr lo mismo con appbarlayout y Nestedscrollview ... Gracias de antemano ...
Harry Sharma
2
@Hardeep change target instanceof RecyclerViewto target instanceof NestedScrollView, or more for genérico case to target instanceof ScrollingView. Actualicé la respuesta.
Kirill Boyarshinov
15

He encontrado la solución aplicando OnScrollingListener al recyclerView. ahora funciona muy bien El problema es que la vista de reciclaje proporcionó el valor de consumo incorrecto y el comportamiento no sabe cuándo la vista de reciclaje se desplaza hacia arriba.

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
fuente
Gracias por tu publicación He probado todas las respuestas en esta página y, en mi experiencia, esta es la respuesta más efectiva. Pero, RecylerView en mi diseño se desplaza internamente antes de que AppBarLayout se haya desplazado fuera de la pantalla si no desplazo RecyclerView con suficiente fuerza. En otras palabras, cuando desplazo RecyclerView con suficiente fuerza, la barra de aplicaciones se desplaza fuera de la pantalla sin que RecyclerView se desplace internamente, pero cuando no desplazo RecyclerView con suficiente fuerza, RecyclerView se desplaza internamente antes de que AppbarLayout se haya desplazado fuera de la pantalla. ¿Sabes qué está causando eso?
Micah Simmons
La vista del reciclador todavía recibe eventos táctiles, por eso todavía se desplaza, el comportamiento en AntedFling se animaría a desplazar el diseño de la barra de aplicaciones al mismo tiempo. Tal vez pueda intentar anular onInterceptTouch en el comportamiento para cambiar esto. Para mí, el comportamiento actual es aceptable por lo que veo. (no estoy seguro si estamos viendo lo mismo)
Mak Sing
@MakSing es realmente útil con la configuración CoordinatorLayouty ViewPagermuchas gracias por esta solución tan esperada. Escriba un GIST para el mismo para que otros desarrolladores también puedan beneficiarse de él. Estoy compartiendo esta solución también. Gracias de nuevo.
Nitin Misra
1
@MakSing Fuera de todas las soluciones, esto funciona mejor para mí. Ajusté la velocidad entregada al onNestedFling un poco de velocidad * 0.6f ... parece darle un mejor flujo.
Sablerider
Funciona para mi. @MakSing ¿En el método onScrolled debe invocar onNestedFling de AppBarLayout.Behavior y no de RecyclerViewAppBarBehavior? Me parece un poco extraño.
Anton Malmygin
13

Se ha solucionado desde el diseño de soporte 26.0.0.

compile 'com.android.support:design:26.0.0'
Xiaozou
fuente
2
Esto necesita moverse hacia arriba. Esto se describe aquí en caso de que alguien esté interesado en los detalles.
Chris Dinon
1
Ahora parece haber un problema con la barra de estado, donde cuando te desplazas hacia abajo la barra de estado baja un poco con el desplazamiento ... ¡es muy molesto!
caja del
2
@Xiaozou Estoy usando 26.1.0 y todavía tengo problemas con el lanzamiento. El lanzamiento rápido a veces produce un movimiento opuesto (la velocidad del movimiento es opuesta / incorrecta como se puede ver en el método onNestedFling). Reproducido en Xiaomi Redmi Note 3 y Galaxy S3
dor506
@ dor506 stackoverflow.com/a/47298312/782870 No estoy seguro de si tenemos el mismo problema cuando dices resultado de movimiento opuesto. Pero publiqué una respuesta aquí. Espero que ayude :)
vida
5

Esta es una versión fluida de Google Support Design AppBarLayout. Si está utilizando AppBarLayout, sabrá que tiene un problema con fling.

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

Ver Biblioteca aquí .. https://github.com/henrytao-me/smooth-app-bar-layout

Mansukh Ahir
fuente
4

Es un error de la vista de reciclaje. Se supone que está arreglado en v23.1.0.

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

ingrese la descripción de la imagen aquí

dupengtao
fuente
9
v23.4.0 - Todavía no solucionado
Arthur
3
Todavía no se corrigió en v25.1.0
0xcaff
v25.3.1, todavía se ve mal.
Bawa
1
¡Finalmente está arreglado v26.0.1!
Micer
2

Este es mi diseño y el desplazamiento está funcionando como debería.

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

Mi solución hasta ahora, basada en las respuestas de Mak Sing y Manolo García .

No es totalmente perfecto. Por ahora no sé cómo volver a calcular una velocidad de valide para evitar un efecto extraño: la barra de aplicaciones puede expandirse más rápido que la velocidad de desplazamiento. Pero no se puede alcanzar el estado con una barra de aplicaciones expandida y una vista de reciclador desplazada.

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
fuente
Puede obtener la velocidad actual de un recyclerView (a partir del 25.1.0) utilizando la reflexión: 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

En mi caso, estaba teniendo el problema de que el lanzamiento RecyclerViewno lo desplazaría suavemente, haciendo que se atascara.

Esto fue porque, por alguna razón, había olvidado que había puesto mi RecyclerViewen unNestedScrollView .

Es un error tonto, pero me tomó un tiempo resolverlo ...

Farbod Salamat-Zadeh
fuente
1

Agrego una vista de 1dp de altura dentro de AppBarLayout y luego funciona mucho mejor. Este es mi diseño.

  <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
fuente
Funciona solo si te desplazas hacia arriba. Sin embargo
Arthur
Para mí funciona bien en ambas direcciones. ¿Agregó la vista 1dp dentro de la barra de aplicaciones? Solo lo probé en Android lollipop y kitkat.
Jachumbelechao Unto Mantekilla
Bueno, también estoy usando CollapsingToolbarLayout que envuelve la barra de herramientas. Puse la vista 1dp dentro de eso. Es algo así como AppBarLayout -> CollapsingToolbarLayout -> Toolbar + 1dp view
Arthur
No sé si funciona bien con CollapsingToolbarLayout. Solo probé con este código. ¿Intentaste poner la vista de 1dp fuera de CollapsingToolbarLayout?
Jachumbelechao Unto Mantekilla
Si. Desplazarse hacia arriba funciona, desplazarse hacia abajo no expande la barra de herramientas.
Arthur
1

Ya hay algunas soluciones bastante populares aquí, pero después de jugar con ellas, encontré una solución bastante simple que funcionó bien para mí. Mi solución también garantiza que AppBarLayoutsolo se expanda cuando el contenido desplazable llega a la cima, una ventaja sobre otras soluciones aquí.

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
fuente
¿Qué es mPrevDy
ARR.s
1

La respuesta aceptada no funcionó para mí porque tenía RecyclerViewdentro de a SwipeRefreshLayouty a ViewPager. Esta es la versión mejorada que busca un RecyclerViewen la jerarquía y debería funcionar para cualquier diseño:

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

Respuesta: se solucionó en la biblioteca de soporte v26

pero v26 tiene algún problema al lanzar. A veces, AppBar se recupera de nuevo incluso si el lanzamiento no es demasiado difícil.

¿Cómo elimino el efecto de rebote en la barra de aplicaciones?

Si encuentra el mismo problema al actualizar para admitir v26, aquí está el resumen de esta respuesta .

Solución : Extienda el Comportamiento predeterminado de AppBar y bloquee la llamada a AppBar.Behavior's onNestedPreScroll () y onNestedScroll () cuando se toca AppBar mientras NestedScroll aún no se ha detenido.

vida
fuente
0

Julian Os tiene razón.

La respuesta de Manolo García no funciona si la vista del reciclador está por debajo del umbral y se desplaza. Debe comparar la vista offsetdel reciclador y la velocity to the distanceposición del artículo, no la del artículo.

Hice la versión de Java refiriéndome al código de Kotlin de Julian y restando la reflexión.

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

}
정성민
fuente
no se puede volver a reservarBaseApplication
ARR.s
@ ARR.s lo siento, solo reemplaza tu contexto como a continuación
정성민
YOUR_CONTEXT.getResources (). GetDisplayMetrics (). Density * 160.0f;
정성민
0

Agregar otra respuesta aquí ya que las anteriores no satisfacían completamente mis necesidades o no funcionaban muy bien. Este se basa parcialmente en ideas difundidas aquí.

Entonces, ¿qué hace este?

Escenario hacia abajo: si AppBarLayout está colapsado, permite que RecyclerView se ejecute por sí solo sin hacer nada. De lo contrario, colapsa AppBarLayout y evita que RecyclerView realice su lanzamiento. Tan pronto como se colapsa (hasta el punto que demanda la velocidad dada) y si queda velocidad, el RecyclerView se lanza con la velocidad original menos lo que AppBarLayout acaba de consumir colapso.

Lanzamiento ascendente del escenario: si el desplazamiento de desplazamiento del RecyclerView no es cero, se lanza con la velocidad original. Tan pronto como termine eso y si aún queda velocidad (es decir, el RecyclerView se desplaza a la posición 0), el AppBarLayout se expande hasta el punto que la velocidad original menos las demandas recién consumidas. De lo contrario, AppBarLayout se expande hasta el punto que exige la velocidad original.

AFAIK, este es el comportamiento indended.

Hay mucha reflexión involucrada, y es bastante personalizada. No se encontraron problemas todavía. También está escrito en Kotlin, pero entenderlo no debería ser un problema. Puede usar el complemento IntelliJ Kotlin para compilarlo en bytecode -> y descompilarlo de nuevo en Java. Para usarlo, colóquelo en el paquete android.support.v7.widget y configúrelo como el comportamiento de CoordinadorLayout.LayoutParams de AppBarLayout en el código (o agregue el constructor xml aplicable o algo así)

/*
 * 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
fuente
¿Cómo configurarlo?
ARR.s
0

Esta es mi solución en mi proyecto.
simplemente detenga el mScroller cuando obtenga 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
fuente
0

para androidx,

Si su archivo de manifiesto tiene una línea android: hardwareAccelerated = "false", elimínela.

Jetwiz
fuente