171

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

tylerjroach
  • 2,639
  • 4
  • 16
  • 24
  • 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... – Aneem Jun 19 '15 at 04:07
  • Hey Aneem, I know this isn't the greatest solution but I began experimenting with this library: https://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.. – tylerjroach Jun 19 '15 at 12:36
  • 2
    Same issue here, I ended up avoiding AppBarLayout. – Renaud Cerrato Jun 23 '15 at 19:51
  • Yep. I ended up getting exactly what I needed out of the OvservableScrollView library. I'm sure it'll be fixed in future versions. – tylerjroach Jun 24 '15 at 02:48
  • The appbarlayout appears to 'consume' flings made with gestures shorter than the appbarlayout height. – Rich Ehmer Jun 26 '15 at 18:31
  • 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. – Den Drobiazko Jul 02 '15 at 15:51
  • @DenRimus I can't stress this library enough. https://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. – tylerjroach Jul 02 '15 at 15:58
  • 8
    The fling is buggy, [an issue](https://code.google.com/p/android/issues/detail?id=177729) has been raised (and accepted). – Renaud Cerrato Jul 02 '15 at 21:20
  • My problem exactly. Thanks for asking about it. – Ashkan Sarlak Jan 11 '16 at 13:23
  • 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. – jch000 Feb 10 '16 at 20:38
  • it has been fixed with Android 26.0.0-beta2 version of support library. ref https://issuetracker.google.com/issues/37053410 – Prags Dec 16 '17 at 14:13

19 Answers19

114

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.

Community
  • 1
  • 1
Manolo Garcia
  • 3,595
  • 2
  • 17
  • 19
  • You saved my day! Seems to be working absolutely fine! Why is your answer not accepted? – Zordid Sep 12 '15 at 08:39
  • This won't work with fling up, right? Any hack for that? – David Corsalini Oct 29 '15 at 11:41
  • But this defeats the AppBarLayout behavior sadly. – EngineSense Jan 02 '16 at 10:14
  • I have tried all them but unfortunately nothing changed hopefully...could you help – Ansal Ali Feb 09 '16 at 07:16
  • 9
    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) {` – LucasFM May 16 '16 at 16:16
  • 1
    + 1 Analyzing this fix, I do not understand Why Google has not yet fixed this. The code seems to be quite simple. – Gaston Flores May 30 '16 at 18:31
  • 3
    Hello how to achieve the same thing with appbarlayout and Nestedscrollview...Thanks in advance.. – Harry Sharma Jun 30 '16 at 07:56
  • 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. – ubuntudroid Sep 20 '16 at 09:42
  • 1
    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. – Augusto Carmo Oct 26 '16 at 17:02
  • If you use both RecyclerView and NestedScrollview, you must use NestedScrollview first (I am using NestedScrollview for empty message) – hoi Jan 23 '17 at 07:25
  • 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"? – Massimo Baldrighi Feb 23 '17 at 02:28
  • Hmm I've tried using it, but flinging down only goes to the top of the `RecyclerView` not to the `header`. – jlively Apr 11 '17 at 17:20
  • sorry ,I'm a newbie about java and android ,I want to ask you,How to use it? – ARR.s Aug 27 '17 at 10:17
69

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.

Behzad Bahmanyar
  • 5,696
  • 4
  • 29
  • 38
Kirill Boyarshinov
  • 5,963
  • 4
  • 31
  • 29
  • Getting string types are not allowed for layout_behavior error – Vaisakh N Feb 12 '16 at 05:12
  • I tested it and works better man! but what is the purpose of the TOP_CHILD_FLING_THRESHOLD? and why it is 3? – Julio_oa Feb 18 '16 at 16:50
  • @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. – Kirill Boyarshinov Feb 19 '16 at 01:33
  • Hello how to achieve the same thing with appbarlayout and Nestedscrollview...Thanks in advance.. – Harry Sharma Jun 30 '16 at 07:56
  • 2
    @Hardeep change `target instanceof RecyclerView` to `target instanceof NestedScrollView`, or more for generic case to `target instanceof ScrollingView`. I updated the answer. – Kirill Boyarshinov Jul 08 '16 at 07:44
  • 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 Dec 10 '17 at 08:29
15

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);
            }
        }
    }
}
Mak Sing
  • 871
  • 2
  • 9
  • 18
  • 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 ? – Micah Simmons Apr 07 '16 at 12:31
  • 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) – Mak Sing Apr 09 '16 at 03:14
  • @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. – Nitin Misra Oct 25 '16 at 02:25
  • 1
    @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. – saberrider Nov 24 '16 at 12:55
  • 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. – Anton Malmygin Dec 09 '16 at 17:01
  • BEST SOLUTION HERE! Thanks. May you become a millionaire! – Manish Kumar Sharma Dec 10 '17 at 08:22
  • 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. – Manish Kumar Sharma Dec 10 '17 at 08:41
13

It has been fixed since support design 26.0.0.

compile 'com.android.support:design:26.0.0'
Xiaozou
  • 1,585
  • 1
  • 14
  • 28
  • 2
    This needs to move up. This is described [here](https://chris.banes.me/2017/06/09/carry-on-scrolling/) in case anyone is interested in the details. – Chris Dinon Aug 01 '17 at 18:03
  • 1
    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! – box Oct 16 '17 at 19:09
  • 2
    @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 – dor506 Oct 20 '17 at 13:08
  • @dor506 https://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 :) – vida Nov 15 '17 at 02:17
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

Mansukh Ahir
  • 3,546
  • 4
  • 36
  • 58
4

It's a recyclerview bug . It's supposed to be fixed in v23.1.0.

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

enter image description here

dupengtao
  • 109
  • 3
2

This is my Layout and the scroll It's working as it should.

<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
  • 71
  • 1
  • 6
2

My solution so far, based on Mak Sing and Manolo Garcia answers.

It's not totally perfect. For now I don't know how to recalculate a valide velocity to avoid a weird effect: the appbar can expand faster than the scroll speed. But the state with an expanded appbar and a scrolled recycler view cannot be reached.

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

    }

}
Community
  • 1
  • 1
Zxcv
  • 1,267
  • 1
  • 13
  • 23
  • 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());``` – Nicholas Jan 19 '17 at 02:41
2

In my case, I was getting the issue where the flinging the RecyclerView would not scroll it smoothly, making it get stuck.

This was because, for some reason, I had forgotten that I had put my RecyclerView in a NestedScrollView.

It's a silly mistake, but it took me a while to figure it out...

Farbod Salamat-Zadeh
  • 18,039
  • 16
  • 66
  • 118
1

I add a view of 1dp height inside the AppBarLayout an then it works much better. This is my 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" />

1

Already some pretty popular solutions here but after playing with them I came up with a rather simpler solution that worked well for me. My solution also ensures that the AppBarLayout is only expanded when the scrollable content reaches the top, an advantage over other solutions here.

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
  • 393
  • 2
  • 16
1

The accepted answer didn't work for me because I had RecyclerView inside a SwipeRefreshLayout and a ViewPager. This is the improved version that seeks a RecyclerView in the hierarchy and should work for any 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;
    }
}
Dmide
  • 6,183
  • 3
  • 21
  • 31
