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.
The goal
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 LayoutBuilder
during build
method. The animation we create will become our source of widget’s width instead of width
field.
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 TransitionDot
.
Let’s break it down step by step:
- We are extending
AnimatedWidget
. It means that instead of wrapping wholeWidget
inAnimatedBuilder
, it will be rebuilt every time the animation from constructor changes its value. - We create a new
dot
widget. 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 fromInputPage
but withoutAppBar
and with different body. - So far let’s just have a
Column
with 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 Column
. 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 Animatable
.
As you can see, we used sin
function to handle changing the size back and forward. Our animation will go from defaultSize
to 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:
Fade transition
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.
If you liked the whole series, leave a star on the Github repository where you can find all the code, I would appreciate it. 🙂
Cheers 🙂
Originally published at marcinszalek.pl on November 14, 2018.