A custom behavior — surprisingly easy

I knew the implementation would involve writing a custom CoordinatorLayout Behavior which is what threw me off initially. Anytime I’ve delved into the example code of custom behaviors it seemed way too complicated. It turns out, though, that in this case it was not that hard at all:

And the result? Voilà:

I’d call that on par with what we are aiming for

The first important part of the code is the onStartNestedScroll method where we instruct CoordinatorLayout that we care about vertical scroll events. Then we handle onNestedPreScroll . This is the same method that the AppBarLayout behavior overrides to hide itself away. “PreScroll” means that we get the scroll event before the RecyclerView (the actual scrolling container in this case) receives it. The only part of it we care about is the dy which is the scroll change delta. Then all we have to do is set the translationY property of our view. max(0f, min(child.height.toFloat(), child.translationY + dy)) is a clamping function. It clamps the translationY value between two bounds — 0 and the view height — because we don’t want to push the bottom bar further down than its own height and we don’t want to push it up further than its starting position.

Attaching the behaviour to your view

If you’ve never used a custom CoordinatorLayout behavior the way you attach it to your view is like this — just put in layout_behavour the path to your own class:

What about Snackbars?

What we have is great but it has a flaw — it doesn’t respect Snackbars appearing in the CoordinatorLayout. If one was to appear it’ll be behind the BottomNavigationView which is wrong. The material specs are clear on what should happen:

Snackbars should appear above the Bottom bar

When a snackbar appears it needs to be docked above the bottom bar and move with it as it moves. Well as it turns out doing that isn’t too complicated too (notice a pattern here?):

Hey presto — we have Snackbar support:

The way this works is that we’re tapping into the layoutDependsOn method which fires off when a new layout event happens — as a result of a Snackbar being added, for example. Since we know the Snackbar is being added to a CoordinatorLayout we can make use of that and change its layout parameters. Making use of the very handy anchor , anchorGravity and gravity properties we instruct the Snackbar to be anchored to the top edge of the bottom bar (remember, child in that code is the BottomNavigationView the behavior is attached to) and it should appear with gravity top, meaning above the bar.

What about Floating Action Buttons (FAB)?

Another element to handle with this BottomNavigationView implementation is Floating Action Buttons.

First off, to make the FAB move with the BottomNavigationView we can do what we do with the Snackbar — just anchor it to it!

If you look at the code you’ll see it’s the same thing we do with the Snackbar in BottomNavigationBehaviour but just declared in XML. Our FAB now moves correctly:

But right now if we show our snackbar, it’ll appear over the FAB, which isn’t nice. What we want is for the FAB to move up when a Snackbar appears and move back down when it disappears, as it does by default in normal layouts.

We can do that with another simple custom CoordinatorLayout Behavior:

We have our desired result:

BottomNavigationFABBehavior isn’t very complicated but lets break down what it does.

First off, similar to the other Behavior this one declares that it depends on a Snackbar. This allows it to get a callback in onDependentViewChanged when the Snackbar appears and moves. Now, in order to get a smooth movement transition the FAB needs to move up together with the Snackbar. The way Snackbars are animated in is by translating them up by their entire height. This means that at any one time the difference between the snackbar’s translationY property and its height is the amount of it that is showing from the bottom of the screen. This amount is how much we have to translate the FAB by so that it’s not covered by the Snackbar.

We compare oldTranslation and newTranslation so that we can return a correct response to onDependentViewChanged which expects Behaviors to return true or false depending on whether Behavior changed the child view’s size or position. Not doing this doesn’t have any visible negative consequences but I think it’s better to be correct 👍

The final thing to notice here is the overriden onDependentViewRemoved method — this is used so that if the snackbar is manually dismissed (via swipe) the FAB can return to it’s old position.

What about snap behaviour?

By popular demand I’ve decided to include how to extend the BottomNavigationBehavior to handle snapping. By snapping I mean a behavior similar to what you get when you add app:layout_scrollFlags=”snap” to your AppBarLayout views — for example to your Toolbar. It makes it so the Toolbar snaps to either be hidden or visible when the user stops scrolling.

Since we want our BottomNavigationView to handle snapping like the AppBar does, it’s worth to look at the AppBarLayout source code to see how it handles snapping. Instead of showing it, I’ll summarize it:

It waits for the onStopNestedScroll event. When it happens it checks if any of it’s children have a snap behavior attached. If so, it checks if the child should be snapped to full visibility or to a hidden state. It does this by checking how much of the child is currently showing. It then animates the child’s offset to that new state.

The actual implementation is obviously slightly more complex but those 3 steps are the essence of it.

Ok, lets follow these steps and edit our code to support snapping:

Oh, snap, we have it:

Looks great but we’ve changed quite a bit haven’t we? Lets take it one change at a time to see how we got it working. The first thing you’ll notice is that we keep a track of lastStartedType in onStartNestedScroll in order to make sure we can indeed run our snap behavior in onStopNestedScroll . This code is borrowed directly from the AppBarLayout source.

Then in onStopNestedScroll we first check whether we support snapping and can run our logic. If we can than the first thing we do is check whether we should hide or show the view. We do this really easily by just checking whether the current translationY is greater or less than the half height of the view.

Onwards to our animateBarVisibility method. Again, this is inspired by the AppBarLayout source. We first instantiate our offsetAnimator if we’ve not already. It’s a really simple ValueAnimator object that will animate the translationY property of our view. The duration of 150 milliseconds and the DecelarateInterpolator are the same as the ones that AppBarLayout uses. This keeps our snap behaviour in sync with the AppBar.