Quick return with CoordinatorLayout

One nifty UI element that you can add to a scrolling view is a quick return view- an element that disappears when the user scrolls in one direction, then reappears when the user scrolls in the opposite direction.

In other words, what the floating action button in the Google+ app does.

Now you see it, now you don’t

I was in the process of migrating a ListView to a RecyclerView, and one piece of this screen I needed to re-implement was a similar quick return view.

There are some reasonably popular libraries for accomplishing this with ListView, such as Lars Werkman’s. Nick Butcher and Roman Nurik’s scroll tricks inspired many of these libraries. Prior to the release of the design support library, Makovastar’s FAB library was the go-to for many apps with a FAB. That library also provides a utility to attach to scrollable views so the FAB can automatically disappear as the user scrolls down the list.

However my quick return view wasn’t a FAB and I had a feeling I could get this behavior without the complexity of some of those existing solutions.

Spoiler: The answer came with CoordinatorLayout.

I thought of how AppBarLayout used CoordinatorLayout’s Behavior mechanism to collapse a Toolbar as a user scrolls a nested scroll view. Then I got scared. CoordinatorLayout is fairly new and I assumed that Behaviors were complex.

It turns out that Behaviors aren’t terribly complex. I’m going to show you how to create a CoordinatorLayout Behavior that will provide easy quick return functionality.

Enter CoordinatorLayout

First, we need a layout. Our layout is going to be pretty basic- just a CoordinatorLayout that contains a RecyclerView and our footer view.

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

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

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="QuickReturn Footer"/>

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

Next, lets take a look at the CoordinatorLayout.Behavior class. It offers a many callbacks for receiving events from other views in the CoordinatorLayout that you want to, well, coordinate with.

A Behavior is associated with a specific View- the callbacks refer to this View as the “child.” Many callbacks also pass a “target” or a “dependency,” which is the View that triggered the callback.

Some of these callbacks exist to let the CoordinatorLayout what other Views your Behavior cares about. If you want one View to do something based on the presence of a ImageView for instance, you could use the following overrides:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof ImageView;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
// Adjust the child View accordingly
return true;
}

Scrolling Behavior

Our Behavior, however, will only care about scroll events. A Behavior will receive some events out of the box without us needing to declare any dependencies.

There’s one catch- the CoordinatorLayout will let our Behavior know when a scroll is started from any of its descendants, but to receive any future scroll events we need to let the CoordinatorLayout know we care:

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent,
View child, View target, View target,int scrollAxes) {
    return (scrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

I only care about vertical scrolling in my RecyclerView, so I return true only when the scroll event contains a vertical component.

Next we will react to the scroll event. We have two callbacks that we could utilize for this- onNestedScroll() and onNestedPreScroll(). These two methods exist because some Behaviors (like those used with an AppBarLayout) might consume part of a scroll event. Since AppBarLayout allows your toolbar to scroll away with the content, it might “consume” however much distance it scrolled to indicate to the other Views that it already accounted for some of the scroll distance.

I want my quick return view to show and hide independently of the other Views, so I just went with onNestedPreScroll() to get the raw distance that the user scrolled.

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
if (dy > 0 && mDySinceDirectionChange < 0
|| dy < 0 && mDySinceDirectionChange > 0) {
mDySinceDirectionChange = 0;
}

mDySinceDirectionChange += dy;

if (mDySinceDirectionChange > child.getHeight()
&& child.getVisibility() == View.VISIBLE) {
hide(child);
} else if (mDySinceDirectionChange < 0
&& child.getVisibility() == View.GONE) {
show(child);
}
}

This is a big(ish) chunk of code, so let’s take a moment to go over it.

First, my Behavior is keeping track of how much the target view has scrolled since the user last changed directions. This allows the behavior to react appropriately when the user changes direction without lifting their finger. Because the Behavior tracks the cumulative amount that the user has scrolled we can also wait to hide the view for a set distance. In this case that distance is the height of the quick return view.

I’m also checking whether the child is visible before hiding or showing it- you’ll see why in just a second.

Hiding and Showing the quick return view

Setting the quick return’s visibility to GONE would work, but I don’t want my quick return view to just disappear like magic. I want it to animate in and out.

Fortunately ViewPropertyAnimators make this easy:

private void hide(final View view) {
view.animate()
.translationY(view.getHeight())
.setInterpolator(INTERPOLATOR)
.setDuration(200)
.start();
}
private void show(final View view) {
view.animate()
.translationY(0)
.setInterpolator(INTERPOLATOR)
.setDuration(200)
.start();
}

That’s all we have to do!

Well, there are a few more details to take care of. I told you that we only wanted to hide the view if it is visible, and show the view if it is hidden. Another issue is that the Behavior won’t react particularly well if a user changes scroll directions while the view is animating. Finally, we can do some layout optimization by setting the view visibility to GONE once it is no longer on screen.

To fix this, we will add an AnimatorListener to these animations and update the visibility of our view once the animation is complete:

animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {}

@Override
public void onAnimationEnd(Animator animator) {
// Prevent drawing the View after it is gone
view.setVisibility(View.GONE);
}

@Override
public void onAnimationCancel(Animator animator) {
// Canceling a hide should show the view
show(view);
}

@Override
public void onAnimationRepeat(Animator animator) {}
});

This is the animator listener for the hide() method. The show() version is pretty similar.

Using the behavior

We have one last task before our quick return behavior works! We need to associate our Behavior with our quick return view.

The easiest way to do this is with the app:layout_behavior attribute in our layout.

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/quickreturn_background"
android:gravity="center"
android:padding="16dp"
android:layout_gravity="bottom"
android:textColor="@android:color/white"
android:text="QuickReturn Footer"
app:layout_behavior=".QuickReturnFooterBehavior"/>

Wrap Up

CoordinatorLayout Behaviors can do so much more- they are the magic that powers the Material collapsing toolbar, the way FloatingActionButtons automatically move out of the way of Snackbars, and more. I highly recommend becoming familiar with how to use them so that you can begin creating these delightful interactions yourself.

If you want to play around with the Behavior or see the full working code, here is my sample on GitHub.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.