Flutter — Bottom Tab Bar animation
Another Dribbble design I found here. This bottom bar concept, I think, looks good, and had a few subtle things going on that I thought might be fun to make in Flutter.
TL;DR .. here’s the code on GitHub.
As with my other dribble challenge, and generally the way I work, I want to get the basics done first and then refine.
So I wanted a bottom bar with three icons evenly spaced. Easily achieved with a
Row , and 3
Expanded widgets, containing
IconButton widgets. To make the Row white, I wrapped it in a
Container with a
BoxDecoration coloured white.
Floating Action Button (ish)
The next step was to get what looks like a floating action button to sit on top of the Row. So for that I used a
Stack widget, as the FAB is not clickable at first I chose to use a
Card shaped as a circle, with a white border, and an
To get the circle to sit center aligned to the top of the tab bar, I had to decide on a height for the tab bar, add a
Container and add some top margin to it (half the circle height). The
Stack alignment was set to top center, which worked. (check the code fancy_tab_bar.dart line 70).
Animating the circle
First things first, how do I get the circle to line up with the icons. First I thought put it in a row, but then how to animate it? Start measuring the screen size, then place using Positioned (that could work).
Then I discovered
FractionallySizedBox which takes
widthFactor attributes. So by wrapping the circle in a
FractionallySizedBox with a
1/3 I got a widget the same size as one of the Row items.
But, its still in the center. So wrap that in an
Align widget, and we can align it left, center, right and its in the correct place.
To animate, we’re not bothered about the Y value of the Align widget, but we do need to move the X.
- -1 = left
- 0 = center
- 1 = right
Now we can use an
Animation<double> and a
Tween<double> to change the x value, and use it in the
Click and move
As I’m relatively new to Flutter animations, the only animations I had used before were pre-determined, eg Animate from 0 to 100. But now I need to animate from the circle position to a new position.
This took me longer than it should maybe, but thats what happens when learning a new framework.
What I discovered was that the values of a
Tween can be changed after its creation. I separated the
Tween objects. Now when an Icon was pressed I could set the
Tween begin and end values to the current position of the circle, and its desired final position.
One gotcha here, you have to reset the
AnimationController to be able to call
forward() on it again.
Fade, move, appear
The circle didn’t just move. It moves and at the same time fades out the icon, then when nearing the final destination a new Icon fades back in.
The timeline is:
Start Move → Fade Out →️ Change Icon → FadeIn → Stop Move
The Move is taken care of. To fade out, I first of all thought to use the same
AnimationController but for the fade out use an
Interval which extends
Curve that way I could set the start to be 0 and the end to be say 0.2. The problem I had was, I need to listen to the end of the animation, and change the icon whilst invisible. Listening to the end of an
Interval animation, the completed event isn’t fired until the entire animation has completed. So I used a new
AnimationController wrapped the
Icon in an
Opacity widget and animated the opacity attribute, then once complete change the
initState for more detail).
To fade back in. This time I could use an
Interval begin 0.8, end 1. This was attached to the position animation controller, and all was good.
Tab bar item animation
When clicked, the circle moves across to that tab. But at the same time, the tab icon moves up and fades, and the tab title animates in from the bottom.
Up to this point I had been hacking everything in one file. It was getting difficult to see what was going on. So the
TabItem was made into a
StatefulWidget (see tab_item.dart).
I needed two widgets
IconButton and they both needed to move. After a fair amount of playing, I found the best way was to use a
Stack , and two
AnimatedAlign widgets. The
AnimatedAlign widget will automatically animated the align property (probably why they called it that), and has a duration attribute for the animation duration.
This time we only care about the Y value. animating the
IconButton to the very top of that
Container , and the
Text in from outside to the bottom of the container, and of course reverse that when leaving the tab.
Now when the
selected property of the
TabItem is changed, I kick of the change in values and the
AnimatedAlign takes care of the rest.
One other thing happens, the Icon fades. So I wrapped the
IconButton in an
AnimatedOpacity widget (you can guess what that does).
IconButton animates up and fades, and the
Text widget animates in from underneath the screen.
We’re looking good
At this point everything is working as expected, but there are a few of things left. One was that the shape border I was using was leaving a tiny grey border inside the tab bar. Not bad, but not great. Another was that I had no shadow above the tab bar (and circle), and the last the circle joins the bar with rounded corners. Time to change a few things…
All this happens in fancy_tab_bar.dart line 128 onwards.
Shadow: easy, lets just add a
BoxShadow to the tab bar and circle. This worked fine for the tab bar, but the circle cast a shadow onto the tab bar. I’ll come back to this…
Rounded corners to tab bar: I needed to separate the purple circle away from the white outer circle. So moved to a
Stack , at first I added a white circle, and positioned the purple circle inside. Now instead of painting a circle I needed to paint something that starts from flat, into a semi-circle, then back to flat if that makes any sense at all 🙄.
To do this I used a
CustomPainter this is called
HalfPainter and is at the bottom of the fancy_tab_bar.dart file. It creates a path, and then draws it to the canvas. Now it looks correct, like the circle is part of the tab bar.
Now the circle join is resolved, and then weird faint border is resolved. The last is the *%&%* shadow!
Back to the shadow: I tried to add a shadow to the canvas of the
CustomPainter but failed. Then I added another circle underneath the white circle to add a shadow from that, but the shadow still cast onto the tab bar.
But this was closer, I just needed to stop the shadow casting downwards. What I landed on was wrapping this shadow casting circle in a
ClipRect widget. Now I could clip half of the circle, it just needed space at the top and sides to allow the shadow to cast (see line 134 of fancy_tab_bar.dart).
All working as in the design, with a little refactoring this could easily be used as a working tab bar I think.
I learned quite a bit from this, more
Widgets to remember and use, and more animation details.