Arremessando com RecyclerView + AppBarLayout


171

Estou usando o novo CoordinatorLayout com AppBarLayout e CollapsingToolbarLayout. Abaixo do AppBarLayout, tenho um RecyclerView com uma lista de conteúdo.

Eu verifiquei que a rolagem de arremesso funciona no RecyclerView quando estou rolando a lista para cima e para baixo. No entanto, eu também gostaria que o AppBarLayout rolasse suavemente durante a expansão.

Ao rolar para cima para expandir o CollaspingToolbarLayout, a rolagem para imediatamente após levantar o dedo da tela. Se você rolar para cima em um movimento rápido, às vezes o CollapsingToolbarLayout também recolhe. Esse comportamento com o RecyclerView parece funcionar de maneira muito diferente do que ao usar um NestedScrollView.

Tentei definir diferentes propriedades de rolagem na recyclerview, mas não consegui descobrir isso.

Aqui está um vídeo mostrando alguns dos problemas de rolagem. https://youtu.be/xMLKoJOsTAM

Aqui está um exemplo mostrando o problema com o RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

Aqui está o exemplo original que usa um NestedScrollView de Chris Banes. https://github.com/chrisbanes/cheesesquare


Estou enfrentando esse mesmo problema exato (estou usando com um RecyclerView). Se você olhar para uma loja Google Play lista para qualquer aplicativo, ele parece se comportar corretamente, então há definitivamente uma solução lá fora ...
Aneem

Olá Aneem, sei que essa não é a melhor solução, mas comecei a experimentar esta biblioteca: github.com/ksoichiro/Android-ObservableScrollView . Especialmente nesta atividade para alcançar os resultados que eu precisava: FlexibleSpaceWithImageRecyclerViewActivity.java. Desculpe por digitar seu nome incorretamente antes da edição. Autocorreção ..
tylerjroach

2
Mesmo problema aqui, acabei evitando o AppBarLayout.
Renaud Cerrato

Sim. Acabei obtendo exatamente o que precisava da biblioteca OvservableScrollView. Tenho certeza de que será corrigido em versões futuras.
tylerjroach

8
O arremesso é de buggy, um problema foi levantado (e aceito).
Renaud Cerrato

Respostas:


114

A resposta de Kirill Boyarshinov estava quase correta.

O principal problema é que o RecyclerView às vezes está dando uma direção incorreta, então se você adicionar o seguinte código à resposta dele, ele funcionará corretamente:

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

Eu espero que isso ajude.


Você salvou o meu dia! Parece estar funcionando absolutamente bem! Por que sua resposta não é aceita?
Zordid 12/09/15

9
se você estiver usando um SwipeRefreshLayout como pai da sua recyclerview, basta adicionar este código: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }antes if (target instanceof RecyclerView && velocityY < 0) {
LucasFM 16/16/16

1
+ 1 Analisando essa correção, não entendo Por que o Google ainda não corrigiu isso. O código parece ser bastante simples.
Gaston Flores

3
Olá como conseguir a mesma coisa com appbarlayout e Nestedscrollview ... Obrigado antecipadamente ..
Harry Sharma

1
Ele não funcionou para mim = / A propósito, você não precisa mover a classe para o pacote de suporte para alcançá-lo; é possível registrar um DragCallback no construtor.
Augusto Carmo

69

Parece que a v23atualização ainda não foi corrigida.

Eu encontrei uma espécie de hack para corrigi-lo com arremessar para baixo. O truque é reconsiderar o evento fling se o filho superior do ScrollingView estiver próximo do início dos dados no 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);
    }
}

Use-o no seu layout assim:

 <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: o reconsumo de evento de lançamento agora é baseado em verticalScrollOffsetvez da quantidade de itens da parte superior RecyclerView.

EDIT2: Verifique o destino como ScrollingViewinstância da interface em vez de RecyclerView. AmbosRecyclerView e NestedScrollingViewimplementá-lo.


Obtendo tipos string não são permitidos para erro layout_behavior
Vaisakh N

Eu testei e funciona melhor cara! mas qual é o objetivo do TOP_CHILD_FLING_THRESHOLD? e porque é 3?
Julio_oa

@Julio_oa TOP_CHILD_FLING_THRESHOLD significa que o evento de arremesso seria reconsumido se a exibição do reciclador fosse rolada para o elemento cuja posição está abaixo desse valor limite. Btw eu atualizei a resposta para usar verticalScrollOffsetque é mais geral. Agora, o evento fling será reconsumido quando recyclerViewrolado para cima.
Kirill Boyarshinov 19/02/16

