DEV Community

Cover image for Building Animated Buttons in Flutter with Lottie
Dinko Marinac
Dinko Marinac

Posted on

Building Animated Buttons in Flutter with Lottie

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:

Animated Button in Flutter created with Lottie

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 open the editor, it will look something like this:

LottieFiles Editor

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:

  1. Intro (frames 0-60)

  2. Start playing (frames 60-180)

  3. Stop playing (frame 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
Enter fullscreen mode Exit fullscreen mode

We will also need two animation checkpoints:

const _introAnimationEnd = 60 / 240;
const _lastStopIconAnimationEnd = 180 / 240;
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
        },
Enter fullscreen mode Exit fullscreen mode

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);
        }
      },
Enter fullscreen mode Exit fullscreen mode

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;
            }
          });
        },
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Reposted from my blog.

Top comments (0)