DarkLeafyGreen
DarkLeafyGreen

Reputation: 70466

Android toolbar elevation when scrolling

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

Upvotes: 30

Views: 20337

Answers (6)

Daniel Gomez Rico
Daniel Gomez Rico

Reputation: 15945

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

Upvotes: 2

Daniel Veihelmann
Daniel Veihelmann

Reputation: 1487

EDIT As pointed out in the comments, my answer is now outdated, see https://stackoverflow.com/a/58272283/4291272


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!

Upvotes: 35

Faisal Shaikh
Faisal Shaikh

Reputation: 4157

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

Upvotes: 49

Vahid Amiri
Vahid Amiri

Reputation: 11117

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.

Upvotes: 4

user3425867
user3425867

Reputation: 654

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>

Upvotes: 3

guipivoto
guipivoto

Reputation: 18687

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

Upvotes: 3

Related Questions