Curved (Cut out) Bottom Navigation With Animation in Android
“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.
- The curved background will translate horizontally based on selected
MenuItem
. - The FAB will hide under the curve when the curve starts animation and then appear once the curved animation finishes.
- The
MenutItem
will also disappear and reappear as the curve moves above it. - 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.
- Get the previous item index and compute the x-offset(menuItem_width * index).
- Get the current selected item index and compute the x-offset.
- 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.
- Get the previous center (x,y) of the FAB (menuItem_width * previous_index+menuItem_width/2)
- Get the new center (x,y) of the FAB (use above formula with new index)
- Animate hiding the FAB for the first half of the curve animation time.
- 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.
- Compute the time it will take for the curve to move over a single
MenuItem
inBottomNavigationView
. - When the curve is animating compute the index of
MenuItem
over which the curve is currently moving. - When the curve leaves the
sourceIcon
then fade in thesourceIcon
for the duration of the entire animation. - When the curve is before
intermediateIcon
start fade out animation and after the curve reaches the end of theintermediateIcon
then start the fade in animation. - Finally, when the curve reaches the starting of the
destinationIcon
then fade out thedestinationIcon
.
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.