Curved (Cut out) Bottom Navigation With Animation in Android

Suson Thapa
The Startup
Published in
9 min readOct 8, 2020
Photo by Natalya Letunova on Unsplash

“Requirement is the mother of innovation”

I recently had a requirement to implement a curved bottom navigation with animation inspired by this pinterest post.

https://www.pinterest.com/pin/648729521319201054/

Here is the final demo of what we are going to build

Well that look’s great but.

That’s the comment in the post and I literally felt the same way 😅.

As usual if you are in a hurry, here is the link to the github repository. Just read the README and you are good to go. If you want to dive deeper keep reading (there is also a bonus section at the end 😃).

As a good developer, the first thing that I did was to look for some library(😅). If someone has already done that we don’t need to reinvent the wheel. After my quick research I found library for both flutter and react but was unable to find a library for Android (more on that later).

I had previously used cut out with BottomNavigationView and FloatingActionButton(FAB) by using a CoordinatorLayout. But the requirement is far more than that. Along with the cut out we also have to implement the animation both for the BottomNavigationView and the MenuItem itself.

Talks aside, let’s dive in. I came across this medium post that describes drawing custom shapes in the BottomNavigationView. After reading it I got fairly good idea on how to draw shapes with bezier curves. Just quickly opened my OpenGl book to revise how bezier curve works and off to races 🐎.

Here I describe two approaches that I have taken during my development.

Extending Bottom Navigation View

Let’s try to reuse the existing BottomNavigationView by extending it and see if we can customize it to our needs. Here is the class with some variables to keep track of the cut out.

I guess the comments are self explanatory. First we get the FAB radius along with the offset for both control points. We will be creating two cubic bezier curve to implement the two halves of the curve. It is also super important to set the background to Transparent as we will draw our own background. Here is a picture of what I meant. (Sorry for reusing your picture I was too lazy to create such a nice one 😄)

The control point C1 is at offset (TOP_CONTROL_X, TOP_CONTROL_Y) from P2 and so is the control point C3 from P4. Also, the control point C2 is at offset (BOTTOM_CONTROL_X, BOTTOM_CONTROL_Y) from P3. You get the point right!

Now we will create the actual curve in onSizeChanged() function. Then draw the curve in onDraw() function.

I think the comments in the above code snippet are self explanatory. This is how it looks.

After some hit and trail I realized that it would be quite difficult to implement the animations specified in the requirements with the BottomNavigationView and also the cut out will interfere with the icons. It’s time we get our hands dirty 👷.

Starting from scratch

Just a quick look through the source code of BottomNavigationView gave a rough ideas of things that I need to implement. Here is the basic layout of what we will be implementing.

So, basically we will use FrameLayout as our root layout. Then a LinearLayout will act as the BottomNavigationView with each item being an ImageView with weight assigned to 1 to equally distribute the space. We will reuse the code from onSizeChanged() described above to compute the curved path. We will implement the FAB with a simple circle.

It’s all about the Animations!

We will be using four types of animations.

  1. The curved background will translate horizontally based on selected MenuItem.
  2. The FAB will hide under the curve when the curve starts animation and then appear once the curved animation finishes.
  3. The MenutItem will also disappear and reappear as the curve moves above it.
  4. The icon in the FAB will animate when the FAB shows up at the end of the animation.

Bezier curve animation

Here is the basic algorithm to implement the curve animation.

  1. Get the previous item index and compute the x-offset(menuItem_width * index).
  2. Get the current selected item index and compute the x-offset.
  3. Animate between the start and end x-offset.

This is how it looks.

FAB Animation

The algorithm to implement FAB animation is quite involved but I will try to clear that out. When the user clicks the icon, animate the curve to that position using above code along with the FAB center so that it is in sync with the curve.

  1. Get the previous center (x,y) of the FAB (menuItem_width * previous_index+menuItem_width/2)
  2. Get the new center (x,y) of the FAB (use above formula with new index)
  3. Animate hiding the FAB for the first half of the curve animation time.
  4. Animate showing the FAB for the remaining half of the curve animation time.

Then we need to play all these animations to get the nice effect.

Here is how it looks(in slow motion).

Bottom Navigation Icons Animations

This is one of the complex animations and took me quite a while to figure it out. Here is the algorithm for that but first some naming.

sourceIcon: The previously selected cell.

destinationIcon: The currently selected cell.

