The power of animations in Flutter

The power of animations in Flutter
Photo by Daniel Klein / Unsplash

In today's digital landscape, users expect software with top-notch user interfaces. The reason for this is that a first-rate UI can keep users engaged, even if the backend is slow. However, we can do even more to enhance user satisfaction.

That's where animations come in as a crucial component to ensure seamless operation. If something jumps or changes properties, like colour or brightness, without any accompanying animation during interaction, it can harm the user experience, despite its great visual appearance.

Flutter has a range of pre-built features, such as AnimatedSwitcher, AnimatedSize, AnimatedOpacity, and many more. For more sophisticated animations, there are various transitions, tweens, and animated controllers that provide a solid foundation for any project.


For this article, I have created a small program with a few simple widgets, which I will use as a demonstration - https://gist.github.com/hawkkiller/f57c79126a12e0921591b851cc99cba1.

0:00
/0:13

This appears nice, doesn't it? I like these interfaces. However, we can enhance a few elements:

  1. The colours change without animation.
  2. When you click the "Submit" button, it becomes inaccessible and just grey.

Color animation

Currently, the code for SentimentItem looks like this:

  Widget build(BuildContext context) {
    final isActive = activeText == text;
    return Column(
      children: [
        IconButton.filled(
          style: IconButton.styleFrom(
            backgroundColor: isActive
                ? Theme.of(context).colorScheme.primary
                : Theme.of(context).colorScheme.surfaceVariant,
          ),
          onPressed: () => onTap(text),
          icon: Icon(
            icon,
            color: Theme.of(context).colorScheme.onPrimary,
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            text,
            style: GoogleFonts.montserrat(
              fontSize: 12,
              fontWeight: FontWeight.w500,
              color: Theme.of(context).colorScheme.onSurface,
            ),
          ),
        ),
      ],
    );
  }

What I aim to do is create a stylish animation that transitions between two colours - primary and surfaceVariant. This task can be accomplished with the use of AnimationController and ColorTween.

  late final AnimationController _controller;
  late Animation<Color?> _colorAnimation;

  @override
  void initState() {
    _controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: this,
      value: widget.activeText == widget.text ? 1 : 0,
    );
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    _colorAnimation = ColorTween(
      begin: Theme.of(context).colorScheme.surfaceVariant,
      end: Theme.of(context).colorScheme.primary,
    ).animate(_controller);
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(covariant _SentimentItem oldWidget) {
    if (widget.activeText != oldWidget.activeText) {
      if (widget.activeText == widget.text) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: Column(
        children: [
          AnimatedBuilder(
            animation: _colorAnimation,
            child: Icon(
              widget.icon,
              color: Theme.of(context).colorScheme.onPrimary,
            ),
            builder: (context, icon) {
              return IconButton.filled(
                style: IconButton.styleFrom(
                  backgroundColor: _colorAnimation.value,
                ),
                onPressed: () => widget.onTap(widget.text),
                icon: icon!,
              );
            },
          ),
          Padding(
            padding: const EdgeInsets.only(top: 8),
            child: Text(
              widget.text,
              style: GoogleFonts.montserrat(
                fontSize: 12,
                fontWeight: FontWeight.w500,
                color: Theme.of(context).colorScheme.onSurface,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
0:00
/0:05

Improved version:

Looks better. Some important points from the code:

  1. See how I'm utilising the child function in AnimatedBuilder to solely reassemble the IconButton widget, without remaking the Icon each time.
  2. I create a ColorTween in the didChangeDependencies method so that the colors can be changed when there is a change in the theme.
  3. I implement Repaint Boundary in every item to restrict the painted area as these widgets require frequent repainting (refer to my article on Repaint Boundaries).

Filled button animation

Currently, the submit button becomes inaccessible when it's being processed. It would be better to disable the button without changing its color. Additionally, I suggest changing the icon on the activity indicator while the submission is in progress.

Now, it looks better. See:

0:00
/0:16

The code is simple as a pie:

RepaintBoundary(
  child: IgnorePointer(
    ignoring: inProgress,
    child: FilledButton.icon(
      icon: SizedBox.square(
        dimension: 24,
        child: AnimatedSwitcher(
          duration: const Duration(milliseconds: 200),
          child: Visibility(
            key: ValueKey(inProgress),
            visible: !inProgress,
            replacement: _AdaptiveIndicator(
              color: Theme.of(context).colorScheme.onPrimary,
            ),
            child: Icon(
              Icons.send_rounded,
              color: Theme.of(context).colorScheme.onPrimary,
            ),
          ),
        ),
      ),
      onPressed: _submit,
      label: Text(
        'Submit',
        style: GoogleFonts.montserrat(
          fontSize: 14,
          fontWeight: FontWeight.w600,
          color: Theme.of(context).colorScheme.onPrimary,
        ),
      ),
    ),
  ),
)

Key points:

  1. The IgnorePointer component is employed to prevent hit testing for sub-elements when in progress giving button a more natural look.
  2. RepaintBoundary: The use of CircularProgressIndicator and CupertinoActivityIndicator leads to multiple repaints.
  3. An AnimatedSwitcher is utilised to alternate between different elements.
  4. Visibility is used for readability (instead of ternary operator)

Conclusion

Creating animations, particularly simple ones, is straightforward and can make the application look more appealing. It's always worth discussing this aspect with your designer (if you have one) to enhance the user interface.

Here are some examples of how to use animations:

  1. Displaying errors using size and fade transitions
  2. Changing between widgets using AnimatedSwitcher and AnimatedCrossFade.
  3. Animated color switching
  4. Progress indicators (linear, circular)
  5. Navigation transitions
  6. Placeholder animations (shimmer)

Don't forget to use RepaintBoundary and read my articles! Have a pleasant day and good luck mastering Flutter!

Read more