Android toolbar elevation when scrolling
Asked Answered
K

6

30

I try to implement a search bar like in google maps android app:

enter image description here

When the recycler view is in its initial state, the toolbar has no elevation. Only when the users starts scrolling the elevation becomes visible. And the search bar (toolbar) never collapses. Here is what I tried to replicate this:

<android.support.design.widget.CoordinatorLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="64dp">

            <!-- content -->

        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

And here you can see the result:

enter image description here

So the problem with my solution is, that the elevation of the toolbar is always visible. But I want it to appear only when the recycler view scrolls behind it. Is there anything from the design support library that enables such behavior as seen in the google maps app?

I am using

com.android.support:appcompat-v7:23.2.0
com.android.support:design:23.2.0
Keir answered 1/3, 2016 at 13:31 Comment(2)
Have you tried using CollapsingToolbarLayout ?Bonded
The accepted answer is outdated. Now there is inbuilt functionality to do this. Read my answer for more details. https://mcmap.net/q/466553/-android-toolbar-elevation-when-scrollingTheft
T
34

EDIT As pointed out in the comments, my answer is now outdated, see https://mcmap.net/q/466553/-android-toolbar-elevation-when-scrolling


Whether you are using a CoordinatorLayout or not, a RecyclerView.OnScrollListener seems like the right way to go as far as the elevation is concerned. However, from my experience recyclerview.getChild(0).getTop() is not reliable and should not be used for determining the scrolling state. Instead, this is what's working:

private static final int SCROLL_DIRECTION_UP = -1;
// ...
// Put this into your RecyclerView.OnScrollListener > onScrolled() method
if (recyclerview.canScrollVertically(SCROLL_DIRECTION_UP)) {
   // Remove elevation
   toolbar.setElevation(0f);
} else {
   // Show elevation
   toolbar.setElevation(50f);
}

Be sure to assign a LayoutManager to your RecyclerView or the call of canScrollVertically may cause a crash!

Tims answered 28/3, 2016 at 6:24 Comment(3)
This works but now is easier to get the desired functionality using liftOnScroll. Doc link: github.com/material-components/material-components-android/blob/…Funds
The accepted answer is outdated. Now there is inbuilt functionality to do this. Read my answer for more details. https://mcmap.net/q/466553/-android-toolbar-elevation-when-scrollingTheft
@RicardoContreras liftOnScroll isn't easier in all cases, though. I have a fairly complex navigation structure and liftOnScroll was actually completely impossible to implement in my case, because of RecyclerViews nested in fragments and ViewPagers, while the OnScrollListener solution was super easy with only a few code tweaks.Southwards
T
49

The accepted answer is outdated. Now there is inbuilt functionality to do this. I am pasting the whole layout code so it will help you to understand.

You just need to use CoordinatorLayout with AppBarLayout. This design pattern is called Lift On Scroll and can be implemented by setting app:liftOnScroll="true" on your AppBarLayout.

Note: the liftOnScroll attribute requires that you apply the @string/appbar_scrolling_view_behavior layout_behavior to your scrolling view (e.g., NestedScrollView, RecyclerView, etc.).

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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"
    tools:context=".MainActivity"
    android:background="@color/default_background">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:liftOnScroll="true">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/default_background" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/appbar"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:orientation="vertical" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Refered this documentation https://github.com/material-components/material-components-android/blob/master/docs/components/AppBarLayout.md

Theft answered 7/10, 2019 at 15:4 Comment(5)
Unfortunately liftOnScroll is not working for me. I have an appbar with 2 views inside it.Osteoarthritis
Your application theme needs to have a MaterialComponent parent e.g parent="Theme.MaterialComponents.Light.NoActionBar"Soutine
thanks,man, it was a very accurate answer for me.Saccharase
What if my top header has a margin right and left I would like to reduce while scrollingLeitman
liftOnScroll shows the elevation when scrolling, but it removes the elevation when it's all the way at the top. How do you also add elevation to the appbar when it's scrolled all the way to the top?Infarction
T
34

EDIT As pointed out in the comments, my answer is now outdated, see https://mcmap.net/q/466553/-android-toolbar-elevation-when-scrolling


Whether you are using a CoordinatorLayout or not, a RecyclerView.OnScrollListener seems like the right way to go as far as the elevation is concerned. However, from my experience recyclerview.getChild(0).getTop() is not reliable and should not be used for determining the scrolling state. Instead, this is what's working:

private static final int SCROLL_DIRECTION_UP = -1;
// ...
// Put this into your RecyclerView.OnScrollListener > onScrolled() method
if (recyclerview.canScrollVertically(SCROLL_DIRECTION_UP)) {
   // Remove elevation
   toolbar.setElevation(0f);
} else {
   // Show elevation
   toolbar.setElevation(50f);
}

Be sure to assign a LayoutManager to your RecyclerView or the call of canScrollVertically may cause a crash!

Tims answered 28/3, 2016 at 6:24 Comment(3)
This works but now is easier to get the desired functionality using liftOnScroll. Doc link: github.com/material-components/material-components-android/blob/…Funds
The accepted answer is outdated. Now there is inbuilt functionality to do this. Read my answer for more details. https://mcmap.net/q/466553/-android-toolbar-elevation-when-scrollingTheft
@RicardoContreras liftOnScroll isn't easier in all cases, though. I have a fairly complex navigation structure and liftOnScroll was actually completely impossible to implement in my case, because of RecyclerViews nested in fragments and ViewPagers, while the OnScrollListener solution was super easy with only a few code tweaks.Southwards
H
4

This is a good question but none of the existing answers are good enough. Calling getTop() is absolutely not recommended as it's very unreliable. If you look at newer versions of Google apps that follow Material Design Refresh (2018) guidelines, they hide the elevation at the beginning and immediately add it as user scrolls down and hide it again as user scrolls and reaches the top again.

