Flinging with RecyclerView + AppBarLayout

55,047

Solution 1

The answer of Kirill Boyarshinov was almost correct.

The main problem is that the RecyclerView sometimes is giving incorrect fling direction, so if you add the following code to his answer it works correctly:

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

I hope that this helps.

Solution 2

Seems that v23 update did not fix it yet.

I have found sort of of hack to fix it with flinging down. The trick is to reconsume fling event if ScrollingView's top child is close to the beginning of data in 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 it in your layout like that:

 <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: Fling event reconsuming is now based on verticalScrollOffset instead of amount of items on from top of RecyclerView.

EDIT2: Check target as ScrollingView interface instance instead of RecyclerView. Both RecyclerView and NestedScrollingView implement it.

Solution 3

I have found the fix by applying OnScrollingListener to the recyclerView. now it works very well. The issue is that recyclerview provided the wrong consumed value and the behavior doesn't know when the recyclerview is scrolled to the top.

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

Solution 4

It has been fixed since support design 26.0.0.

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

Solution 5

This is a smooth version of Google Support Design AppBarLayout. If you are using AppBarLayout, you will know it has an issue with fling.

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

See Library here.. https://github.com/henrytao-me/smooth-app-bar-layout

Share:
55,047

Related videos on Youtube

tylerjroach
Author by

tylerjroach

Senior Software Engineer - Android @ Salesforce

Updated on April 08, 2020