1

Answer: It's fixed in support library v26

but v26 has some issue in flinging. Sometimes, AppBar bounces back again even if fling is not too hard.

How do I remove the bouncing effect on appbar?

If you encounter the same issue when updating to support v26, here's the summary of this answer.

Solution: Extend AppBar's default Behavior and block the call for AppBar.Behavior's onNestedPreScroll() and onNestedScroll() when AppBar is touched while NestedScroll hasn't stopped yet.

vida
  • 4,159
  • 1
  • 18
  • 25
0

Adding another answer here as the above ones did either not fulfill my needs completely or didn't work very well. This one is partially based on ideas spread here.

So what does this one do?

Scenario downwards fling: If the AppBarLayout is collapsed, it lets the RecyclerView fling on its own without doing anything. Otherwise, it collapses the AppBarLayout and prevents the RecyclerView from doing its fling. As soon as it is collapsed (up to the point that the given velocity demands) and if there is velocity left, the RecyclerView gets flung with the original velocity minus what the AppBarLayout just consumed collapsing.

Scenario upwards fling: If the RecyclerView's scroll offset is not zero, it gets flung with the original velocity. As soon as that is finished and if there is still velocity left (i.e. the RecyclerView scrolled to position 0), the AppBarLayout gets expanded up to the point that the original velocity minus the just consumed demands. Otherwise, the AppBarLayout gets expanded up to the point that the original velocity demands.

AFAIK, this is the indended behavior.

There is a lot of reflection involved, and it's pretty custom. No issues found yet though. It is also written in Kotlin, but understanding it should be no problem. You can use the IntelliJ Kotlin plugin to compile it to bytecode -> and decompile it back to Java. To use it, place it in the android.support.v7.widget package and set it as the AppBarLayout's CoordinatorLayout.LayoutParams' behavior in code (or add the xml applicable constructor or something)

/*
 * 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
  • 253
  • 1
  • 15
0

Julian Os is right.

Manolo Garcia's answer does not work if the recyclerview is below the threshold and scrolls. You must compare the offset of the recyclerview and the velocity to the distance, not the item position.

I made java version by referring to julian's kotlin code and subtract reflection.

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

}
Anh Pham
  • 2,004
  • 8
  • 16
  • 28
정성민
  • 33
  • 1
  • 4
0

I have found the fix by Eniz Bilgin https://stackoverflow.com/a/45090239/7639018

The problem has been solved with the libraries in this repository.

(https://developer.android.com/topic/libraries/support-library/setup.html)

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    }
}
ARR.s
  • 679
  • 1
  • 17
  • 35
0

With reference to Google issue tracker, it has been fixed with Android 26.0.0-beta2 version of support library

Please update your Android support library version 26.0.0-beta2.

If any issue persists, please report at Google issue tracker they will re-open to examine.

Prags
  • 2,277
  • 2
  • 18
  • 32
0

this is my solution in my project.
just stop the mScroller when get 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
  • 5
0

for androidx,

If your manifest file has a android:hardwareAccelerated="false" line, delete it.

Jetwiz
  • 363
  • 2
  • 12