Olá como conseguir a mesma coisa com appbarlayout e Nestedscrollview ... Obrigado antecipadamente ..
Harry Sharma

2
@Altere a mudança target instanceof RecyclerViewpara target instanceof NestedScrollView, ou mais, para o caso genérico para target instanceof ScrollingView. Eu atualizei a resposta.
Kirill Boyarshinov 08/07/16

15

Encontrei a correção aplicando OnScrollingListener ao recyclerView. agora funciona muito bem. O problema é que a reciclagem forneceu o valor consumido errado e o comportamento não sabe quando a reciclagem é rolada para o topo.

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

Obrigado pela sua postagem. Eu tentei todas as respostas nesta página e, na minha experiência, essa é a resposta mais eficaz. Porém, o RecylerView no meu layout rola internamente antes que o AppBarLayout saia da tela se eu não rolar o RecyclerView com força suficiente. Em outras palavras, quando eu rolar o RecyclerView com força suficiente, o AppBar sai da tela sem o RecyclerView rolar internamente, mas quando eu não rolar o RecyclerView com força suficiente, o RecyclerView rola internamente antes que o AppbarLayout saia da tela. Você sabe o que está causando isso?
Micah Simmons

A visualização de reciclagem ainda recebe eventos de toque e é por isso que ainda rola, o comportamento em NestedFling seria animado para rolar o appbarLayout ao mesmo tempo. Talvez você possa tentar substituir onInterceptTouch no comportamento para alterar isso. Para mim, o comportamento atual é aceitável pelo que vejo. (não sei se estamos vendo a mesma coisa)
Mak Cante

O @MakSing é realmente útil CoordinatorLayoute ViewPagermuito obrigado pela solução mais esperada. Escreva um GIST para o mesmo, para que outros desenvolvedores também possam se beneficiar. Também estou compartilhando esta solução. Obrigado novamente.
Nitin Misra

1
@MakSing Fora de todas as soluções, isso funciona melhor para mim. Eu ajustei a velocidade entregue ao onNestedFling um pouco a velocidade * 0,6f ... parece fornecer um fluxo melhor para ele.
Sabreider 24/11

Funciona para mim. @MakSing No método onScrolled, você deve chamar onNestedFling de AppBarLayout.Behavior e não de RecyclerViewAppBarBehavior? Parece um pouco estranho para mim.
Anton Malmygin

13

Foi corrigido desde o design de suporte 26.0.0.

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

2
Isso precisa subir. Isso é descrito aqui , caso alguém esteja interessado nos detalhes.
Chris Dinon 01/08/19

1
Agora parece haver um problema com a barra de status, onde, quando você rola a barra de status, diminui um pouco com a rolagem ... é super irritante!
caixa

2
@Xiaozou Estou usando o 26.1.0 e ainda tenho problemas com arremessar. O arremesso rápido às vezes resulta em movimento oposto (a velocidade do movimento é oposta / incorreta, como pode ser visto no método onNestedFling). Reproduzi-o no Xiaomi Redmi Note 3 e Galaxy S3
dor506 20/10

@ dor506 stackoverflow.com/a/47298312/782870 Não tenho certeza se temos o mesmo problema quando você diz o resultado oposto do movimento. Mas eu postei uma resposta aqui. Espero que ajude :)
vida



2

Este é o meu Layout e o pergaminho. Está funcionando como deveria.

<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

Minha solução até agora, com base em Mak Sing e Manolo Garcia respostas de .

Não é totalmente perfeito. Por enquanto, não sei como recalcular uma velocidade de valide para evitar um efeito estranho: a barra de aplicativos pode se expandir mais rapidamente que a velocidade de rolagem. Mas o estado com uma barra de aplicativos expandida e uma exibição de reciclador rolada não pode ser alcançado.

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

    }

}

Você pode obter a velocidade atual de um recyclerView (a partir de 25.1.0) usando reflexão: 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

No meu caso, eu estava entendendo o problema de arremessar o RecyclerView não rolaria suavemente, fazendo com que ficasse preso.

Isso porque, por algum motivo, eu tinha esquecido que eu tinha colocado o meu RecyclerViewem umNestedScrollView .

É um erro bobo, mas levei um tempo para descobrir ...


1

Eu adiciono uma visão da altura de 1dp dentro do AppBarLayout e funciona muito melhor. Esse é o meu 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" />