Comments

  • tylerjroach
    tylerjroach about 4 years

    I am using the new CoordinatorLayout with AppBarLayout and CollapsingToolbarLayout. Below AppBarLayout, I have a RecyclerView with a list of content.

    I have verified that fling scrolling works on the RecyclerView when I am scrolling up and down the list. However, I would also like the AppBarLayout to smoothly scroll during expansion.

    When scrolling up to expand the CollaspingToolbarLayout, scrolling immediately stops once lifting your finger off the screen. If you scroll up in a quick motion, sometimes the CollapsingToolbarLayout re-collapses as well. This behavior with the RecyclerView seems to function much differently than when using a NestedScrollView.

    I've tried to set different scroll properties on the recyclerview but I haven't been able to figure this out.

    Here is a video showing some of the scrolling issues. https://youtu.be/xMLKoJOsTAM

    Here is an example showing the issue with the RecyclerView (CheeseDetailActivity). https://github.com/tylerjroach/cheesesquare

    Here is the original example that uses a NestedScrollView from Chris Banes. https://github.com/chrisbanes/cheesesquare

    • Aneem
      Aneem almost 9 years
      I'm experiencing this same exact issue (I'm using with a RecyclerView). If you look at a google play store listing for any app, it seems to behave correctly, so there's definitely a solution out there...
    • tylerjroach
      tylerjroach almost 9 years
      Hey Aneem, I know this isn't the greatest solution but I began experimenting with this library: github.com/ksoichiro/Android-ObservableScrollView. Especially at this activity to achieve the results I needed: FlexibleSpaceWithImageRecyclerViewActivity.java. Sorry about misspelling your name before the edit. Autocorrect..
    • Renaud Cerrato
      Renaud Cerrato almost 9 years
      Same issue here, I ended up avoiding AppBarLayout.
    • tylerjroach
      tylerjroach almost 9 years
      Yep. I ended up getting exactly what I needed out of the OvservableScrollView library. I'm sure it'll be fixed in future versions.
    • Rich Ehmer
      Rich Ehmer almost 9 years
      The appbarlayout appears to 'consume' flings made with gestures shorter than the appbarlayout height.
    • Den Drobiazko
      Den Drobiazko almost 9 years
      I want to upwote this question a hundred times more. @RenaudCerrato your comment doesn't help because CollapsingToolbarLayout must be a direct child of AppBarLayout. Maybe it was sufficient for you to drop both of them, but I need that parallax effect when collapsing.
    • tylerjroach
      tylerjroach almost 9 years
      @DenRimus I can't stress this library enough. github.com/ksoichiro/Android-ObservableScrollView While the solution itself may be a little "hacky". It gets the job done and is extremely easy to implement. The library has a demo that has any effect you would need.
    • Renaud Cerrato
      Renaud Cerrato almost 9 years
      The fling is buggy, an issue has been raised (and accepted).
    • Ashkan Sarlak
      Ashkan Sarlak over 8 years
      My problem exactly. Thanks for asking about it.
    • jch000
      jch000 over 8 years
      Removing snap from the scroll flags seemed to get rid of any wonkiness with AppBarLayout and a RecyclerView scrolling on my end. Tried that after I realized the Google Play Store AppBarLayout does not use snapping and achieved similar behavior to what I wanted.
    • Prags
      Prags over 6 years
      it has been fixed with Android 26.0.0-beta2 version of support library. ref issuetracker.google.com/issues/37053410
  • Arthur
    Arthur almost 9 years
    It works only if you scroll up. Not when you scroll down though
  • Jachumbelechao Unto Mantekilla
    Jachumbelechao Unto Mantekilla almost 9 years
    For me works well in both directions. Did you add the 1dp view inside the appbarlayout?. I only tested it in android lollipop and kitkat.
  • Arthur
    Arthur almost 9 years
    Well, i'm also using CollapsingToolbarLayout which wraps the toolbar. I put the 1dp view inside that. It's kinda like this AppBarLayout ->CollapsingToolbarLayout ->Toolbar + 1dp view
  • Jachumbelechao Unto Mantekilla
    Jachumbelechao Unto Mantekilla almost 9 years
    I don't know if it works well with the CollapsingToolbarLayout. I only tested with this code. Did you try to put the 1dp view outside the CollapsingToolbarLayout?
  • Arthur
    Arthur almost 9 years
    Yes. Scroll up works, scroll down doesn't expand the toolbar.
  • Zordid
    Zordid over 8 years
    You saved my day! Seems to be working absolutely fine! Why is your answer not accepted?
  • David Corsalini
    David Corsalini over 8 years
    This won't work with fling up, right? Any hack for that?
  • EngineSense
    EngineSense over 8 years
    But this defeats the AppBarLayout behavior sadly.
  • Ansal Ali
    Ansal Ali over 8 years
    I have tried all them but unfortunately nothing changed hopefully...could you help
  • Vaisakh N
    Vaisakh N about 8 years
    Getting string types are not allowed for layout_behavior error
  • Julio_oa
    Julio_oa about 8 years
    I tested it and works better man! but what is the purpose of the TOP_CHILD_FLING_THRESHOLD? and why it is 3?
  • Kirill Boyarshinov
    Kirill Boyarshinov about 8 years
    @Julio_oa TOP_CHILD_FLING_THRESHOLD means that fling event would be reconsumed if recycler view is scrolled to the element which position is below this threshold value. Btw I updated the answer to use verticalScrollOffset which is more general. Now fling event will be reconsumed when recyclerView is scrolled to top.
  • Micah Simmons
    Micah Simmons about 8 years
    Thanks for your post. I've tried all the answers on this page & in my experience this is the most effective answer. But, the RecylerView in my layout scrolls internally before the AppBarLayout has scrolled off screen if I don't scroll the RecyclerView with enough force. In other words, when I scroll the RecyclerView with enough force the AppBar scrolls off the screen without the RecyclerView scrolling internally, but when I don't scroll the RecyclerView with enough force the RecyclerView scrolls internally before the AppbarLayout has scrolled off the screen. Do you know what is causing that ?
  • Mak Sing
    Mak Sing about 8 years
    The recyclerview still receive touch events that's why it still scrolls, the behavior onNestedFling would animate to scroll the appbarLayout at the same time. Maybe you can try override onInterceptTouch in the behavior to change this. To me the current behavior is acceptable from what I see. (not sure if we are seeing the same thing)
  • LucasFM
    LucasFM almost 8 years
    if you are using a SwipeRefreshLayout as parent of your recyclerview, just add this code : if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); } before if (target instanceof RecyclerView && velocityY < 0) {
  • Gaston Flores
    Gaston Flores almost 8 years
    + 1 Analyzing this fix, I do not understand Why Google has not yet fixed this. The code seems to be quite simple.
  • Arthur
    Arthur almost 8 years
    v23.4.0 - Still not fixed
  • Harry Sharma
    Harry Sharma almost 8 years
    Hello how to achieve the same thing with appbarlayout and Nestedscrollview...Thanks in advance..
  • Harry Sharma
    Harry Sharma almost 8 years
    Hello how to achieve the same thing with appbarlayout and Nestedscrollview...Thanks in advance..
  • Kirill Boyarshinov
    Kirill Boyarshinov almost 8 years
    @Hardeep change target instanceof RecyclerView to target instanceof NestedScrollView, or more for generic case to target instanceof ScrollingView. I updated the answer.
  • ubuntudroid
    ubuntudroid over 7 years
    We additionally had to override canDragView() and always return true for this to work. Otherwise the layout would sometimes get stuck during scrolling. Unfortunately you have to move the FlingBehavior class to the support package to achieve that as the canDragView() method is just package visible.
  • Nitin Misra
    Nitin Misra over 7 years
    @MakSing it's really helpful with CoordinatorLayout and ViewPager setup thanks very much for this most awaited solution. Please write a GIST for the same so that other devs can also benefit from it. I'm sharing this solution also. Thanks Again.
  • Augusto Carmo
    Augusto Carmo over 7 years
    It did not work for me =/ By the way, you do not need to move the class into the support package to achieve it, you can register a DragCallback in the constructor.
  • saberrider
    saberrider over 7 years
    @MakSing Off all solutions, this works the best for me. I adjusted the velocity handed to the onNestedFling a little bit velocity * 0.6f ... seems to give a nicer flow to it.
  • Anton  Malmygin
    Anton Malmygin over 7 years
    Works for me. @MakSing Does in onScrolled method you must call onNestedFling of AppBarLayout.Behavior and not of RecyclerViewAppBarBehavior ? Seems a bit strange to me.
  • 0xcaff
    0xcaff over 7 years
    Still not fixed in v25.1.0
  • Nicholas
    Nicholas over 7 years
    You can obtain the current velocity of a recyclerView (as of 25.1.0) using reflection: 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());
  • hoi
    hoi over 7 years
    If you use both RecyclerView and NestedScrollview, you must use NestedScrollview first (I am using NestedScrollview for empty message)
  • Massimo Baldrighi
    Massimo Baldrighi about 7 years
    Sorry if I return on this topic: what do you mean with "add this code to his [Kirill's] answer"? Does you onNestedFling() substitute completely his? @ubuntudroid: how can I "move FlingBehavior class to support package"?
  • jlively
    jlively about 7 years
    Hmm I've tried using it, but flinging down only goes to the top of the RecyclerView not to the header.
  • Bawa
    Bawa almost 7 years
    v25.3.1, still looks bad.
  • Chris Dinon
    Chris Dinon almost 7 years
    This needs to move up. This is described here in case anyone is interested in the details.
  • ARR.s
    ARR.s over 6 years
    sorry ,I'm a newbie about java and android ,I want to ask you,How to use it?
  • ARR.s
    ARR.s over 6 years
    what is mPrevDy
  • ARR.s
    ARR.s over 6 years
    cannot reslove BaseApplication
  • ARR.s
    ARR.s over 6 years
    How to set it ?
  • 정성민
    정성민 over 6 years
    @ARR.s sorry, you just replace it your context like below.
  • 정성민
    정성민 over 6 years
    YOUR_CONTEXT.getResources().getDisplayMetrics().density * 160.0f;
  • Micer
    Micer over 6 years
    It's finally fixed in v26.0.1!
  • box
    box over 6 years
    Now there seems to be an issue with status bar, where when you scroll down the status bar goes down a bit with the scroll...its super annoying!
  • dor506
    dor506 over 6 years
    @Xiaozou I'm using 26.1.0 and still got issues with flinging. Quick fling sometimes result in opposite movement (The velocity of the movement is opposite/wrong as can be seen in onNestedFling method). Reproduced it in Xiaomi Redmi Note 3 and Galaxy S3
  • vida
    vida over 6 years
    @dor506 stackoverflow.com/a/47298312/782870 I'm not sure if we have the same issue when you say opposite movement result. But I posted an answer here. Hope it helps :)
  • Manish Kumar Sharma
    Manish Kumar Sharma over 6 years
    BEST SOLUTION HERE! Thanks. May you become a millionaire!
  • Manish Kumar Sharma
    Manish Kumar Sharma over 6 years
    You know the problem lies with RecyclerView that reports incorrect consumption. @mak-sing 's solution below tackles exactly that - a scroll listener for RecyclerView that solves it.
  • Manish Kumar Sharma
    Manish Kumar Sharma over 6 years
    Explanation: Basically, you want to know if RecycleView has scrolled to top right?(because that's when you would be flinging the AppBarLayout). The docs for the RecyclerView.OnScrollListener say about onScrolled() method that it is called once after the scrolled has finished. So, if you keep adding the dy's, you would get a net dy. This net dy would be 0 when we have not scrolled or have returned to the initial position. Voila! that's what we wanted.
  • AlexS
    AlexS almost 3 years
    @ARR.s i think this is 'mPreviousDy '