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.

Basics

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 Icon child.

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 heightFactor and/or widthFactor attributes. So by wrapping the circle in a FractionallySizedBox with a widthFactor of 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 Align widget.

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 Animation and 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 IconData(see fancy_tab_bar.dart 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 Text and 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).

Now the 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…

Polish

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).

Done

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store