Building Animated Buttons in Flutter with Lottie
Table of contents
In mobile app development, user interface elements play a crucial role in enhancing the user experience. Companies like Apple, Spotify, and Airbnb pay a lot of attention to small details that elevate the user experience to another level.
One of those details is animations.
Subtle, slick, and polished animations create an unmatching user experience.
There are many ways to add animations to your Flutter app, but some are more easier to do then the other. You will get the mock bang for your buck by using:
Navigation transition animations
animations package
Animated buttons
There are two libraries that can help you create smoothly animated buttons in Flutter: Lottie and Rive.
The focus of this tutorial will be to show you how to create an animated record video button using a Lottie animation.
Here’s what are are going to build:
Let’s dive in!
Animation and the key frames
First, we have to find the animation that we are going to use. The best place to find free Lottie animations is LottieFiles.
For this tutorial, we are going to use this nice Record/Stop Button animation by Chris Gannon.
What I really like about LottieFiles is that it features an editor when you create an account. Inside this editor, you can play the animation, change it, and play around with it.
When you open the editor, it will look something like this:
The part we are interested in is this bottom bar in the purple outline. Here you can control the playing of the video and see the frame counter, which is the most important information for controlling the animation.
When you play the animation a couple of times you can see there are 3 parts to it:
Intro (frames 0-60)
Start playing (frames 60-180)
Stop playing (frames 180-241)
We can use the starting times of the animation to control at which moments the animation needs to be started, stopped, or reset.
Let’s get to coding.
Coding
To start off, we need to add Lottie to the pubspec.yaml
:
dependencies:
lottie: ^3.1.0
We will also need two animation checkpoints:
const _introAnimationEnd = 60 / 240;
const _lastStopIconAnimationEnd = 180 / 240;
Let’s define our button:
class RecordButton extends StatefulWidget {
const RecordButton({
Key? key,
required this.onStartRecording,
required this.onStopRecording,
this.size = 200,
}) : super(key: key);
final VoidCallback? onStartRecording;
final VoidCallback? onStopRecording;
final double size;
@override
State<RecordButton> createState() => _RecordButtonState();
}
class _RecordButtonState extends State<RecordButton> with SingleTickerProviderStateMixin {
bool isRecording = false;
late final AnimationController _controller;
@override
void initState() {
_controller = AnimationController(vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Placeholder();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
The button has 2 callbacks, onStartRecording
and onStopRecording
, and an optional size property to change the size of the button. It needs to be a StatefulWidget
to support the AnimationController
.
The button has 3 states:
initial state
ready to record state
recording state
Each of these states corresponds to one of the animation parts mentioned previously.
First, the button enters the initial state in the onLoaded()
method of the Lottie widget and plays the intro animation using the controller:
Lottie.network(
"<https://assets.codepen.io/35984/record_button.json>",
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
_controller.animateTo(_introAnimationEnd);
},
The button is now ready to start recording. When the user presses the button it will enter the recording state and play the record state animation. During the recording state, the user can press the button again to stop the recording and play the animation to the end.
return GestureDetector(
onTap: () {
setState(() {
isRecording = !isRecording;
});
if (isRecording) {
widget.onStartRecording?.call();
_controller.animateTo(_lastStopIconAnimationEnd);
} else {
widget.onStopRecording?.call();
_controller.animateTo(1);
}
},
There is one last case we need to take care of. When the user presses the button to stop the recording, and animation will go to the end. The next press would start the recording, but the animation would not play because it was already at the end. To fix this, we need to add the listener to reset the animation to the correct frame (_introAnimationEnd
) in the onLoaded()
method:
onLoaded: (composition) {
_controller.addListener(() {
if (_controller.value == 1) {
_controller.value = _introAnimationEnd;
}
});
},
The final result looks like this:
const _introAnimationEnd = 60 / 240;
const _lastStopIconAnimationEnd = 180 / 240;
class RecordButton extends StatefulWidget {
const RecordButton({
Key? key,
required this.onStartRecording,
required this.onStopRecording,
this.size = 200,
}) : super(key: key);
final VoidCallback? onStartRecording;
final VoidCallback? onStopRecording;
final double size;
@override
State<RecordButton> createState() => _RecordButtonState();
}
class _RecordButtonState extends State<RecordButton> with SingleTickerProviderStateMixin {
bool isRecording = false;
late final AnimationController _controller;
@override
void initState() {
_controller = AnimationController(vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
isRecording = !isRecording;
});
if (isRecording) {
widget.onStartRecording?.call();
_controller.animateTo(_lastStopIconAnimationEnd);
} else {
widget.onStopRecording?.call();
_controller.animateTo(1);
}
},
child: Lottie.network(
"<https://assets.codepen.io/35984/record_button.json>",
controller: _controller,
width: widget.size,
height: widget.size,
fit: BoxFit.fill,
repeat: false,
onLoaded: (composition) {
_controller.duration = composition.duration;
_controller.animateTo(_introAnimationEnd);
_controller.addListener(() {
if (_controller.value == 1) {
_controller.value = _introAnimationEnd;
}
});
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
You can also find the full code as a gist on my Github.
Conclusion
In this tutorial, you have learned how to find a nice Lottie animation, determine the key frames, and create a button that uses it. Hopefully, you can use this to bring your user experience to the next level with some slick animations.
If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter or LinkedIn.
Until next time, happy coding!
Subscribe to my newsletter
Read articles from Dinko Marinac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dinko Marinac
Dinko Marinac
Mobile app developer and consultant. CEO @ MOBILAPP Solutions. Passionate about the Dart & Flutter ecosystem.