Heartbeat AppBar
This is a flutter app that utilizies its animation and painting API to make a very modern, smooth, animation. This was a real challenge to build, specially because it was my first time learning how to use flutter's CustomPainter.
Big thanks to Tubik @dribble for making this awesome design and inspiring me to do this app!
An overview of how the code works
This is a pretty advanced app, so I'll assume you know the basics of flutter.
The first thing I want to talk about is the HeartBeatAppBarContent
widget. As you can see, it's a StatefulWidget
inside of the HeartBeatAppBar
widget, and its state uses the SingleTickerProviderStateMixin
. For those of you who don't know, this is what allows us to animate widgets in flutter. We have one AnimationController
and 3 Animations<double>
:
-
The controller itself is what's going to control the animation values
-
The
_activatingOpacity
will animate the opacity of one of the icons so it becomes brighter (indicating it being activated) -
The
_deactivatingOpacity
will animate the opacity of one of the icons so it becomes dimmer (indicating it being deactivated) -
The
_heartBeatAnimation
is what's going to allow us to control the percentage of the animationg, defining the beginning and ending points of the path of the Painter, so we actually see the line of the heartbeat moving.
We have 3 properties in the HeartBeatAppBarContent's
state.
-
_activated
allows us to keep track of what is the activated icon so that, when we click one of them, we can animate from the _activated to the_nextActivated
-
_nextActivated
allows us to keep track of what's the next icon that is going to be activated so that we can make the heartbeat path from the_activated
X position, to the_nextActivated
X position. Its default value is 0, and its immediately changed when the animation is called -
_positions
is a List of doubles which allows us to know what is the position in the X axis of each icon. This was hardcoded but it can and should be responsive to the width of the device.
When an animation is called,
_nextActivated
gets the value of the clicked button (if the first icon was clicked, then_nextActivated
is set to 1, since the default value is 0). Now, we give the HeartBeatPainter itsbeginning
property of_positions[_activated]
and itsending
property of_positions[_nextActivated]
.In other words, theHeartBeatPath
will start at the position of the activated icon, and will go to the position of the next activated icon. Once the animation ends,_activated
is set to_nextActivated
and _nextActivated is set back to 0.
The CustomPaint
This is where things get a little hairy, but I'll try to keep it very simple. HeartBeatPainter extends CustomPainter
. That means that it needs two methods: paint
and shouldRepaint
. Since we're using an animation coming from its parent widget, we are not going to need shouldRepaint
, so it just returns false.
We have 3 properties:
beginning
is the starting X position of theHeartBeatPath
, and its coming from_positions[_activated]
.ending
is the ending X position of theHeartBeatPath
, and its coming from_positions[_nextActivated]
.animation
is anAnimation<double>
which is what allows us to know how much of the animation has passed. So if its value is 0.5, that means 50% of the animation has gone through, and that's useful to determine what is the current state of the path itself. This will be passed to thesuper
constructor as therepaint
attribute.
Then we are defining 3 functions. lerp
, sigmoid
and parabola
, which are used to determine some of the values. I'm not going to go through the math of this, but this allows us to have smoother animations.
We also have a bool inversed
a List<Offset> _points
. The first one is important to determine if the path will have to go from left to right, or from right to left. The second will contain all of the points that are going to be used to make our heartbeat path. That means _points[0]
is the first point of the path, and the last value of _points
is last point of the path.
In _points
, we hard coded a bunch of values:
- The ones that start with
beginning + a * PERCENTAGE
allows us to make a lot of points in between the beginning of the path, and the heartbeat wave (which is always in the middle). If we didn't have this, the path would be "skippy", and the animation would not look smooth at all. - The same goes for the ones that start with
startingPoint + 30 + b * PERCENTAGE
. This makes a bunch of points in between the ending of the heartbeat wave, and the ending point of the whole path. - All of the other points are the heartbeat wave being hard coded
startingPoint
is the X position of the beginning of the heartbeat wave.
As you can see from the code,
_positions
changes depending on the_inversed
value, and therefore, on the direction of the heartbeat animation.
Now here comes the main part. We have a variable start
that indicates what is the first point of our path, and this changes depending on the _animation.value
. Hence, if _animation.value
is closer to 1, the start
value will be greater, which makes the back of the path start to move when the animation starts progressing. We also have a percentage
variable, which is what controls the front of the path, since this will define the last point of our path.
From there, we are calling a for loop, that goes from the start
point (first), to percentage
point (last), and adds all of the points in between to the path.
The color of the path (which is a gradient) and the circle (which is the indicator of the current icon) also animate and get its properties depending on the
inverse
value.