Creating Complex Animation Sequences with Multiple Tweens in Flutter

Flutter provides a powerful animation framework that allows developers to create rich and engaging user experiences. While simple animations can be achieved with basic tweens, more complex animation sequences often require the orchestration of multiple tweens. This involves sequencing animations, combining them in parallel, and controlling their execution to create a seamless visual narrative. This post will guide you through creating complex animation sequences with multiple tweens in Flutter.

Understanding Flutter’s Animation Framework

Before diving into complex sequences, it’s crucial to understand the basic components of Flutter’s animation framework:

  • AnimationController: Manages the animation. It is responsible for starting, stopping, and reversing the animation.
  • Tween: Defines the start and end values of the animation and interpolates between them.
  • Animation: Represents the animation’s value at any given point in time. It listens to the AnimationController and provides the current value.
  • AnimatedWidget: A widget that rebuilds itself whenever the Animation it listens to changes its value.

Why Use Multiple Tweens in Animation Sequences?

Using multiple tweens allows you to create nuanced animations that involve changes in multiple properties over time. For instance:

  • Fading an object in while simultaneously moving it across the screen.
  • Sequencing animations, such as scaling a widget followed by rotating it.
  • Creating overlapping animations for a layered effect.

Creating a Basic Animation Sequence

Let’s start by creating a simple sequence where a widget first moves to the right, then fades out.

Step 1: Set Up the AnimationController and Tweens

Initialize the AnimationController and define the tweens for position and opacity.


import 'package:flutter/material.dart';

class ComplexAnimation extends StatefulWidget {
  @override
  _ComplexAnimationState createState() => _ComplexAnimationState();
}

class _ComplexAnimationState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _positionAnimation;
  late Animation _opacityAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _positionAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(1.5, 0.0), // Move to the right
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );

    _opacityAnimation = Tween(
      begin: 1.0,
      end: 0.0, // Fade out
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.5, 1.0, curve: Curves.easeOut), // Starts fading after 50% of the duration
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Complex Animation Sequence'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Opacity(
              opacity: _opacityAnimation.value,
              child: SlideTransition(
                position: _positionAnimation,
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.blue,
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.forward();
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

In this example:

  • _positionAnimation moves the widget horizontally.
  • _opacityAnimation fades the widget out. It uses an Interval curve to start fading only after 50% of the total duration.
  • AnimatedBuilder listens to the controller and applies both animations.

Step 2: Trigger the Animation

The FloatingActionButton starts the animation when pressed.

Combining Animations in Parallel

To run animations concurrently, you can combine them within the same AnimatedBuilder.

Example: Scaling and Rotating a Widget Simultaneously


import 'package:flutter/material.dart';

class ParallelAnimation extends StatefulWidget {
  @override
  _ParallelAnimationState createState() => _ParallelAnimationState();
}

class _ParallelAnimationState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _scaleAnimation;
  late Animation _rotationAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _scaleAnimation = Tween(
      begin: 1.0,
      end: 2.0, // Scale up
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );

    _rotationAnimation = Tween(
      begin: 0.0,
      end: 360.0 * (3.14159265359 / 180), // Rotate 360 degrees (in radians)
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Parallel Animations'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform.scale(
              scale: _scaleAnimation.value,
              child: Transform.rotate(
                angle: _rotationAnimation.value,
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.green,
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.forward();
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

Here, both _scaleAnimation and _rotationAnimation run simultaneously, controlled by the same AnimationController.

Sequencing Animations with Future.delayed and AnimationController.addStatusListener

For sequential animations, you can use Future.delayed or AnimationController.addStatusListener to trigger subsequent animations upon completion of the previous ones.

Using Future.delayed


import 'package:flutter/material.dart';

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

class _SequentialAnimationState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller1;
  late AnimationController _controller2;
  late Animation _positionAnimation;
  late Animation _opacityAnimation;

  @override
  void initState() {
    super.initState();

    _controller1 = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _controller2 = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _positionAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(1.0, 0.0), // Move to the right
    ).animate(
      CurvedAnimation(
        parent: _controller1,
        curve: Curves.easeInOut,
      ),
    );

    _opacityAnimation = Tween(
      begin: 1.0,
      end: 0.0, // Fade out
    ).animate(
      CurvedAnimation(
        parent: _controller2,
        curve: Curves.easeOut,
      ),
    );
  }

  @override
  void dispose() {
    _controller1.dispose();
    _controller2.dispose();
    super.dispose();
  }

  Future startAnimations() async {
    await _controller1.forward().orCancel;
    await Future.delayed(Duration(milliseconds: 500)); // Delay before starting the next animation
    await _controller2.forward().orCancel;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sequential Animations'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: Listenable.merge([_controller1, _controller2]),
          builder: (context, child) {
            return Opacity(
              opacity: _opacityAnimation.value,
              child: SlideTransition(
                position: _positionAnimation,
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.orange,
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          startAnimations();
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

In this code:

  • Two AnimationController instances are used: _controller1 for moving the widget and _controller2 for fading it out.
  • startAnimations uses Future.delayed to create a delay between the two animations.

Using AnimationController.addStatusListener


import 'package:flutter/material.dart';

class SequentialAnimationStatusListener extends StatefulWidget {
  @override
  _SequentialAnimationStatusListenerState createState() => _SequentialAnimationStatusListenerState();
}

class _SequentialAnimationStatusListenerState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller1;
  late AnimationController _controller2;
  late Animation _positionAnimation;
  late Animation _opacityAnimation;

  @override
  void initState() {
    super.initState();

    _controller1 = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _controller2 = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _positionAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(1.0, 0.0), // Move to the right
    ).animate(
      CurvedAnimation(
        parent: _controller1,
        curve: Curves.easeInOut,
      ),
    );

    _opacityAnimation = Tween(
      begin: 1.0,
      end: 0.0, // Fade out
    ).animate(
      CurvedAnimation(
        parent: _controller2,
        curve: Curves.easeOut,
      ),
    );

    _controller1.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller2.forward();
      }
    });
  }

  @override
  void dispose() {
    _controller1.dispose();
    _controller2.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sequential Animations (Status Listener)'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: Listenable.merge([_controller1, _controller2]),
          builder: (context, child) {
            return Opacity(
              opacity: _opacityAnimation.value,
              child: SlideTransition(
                position: _positionAnimation,
                child: Container(
                  width: 100,
                  height: 100,
                  color: Colors.purple,
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller1.forward();
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

Key aspects of this implementation:

  • The addStatusListener method is attached to _controller1.
  • When _controller1 completes (AnimationStatus.completed), _controller2.forward() is called to start the fade-out animation.

Advanced Techniques and Considerations

  • Using Curves: Different curves can dramatically change the feel of an animation. Experiment with various curves to achieve the desired effect.
  • Chaining with then(): For complex sequences, consider creating extension methods on AnimationController to chain animations using Future.then().
  • Error Handling: Ensure your animation logic gracefully handles errors or interruptions.

Conclusion

Creating complex animation sequences with multiple tweens in Flutter involves careful management of AnimationController instances, tweens, and animation listeners. By combining animations in parallel, sequencing them using Future.delayed or addStatusListener, and employing different curves, you can create visually appealing and engaging user experiences. Mastering these techniques allows you to bring your Flutter apps to life with sophisticated animations.