Creating Sequential and Staggered Animations in Flutter

Flutter is renowned for its capability to create beautiful and performant user interfaces. One of the key aspects that enhance the user experience is the effective use of animations. In this article, we will explore how to create sequential and staggered animations in Flutter, bringing your app’s UI to life with engaging and visually appealing effects.

What are Sequential and Staggered Animations?

Sequential and staggered animations are types of animations where multiple elements animate one after another or with a slight delay between each other. These animations can add a layer of polish to your Flutter app, making transitions and interactions more engaging.

  • Sequential Animations: A series of animations that occur in a specific order, one after the other.
  • Staggered Animations: Animations where each element begins its animation slightly after the previous one, creating a cascading or wave-like effect.

Why Use Sequential and Staggered Animations?

  • Enhanced User Experience: Adds visual interest and polish to UI transitions.
  • Improved Perceived Performance: Staggered animations can make loading processes appear faster.
  • Guide User Attention: Sequences can direct the user’s focus through a series of UI elements.

How to Implement Sequential Animations in Flutter

Sequential animations can be implemented using Future.delayed to chain animations one after the other.

Step 1: Set Up the Animation Controller and Animations

First, create an AnimationController and the individual Animation objects for each part of the sequence.


import 'package:flutter/material.dart';

class SequentialAnimation extends StatefulWidget {
  @override
  _SequentialAnimationState createState() => _SequentialAnimationState();
}