I managed to achieve the same effect using the following:

val toolbar: android.support.v7.widget.Toolbar? = activity?.findViewById(R.id.toolbar);

recyclerView?.addOnScrollListener(object: RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy);

        if(toolbar == null) {
            return;
        }

        if(!recyclerView.canScrollVertically(-1)) {
            // we have reached the top of the list
            toolbar.elevation = 0f
        } else {
            // we are not at the top yet
            toolbar.elevation = 50f
        }
    }
});

This works perfectly with vertical recycler views (even with tab view or other recycler views inside them);

A couple of important notes:

  • Here I'm doing this inside a fragment hence activity?.findViewById...
  • If your Toolbar is nested inside an AppBarLayout, then instead of applying elevation to Toolbar, you should apply it to the AppBarLayout.
  • You should add android:elevation="0dp" and app:elevation="0dp" attributes to your Toolbar or AppBarLayout so that the recycler view doesn't have elevation at the beginning.
Hospers answered 21/9, 2018 at 17:21 Comment(0)
D
3

I have a RecyclerView in my fragment. I could achieve similar effect using code below:

It is not the Smartest way and you can wait for better answers.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

    // Initial Elevation
    final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
    if(toolbar!= null)
        toolbar.setElevation(0);

    // get initial position
    final int initialTopPosition = mRecyclerView.getTop();

    // Set a listener to scroll view
    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if(toolbar!= null && mRecyclerView.getChildAt(0).getTop() < initialTopPosition ) {
                toolbar.setElevation(50);
            } else {
                toolbar.setElevation(0);
            }
        }
    });
}
Demers answered 1/3, 2016 at 14:40 Comment(3)
looks good, thank you. You think this is not possible with just xml?Keir
I think it is not possible only by XML.. DefaultBahevior for toolbar is not related to elevation (but to its Height). Even if you find a way to use CoordinatorLayout, I think you will need some code... I'll trying to achieve same effect using only CoordinatorLayout.. I'll share as soon as possibleDemers
Almost perfect, canScrollVertically is what you're looking for within your OnScrollListener (see my answer). Besides, simply returning if toolbar == null before calling addOnScrollListener() would make your code a little bit better IMHO.Tims
S
3

I found this when page when I wanted to do something similar, but for a more complex View Hierarchy.

After some research, I was able to get the same effect using a custom behavior. This works for any view in a coordinator layout (given that there's a nested scroll element such as RecyclerView or NestedScrollView)

Note: This only works on API 21 and above as ViewCompat.setElevation does not seem to have any effect pre lollipop and AppBarLayout#setTargetElevation is deprecated

ShadowScrollBehavior.java

public class ShadowScrollBehavior extends AppBarLayout.ScrollingViewBehavior
        implements View.OnLayoutChangeListener {

    int totalDy = 0;
    boolean isElevated;
    View child;

    public ShadowScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child,
                                   View dependency) {
        parent.addOnLayoutChangeListener(this);
        this.child = child;
        return super.layoutDependsOn(parent, child, dependency);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull View child, @NonNull View directTargetChild,
                                       @NonNull View target, int axes, int type) {
        // Ensure we react to vertical scrolling
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                        target, axes, type);
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                  @NonNull View child, @NonNull View target,
                                  int dx, int dy, @NonNull int[] consumed, int type) {
        totalDy += dy;
        if (totalDy <= 0) {
            if (isElevated) {
                ViewGroup parent = (ViewGroup) child.getParent();
                if (parent != null) {
                    TransitionManager.beginDelayedTransition(parent);
                    ViewCompat.setElevation(child, 0);
                }
            }
            totalDy = 0;
            isElevated = false;
        } else {
            if (!isElevated) {
                ViewGroup parent = (ViewGroup) child.getParent();
                if (parent != null) {
                    TransitionManager.beginDelayedTransition(parent);
                    ViewCompat.setElevation(child, dp2px(child.getContext(), 4));
                }
            }
            if (totalDy > target.getBottom())
                totalDy = target.getBottom();
            isElevated = true;
        }
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }


    private float dp2px(Context context, int dp) {
        Resources r = context.getResources();
        float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
        return px;
    }


    @Override
    public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
        totalDy = 0;
        isElevated = false;
        ViewCompat.setElevation(child, 0);
    }
}

my_activity_layout.xml

<android.support.design.widget.CoordinatorLayout
    android:fitsSystemWindows="true"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_height="match_parent"
        android:layout_width="match_parent" />

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        app:layout_behavior="com.myapp.ShadowScrollBehavior">


        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_height="64dp"
            android:layout_width="match_parent">

            <!-- content -->

        </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>
Supernumerary answered 15/5, 2018 at 21:25 Comment(0)
C
1

If you use CoordinatorLayout you dont need any extra code to make this work by yourself just some setup on style and layout XML, check this:

  1. Your app style should use a MaterialCompoment style, like src/main/res/values/styles.xml.

  2. Setup you AppBarLayout:

    • Use any MaterialCompoments style for this component like: Widget.MaterialComponents.AppBarLayout.Surface.
    • Set app:liftOnScroll="true" to enable the automatic elevation based on scroll.
  3. Setup your scrolling view:

    • Set app:layout_behavior="@string/appbar_scrolling_view_behavior.

https://github.com/danielgomezrico/spike-appbarlayout-toolbar-automatic-elevation

Cramer answered 6/6, 2020 at 17:43 Comment(2)
link is broken @Daniel Gomez RicoMagnific
@Sekiro I made an update, thanks for pointing it outCramer

© 2022 - 2024 — McMap. All rights reserved.