Hello there! I’m back with a new post about implementing Johny Vino‘s BMI Calculator design. If you are not familiar with previous posts, I recommend checking them out. This time we will focus on an animation between the input screen and the result screen. We will change the shape, position and size of a Widget using only one AnimationController. Let’s see how it goes.
Since we started working on this project, the design changed a lot, therefore the transition animation also changed. However, for the educational purpose, I think it will be much better (and more fun!) if we adapt old animation to the new design. Since the animation is fairly complicated and we already have some advanced stuff here (especially in PacmanSlider), I will be skipping some of the code changes that are only relevant for this project and I will try to focus on general approaches I take to create that animation. If you are interested in the whole code, check out Github repository.
Coming back to work, this is the behavior we would like to achieve:
And this is what we have so far:
Setting up AnimationController
In order to work with animations, we need an
AnimationController, here we will use only one named
_submitAnimationController. This controller will have 2-second duration and it will get started when the user submits the PacMan slider. So far it makes on effect on the app.
Animating slider’s border
We will start our transition animation with changing slider’s borders to be more rounded. To do that, we will use
BorderRadiusTween which will help us animating from one BorderRadius to another. First, let me show the code and then explain it.
The first thing we need to do is pass
AnimationController to the
PacmanSlider widget, it can be easily done by just adding a new argument to the constructor. Then, we need to create an
Animation. As I said earlier, we can use
BorderRadiusTween for that. All we need to do is specify starting and ending radius and Flutter will take care of the rest. We also do one more thing with the Animation, we specify
Interval curve, which means that our border animation will not be run during whole 2 seconds of
AnimationController but only from start to 7% of those 2 seconds. Then we will wrap our widget in
AnimatedBuilder which will take care of rebuilding the slider everytime
AnimationController changes its value. After that we just need to use the value from
_borderAnimation and see the result:
Shrinking the slider down
Great, now let’s try to shrink that slider so that it has the shape of a circle. Let’s start by creating new
Animation that will be responsible for animating widget’s width. The difference between this animation and the border one is that we cannot initialize it in
initState method, because we want to animate slider’s width obtained from
build method. The animation we create will become our source of widget’s width instead of
Theoretically, it already could work, however, inside the
GestureDetector there is a lot of stuff that doesn’t go well with resizing that widget, so we will do a little hack and replace current content of slider with an empty container if the animation is animating. Thanks to that we won’t have to bother on how Pacman icon or the dots behave when sliders get too narrow. We will use animation’s
isDismissed property which tells us if an animation is stopped at the beginning.
Now we can see our slider shrinking down to the shape of a circle:
Moving the dot
We got the slider animation done, now we can work on the input page. We would like to move the dot we got from the slider to the middle of the screen. To achieve that we will create a Widget that will cover the whole Scaffold after the slider finishes its animation. We will start by wrapping the Scaffold in Stack.
Notice that we placed
TransitionDot after the
Scaffold. This way it can cover the default Scaffold. Now let’s take a look at the
Let’s break it down step by step:
- We are extending
AnimatedWidget. It means that instead of wrapping whole
AnimatedBuilder, it will be rebuilt every time the animation from constructor changes its value.
- We create a new
dotwidget. It will take place of old slider, the idea is to make it appear in the same position the slider ended up after shrinking down.
- We wrap the whole widget inside
IgnorePointer, so that it can cover the whole screen and not interact with all the controls.
- We specify the widget’s opacity. If animation is below 0.15 (point where slider animation ends), opacity is set to 0, so we cannot see the widget, otherwise we make it visible.
- Our actual widget is a
Scaffold, same as the one from
AppBarand with different body.
- So far let’s just have a
Columnwith a dot as a body.
The effect looks like this:
Well, not quite impressive, right? Now we can actually animate the position of the dot. Not sure if my idea of how to do it is good, but it works so we will stick with it. So, what we want to do is surround the dot with
Spacers inside the
Spacer is an empty widget that takes as much space as possible, it also has a
flex attribute that can specify the relation of how much space of the whole container multiple spacers should take. The goal is to start with a spacer with high flex on top and very low flex in the bottom. Then, if we change the value of flexes to be equal, the dot will be between two equal spacers so it will be in the middle. To change the flex values we will use
IntTween which will increase int values from 0 to 50.
Those changes cause such result:
Animating dot’s size
Once we get the dot in the middle, we can focus on animating its size. We will create another animation that will be responsible for that, but first, we need to deal with the main problem which is that our dot is supposed to be expanded and shrunk multiple times. Usually, in scenarios like this, we add a listener to the animation and repeat it once it’s done. However, since we are having an
Interval animaiton I can’t see how it could work. So how are we going to deal with that? We are going to create our own
As you can see, we used
sin function to handle changing the size back and forward. Our animation will go from
defaultSize + expansionRange to
defaultSize - expansionRange and then come back to
deafultSize 2 times. Let’s use it 🙂
So we use the
LoopedSizeAnimation in a combination with
Interval. That way our dot will change its size after it got moved to the center. It should look like this:
Expanding the dot
Awesome, now we can modify that animation so that in the end it will cover the whole screen. To do that, we are going to mark a point in time, after which the dot stops pulsing and starts expanding.
What happens here is that if the time of the animation is below 80%, it will be pulsing. After that, it starts to rapidly grow from default 52 logical pixels to 2000 which should cover the whole screen. Now we need to add small changes in using the animation:
We changed two things:
- We make sure that the height and width are smaller than the screen.
- We make sure that dot changes shape to rectangle when it gets close to the edge. Otherwise, it wouldn’t be able to cover the whole screen.
Let’s look at the result:
The only thing left is navigating to the new page. We will create a very simple
Route so that the transition from the blue screen to the next one will be smooth.
Now let’s use this
FadeRoute to navigate to
ResultPage. I won’t describe how this page is created since there is nothing interested there. If you are interested, you can check it out here. To navigate to the new page, we will simply add a listener to
_submitAnimationController and call
Navigator once the controller is completed.
Alright, let’s see the final result!
And that’s it! I hope you liked this post, if there is anything unclear or if you think I could explain or implement something better, please leave a comment. 🙂
I expect it to be the last post of the BMI Calculator since the design part is mostly done. I really enjoyed doing this app and sharing the process with you. I hoped you enjoyed it too.