Funciona apenas se você rolar para cima. Não quando você rolar para baixo embora
Arthur

Para mim funciona bem em ambas as direções. Você adicionou a visualização 1dp dentro do appbarlayout ?. Eu só testei no pirulito android e kitkat.
Jachumbelechao para Mantekilla 15/07/2015

Bem, eu também estou usando CollapsingToolbarLayout, que envolve a barra de ferramentas. Coloquei a vista 1dp dentro disso. É meio assim AppBarLayout -> CollapsingToolbarLayout -> Barra de ferramentas + exibição de 1dp
Arthur

Não sei se funciona bem com o CollapsingToolbarLayout. Eu só testei com esse código. Você tentou colocar a visualização 1dp fora do CollapsingToolbarLayout?
Jachumbelechao Unto Mantekilla

Sim. Rolar para cima funciona, rolar para baixo não expande a barra de ferramentas.
22615 Arthur Arthur

1

Já existem algumas soluções bastante populares aqui, mas depois de brincar com elas, encontrei uma solução mais simples que funcionou bem para mim. Minha solução também garante que ela AppBarLayoutseja expandida apenas quando o conteúdo rolável chegar ao topo, uma vantagem sobre outras soluções aqui.

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

O que é mPrevDy
ARR.s

1

A resposta aceita não funcionou para mim porque eu tinha RecyclerViewdentro de a SwipeRefreshLayoute a ViewPager. Esta é a versão aprimorada que procura um RecyclerViewna hierarquia e deve funcionar para qualquer 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

Resposta: Foi corrigido na biblioteca de suporte v26

mas a v26 tem algum problema no arremesso. Às vezes, o AppBar recupera mesmo que o arremesso não seja muito difícil.

Como removo o efeito de salto na appbar?

Se você encontrar o mesmo problema ao atualizar para o suporte v26, aqui está o resumo desta resposta .

Solução : Estenda o Comportamento padrão do AppBar e bloqueie a chamada para AppNar.Behavior onNestedPreScroll () e onNestedScroll () quando o AppBar for tocado enquanto NestedScroll ainda não parou.


0

Julian Os está certo.

A resposta de Manolo Garcia não funciona se a vista da reciclagem estiver abaixo do limite e rolar. Você deve comparar offseta visão de reciclagem e avelocity to the distance posição do item, não.

Eu fiz a versão java referindo-me ao código kotlin de julian e subtraia a reflexão.

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

}

não pode resloveBaseApplication
ARR.s

@ ARR.s desculpe, você acabou de substituir seu contexto como abaixo.
정성민

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



0

Adicionando outra resposta aqui, como as anteriores, não preencheu minhas necessidades completamente ou não funcionou muito bem. Este é parcialmente baseado em idéias espalhadas aqui.

Então, o que esse faz?

Movimento descendente do cenário: se o AppBarLayout estiver recolhido, ele permitirá que o RecyclerView seja executado sozinho sem fazer nada. Caso contrário, ele recolhe o AppBarLayout e impede que o RecyclerView faça seu arremesso. Assim que ele é recolhido (até o ponto que a velocidade especificada exige) e se houver velocidade restante, o RecyclerView é arremessado com a velocidade original menos o que o AppBarLayout acabou de consumir.

Movimento ascendente do cenário: se o deslocamento de rolagem do RecyclerView não for zero, ele será arremessado com a velocidade original. Assim que isso terminar e se ainda houver velocidade restante (ou seja, o RecyclerView rolou para a posição 0), o AppBarLayout é expandido até o ponto em que a velocidade original menos as demandas consumidas. Caso contrário, o AppBarLayout será expandido até o ponto que a velocidade original exige.

AFAIK, esse é o comportamento incorreto.

Há muita reflexão envolvida, e é bastante personalizado. Ainda não foram encontrados problemas. Também está escrito em Kotlin, mas entendê-lo não deve ser problema. Você pode usar o plug-in IntelliJ Kotlin para compilá-lo no bytecode -> e descompilá-lo novamente em Java. Para usá-lo, coloque-o no pacote android.support.v7.widget e defina-o como o comportamento do CoordinatorLayout.LayoutParams do AppBarLayout no código (ou adicione o construtor aplicável xml ou algo assim)

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

}

Como configurá-lo?
ARR.s

0

esta é a minha solução no meu projeto.
basta parar o mScroller ao obter 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

para androidx,

Se o seu arquivo de manifesto tiver uma linha android: hardwareAccelerated = "false", exclua-o.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.