class _SequentialAnimationState extends State<SequentialAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation1;
  late Animation<double> _animation2;
  late Animation<double> _animation3;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _animation1 = Tween<double>(begin: 0, end: 100).animate(_controller);
    _animation2 = Tween<double>(begin: 0, end: 200).animate(_controller);
    _animation3 = Tween<double>(begin: 0, end: 300).animate(_controller);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sequential Animation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedBuilder(
              animation: _animation1,
              builder: (context, child) => Container(
                width: _animation1.value,
                height: 50,
                color: Colors.blue,
              ),
            ),
            AnimatedBuilder(
              animation: _animation2,
              builder: (context, child) => Container(
                width: _animation2.value,
                height: 50,
                color: Colors.green,
              ),
            ),
            AnimatedBuilder(
              animation: _animation3,
              builder: (context, child) => Container(
                width: _animation3.value,
                height: 50,
                color: Colors.red,
              ),
            ),
            ElevatedButton(
              onPressed: () {
                playSequentialAnimation();
              },
              child: Text('Start Animation'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> playSequentialAnimation() async {
    await _controller.forward().orCancel;
    await Future.delayed(Duration(seconds: 1));
    _controller.reset();
    _animation1 = Tween<double>(begin: 100, end: 0).animate(_controller);
     _animation2 = Tween<double>(begin: 200, end: 0).animate(_controller);
      _animation3 = Tween<double>(begin: 300, end: 0).animate(_controller);
     await _controller.reverse().orCancel;
    await Future.delayed(Duration(seconds: 1));
      _animation1 = Tween<double>(begin: 0, end: 100).animate(_controller);
     _animation2 = Tween<double>(begin: 0, end: 200).animate(_controller);
      _animation3 = Tween<double>(begin: 0, end: 300).animate(_controller);
  }
}

Step 2: Create a Function to Play Animations Sequentially

Implement the playSequentialAnimation function to start animations one after another using Future.delayed.


  Future<void> playSequentialAnimation() async {
    await _controller.forward().orCancel;
    await Future.delayed(Duration(seconds: 1));
    _controller.reset();
    _animation1 = Tween<double>(begin: 100, end: 0).animate(_controller);
     _animation2 = Tween<double>(begin: 200, end: 0).animate(_controller);
      _animation3 = Tween<double>(begin: 300, end: 0).animate(_controller);
     await _controller.reverse().orCancel;
    await Future.delayed(Duration(seconds: 1));
      _animation1 = Tween<double>(begin: 0, end: 100).animate(_controller);
     _animation2 = Tween<double>(begin: 0, end: 200).animate(_controller);
      _animation3 = Tween<double>(begin: 0, end: 300).animate(_controller);
  }

In this example, each await Future.delayed call ensures that the next animation only starts after the specified delay, creating a sequential effect.

How to Implement Staggered Animations in Flutter

Staggered animations can be created using the StaggeredAnimation class or by manually calculating the animation intervals for each element.

Method 1: Using the StaggeredAnimation Class (Deprecated)

Note: The StaggeredAnimation class has been deprecated in recent Flutter versions. The recommended approach is to use AnimatedBuilder with custom calculation.


//Example of StaggeredAnimation
import 'package:flutter/material.dart';

class StaggeredAnimationWidget extends StatefulWidget {
  @override
  _StaggeredAnimationWidgetState createState() => _StaggeredAnimationWidgetState();
}

class _StaggeredAnimationWidgetState extends State<StaggeredAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 4),
      vsync: this,
    )..forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Staggered Animation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            StaggeredAnimatedContainer(
              controller: _controller,
              begin: 0.0,
              end: 0.25,
              child: Container(
                width: 100,
                height: 50,
                color: Colors.blue,
              ),
            ),
            StaggeredAnimatedContainer(
              controller: _controller,
              begin: 0.25,
              end: 0.50,
              child: Container(
                width: 100,
                height: 50,
                color: Colors.green,
              ),
            ),
            StaggeredAnimatedContainer(
              controller: _controller,
              begin: 0.50,
              end: 0.75,
              child: Container(
                width: 100,
                height: 50,
                color: Colors.red,
              ),
            ),
            StaggeredAnimatedContainer(
              controller: _controller,
              begin: 0.75,
              end: 1.0,
              child: Container(
                width: 100,
                height: 50,
                color: Colors.orange,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class StaggeredAnimatedContainer extends StatelessWidget {
  final AnimationController controller;
  final double begin;
  final double end;
  final Widget child;

  const StaggeredAnimatedContainer({
    Key? key,
    required this.controller,
    required this.begin,
    required this.end,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, child) {
        final animationValue = Tween<double>(begin: 0.0, end: 1.0).animate(
          CurvedAnimation(
            parent: controller,
            curve: Interval(begin, end, curve: Curves.ease),
          ),
        ).value;
        return Opacity(
          opacity: animationValue,
          child: Transform.translate(
            offset: Offset(0.0, (1 - animationValue) * 20),
            child: child,
          ),
        );
      },
      child: child,
    );
  }
}

Method 2: Manually Calculating Animation Intervals

You can create staggered animations by manually calculating the animation intervals for each element and using AnimatedBuilder. Here’s how:


import 'package:flutter/material.dart';

class ManualStaggeredAnimation extends StatefulWidget {
  @override
  _ManualStaggeredAnimationState createState() => _ManualStaggeredAnimationState();
}

class _ManualStaggeredAnimationState extends State<ManualStaggeredAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final int numberOfItems = 4;
  final double animationDuration = 1.5; // Total animation duration

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: (animationDuration * 1000).toInt()),
      vsync: this,
    );
    _controller.forward();
  }

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

  double calculateDelay(int index) {
    return index / numberOfItems; // Stagger each animation
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Manual Staggered Animation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(numberOfItems, (index) {
            final delay = calculateDelay(index);
            return AnimatedBuilder(
              animation: _controller,
              builder: (context, child) {
                final animationValue = Tween<double>(begin: 0.0, end: 1.0).animate(
                  CurvedAnimation(
                    parent: _controller,
                    curve: Interval(
                      delay, // Begin animation at the calculated delay
                      1.0,     // End at 1.0
                      curve: Curves.easeOut,
                    ),
                  ),
                ).value;

                return Opacity(
                  opacity: animationValue,
                  child: Transform.translate(
                    offset: Offset(0.0, (1 - animationValue) * 20),
                    child: Container(
                      width: 100,
                      height: 50,
                      color: Colors.blue.withOpacity(0.5),
                    ),
                  ),
                );
              },
            );
          }),
        ),
      ),
    );
  }
}

Conclusion

Creating sequential and staggered animations in Flutter is essential for enhancing the user experience of your app. Whether using Future.delayed for sequential animations or manual interval calculations with AnimatedBuilder for staggered effects, Flutter provides the tools to bring your UI to life with engaging and visually appealing animations. Properly implemented animations not only make your app more attractive but also guide the user through interactions, improving overall usability.