intermediateIcon: All icons in between source and destination icons.

  1. Compute the time it will take for the curve to move over a single MenuItem in BottomNavigationView.
  2. When the curve is animating compute the index of MenuItem over which the curve is currently moving.
  3. When the curve leaves the sourceIcon then fade in the sourceIcon for the duration of the entire animation.
  4. When the curve is before intermediateIcon start fade out animation and after the curve reaches the end of the intermediateIcon then start the fade in animation.
  5. Finally, when the curve reaches the starting of the destinationIcon then fade out the destinationIcon.

We need to change the updateListener of the bezier curve animation to trigger the FAB animation.

Here is how it looks in slow motion.

FAB icon animation

This is the easy part as we will be using AnimatedVectorDrawable(AVD) for the icon animation. This was also the most frustrating part for me during the development. I used shape shifter tool found here to create the AVD.

After many hours of trying different svg icons along with some manual code injection in the svg I finally got some satisfactory results. The AVDs that I created are quite simple so your mileage may vary depending on your designer (if you have one).

When we show the FAB we also need to start the AVD animation and that’s it. We now have a pretty good looking Curved Bottom Navigation.

With this I think we are quite done. Here is the final part again in slow motion.

Of course I have overly simplified the code here for the sake of demonstration. There are many edge cases that are handled in the actual code so I recommend looking through it. I also recommend looking through each commit as it shows the way the code has evolved over time along with some stupid mistakes that I had made 😅.

Also I found a library here for CurvedBottomNavigation (after I had implemented mine 😛) but it has a different curve compared to my requirement. I found this library from here which is a great site for any Android Developer.

That’s it for this story. Here is the link to the github repository again (if you are not feeling well to scroll all the way to the top 😅) . If you find any issues I hope with this detailed explanation you would be able to submit a PR 😉.

Bonus

Important! Not For FaintHearted, You have been warned ⚠️ ⚠️ ⚠️ ⚠️ ⚠️

This is sort of some extra information for those looking forward to working with AVD and rendering it in custom views.

One of the major issues that I was having is with the hardware accelerated rendering and shadows. I had a problem where the AVD below API 25 would just stuck halfway through the animation(i.e once the curve animation was finished the AVD would just stuck there not continuing it’s animation).

But let’s have some context before diving into the problem.

Android provides both hardware (LAYER_TYPE_HARDWARE) and software(LAYER_TYPE_SOFTWARE) based rendering. Why do we need to care?

Glad you asked. It’s because hardware accelerated rendering doesn’t support all types of drawing operations and guess what it’s enabled by default 😫 starting with API 14. Here is the link to android developer page that explains hardware acceleration. One of the things to note if you are drawing any shadows in canvas is that ShadowLayer support is only available starting from API 28 when using hardware acceleration. So, if you are using hardware acceleration then applying ShadowLayer won’t render shadows before API 28, Great!.

Ignoring the shadow issue the other problem is with RenderThread. Referring to this documentation AVD will use RenderThread starting with API 25. So the next obvious thing for me was to check the app below API 25. As expected it didn’t render the AVD. The AVD would stop animating (stuck) as soon as the curves animation is complete. Desperate to fix the issue I switched to software rendering and to my great surprise the AVD animation is now stuck above API 24 as well(before it only happened before API 25).

Great, we now have to solve the problem for the entire API range.

From my initial investigation I found the issue to be related with view not invalidating. As soon as the curve animation finishes we stop calling invalidate which I guess also stops the AVD animation when using software rendering (But the same thing happens when using hardware rendering below API 25, maybe due to AVD not using RenderThread 😕 😕).

I tried different approach like increasing the curves animation time to cover the AVD animation time (it worked but the curve would take forever to animate and I also had to set a hard limit on AVD duration).

Another approach I thought was to run a dummy animator that would tick for the AVD animation period for which I would then call invalidate(). It worked mostly but during startup the dummy animator would go out of sync and leave the AVD path at around 80% complete.

Finally, looking into the Drawable reference I saw a Callback that would call invalidateDrawable() function when it’s time to redraw it. I can call my own invalidate() within that function.

It worked but after sometime it just show previous behavior (AVD stuck). After scratching my head for hours I just thought to check the Callback from the Drawable to see if it’s still there. And Oh man, it wasn’t there 😮.

So, looking at source code of Drawable, we can see it’s using WeakReference.

I think this might be caused by the Callback (which was implemented as a lambda) being Garbage Collected(GC). So, I set the Callback before I start the animation and it worked like a charm.

--

--

Suson Thapa
The Startup

Android | iOS | Flutter | ReactNative — Passionate Software Engineer