【Flutter】Mastering Animation ~3. Interval and multiple TickerProviders~
Tags: flutter, dart, beginners
Published: 2022-11-09 01:19:59

This article is vol 3 of a series of articles on animation.

# `Interval` and multiple `TickerProviders`

In vol 2, we looked at two use cases, and here we will look at the remaining use cases

- ~~Multiple animation effects on a single widget at the same time~~
- ~~Sequenced animation on a single widget~~
- A single widget with different animations at different times
- Staggered animation for multiple widgets
- Animate multiple Widgets separately

# A single widget with different animations at different times -`Interval`-.

In the vol2 article, I used a `TweenSequence` to apply sequenced animation of single animation effect to a widget.

Then how can we apply different animations multiple times instead of the same animation effect?

<img width="350" src="https://user-images.githubusercontent.com/44666053/201475404-be8660e4-a788-4260-bbed-55dc248399c5.gif">


You can use `Interval` in this case.

## `Interval` class
Normally, when an `Animation` is generated using an `AnimationController` and `tween`, value will change by its start value to the end value defined in the `Tween`, using up entire AnimationController's Duration time.

By using `Interval`, you can cut out the specified time within the AnimationController's progress (0 to 1) and apply the animation within that time.

## How to use it
### 1. define the start timing (`begin`) and end timing (`end`)
In `Interval`, the first and second arguments specifies the start and end timing of AnimationController's progress that you want to cut out.

Since the progress of AnimationController is `double` from 0 to 1.0, the start and end timing is also specified in double value.

For example, if `Duration` is 4 seconds, `Interval` with 0 and 0.5 will apply it's `Animation` from 0 to 2 seconds.

```dart
Interval(
    0, // begin
    0.5, // end
    curve: Curves.ease,
)
```

It also accepts a `curve` as its third argument, so you can add effects to the changes.

### 2. bind to `CurvedAnimation`.
When using `Interval` class, use a class called `CurvedAnimation` to bind it to `AnimationController`.

`CurvedAnimation` is a class that can add `curve` to animation, but it is mainly used to add `curve` to `AnimationController`.

By passing `Interval`, it creates an `Animation` that is only applied between its start and end timing.

```dart
CurvedAnimation(
    parent: controller,
    curve: Interval(
        0,
        0.5,
        curve: Curves.ease,
    ),
)
```

### 3. generate `Animation` using `AnimationController.drive` method
Generate `Animation` using `drive` method from `AnimationController` and bind it with `Tween`.

```dart
Animation = CurvedAnimation(
        parent: controller,
        curve: Interval(
            0,
            0.5,
            curve: Curves.ease,
        ),
    ).drive(Tween);
```

### 4. Bind the Animation to the Widget
All that is left to do is to attach the animation to the Widget, just like previous examples.

The `Animation` class generated with `Interval` will apply the animation to the Widget only during the defined start and end times.

### Which means?
You can use `Interval` to specify the timing for multiple animations to move at different timing.

```dart
class _ChainedAnimationState extends State<ChainedAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Tween<Alignment> alignTween;
  late Tween<double> rotateTween;
  late Tween<double> opacityTween;
  late Animation<Alignment> alignAnimation;
  late Animation<double> rotateAnimation;
  late Animation<double> opacityAnimation;
  bool animateCompleted = false;

  @override
  void initState() {
    controller =
        AnimationController(duration: const Duration(seconds: 4), vsync: this);

    // will apply align、rotation, opacity animation to single widget
    alignTween = Tween(begin: Alignment.topCenter, end: Alignment.center);
    rotateTween = Tween(begin: 0, end: pi * 8);
    opacityTween = Tween(begin: 1, end: 0);
    alignAnimation = CurvedAnimation(
      parent: controller,
      // 1. define start and end timing for Interval
      curve: const Interval(0, 0.5, curve: Curves.ease),
    ).drive(alignTween);

    // 2. bind Interval to AnimationController using CurvedAnimation
    rotateAnimation = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.5, 0.7, curve: Curves.ease),
    ).drive(rotateTween);

    // 3. create Animation by AnimationController x Tween
    opacityAnimation = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.7, 1, curve: Curves.ease),
    ).drive(opacityTween);

    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.red[300],
        title: const Text('Chained Animation'),
      ),
      drawer: const MainDrawer(),
      body: AnimatedBuilder(
        animation: controller,
        builder: (context, _) {
          return Opacity(
            opacity: opacityAnimation
                .value, // <<< 4. bind animation created using Interval
            child: Align(
              alignment: alignAnimation.value, // <<< 4.
              child: Transform.rotate(
                angle: rotateAnimation.value, // <<< 4.
                child: const Text('Hello world!'),
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (!animateCompleted) {
            controller.forward().whenComplete(() {
              setState(() => animateCompleted = true);
            });
            return;
          }
          controller.reverse().whenComplete(
            () {
              setState(() => animateCompleted = false);
            },
          );
        },
        backgroundColor: Colors.yellow[700],
        child: const Icon(
          Icons.bolt,
          color: Colors.black,
        ),
      ),
    );
  }
}
```

# Staggered animation of multiple widgets
Animations which animate in staggered manner is called **staggered animation**

<img width="350" src="https://user-images.githubusercontent.com/44666053/201475077-2042e91d-795a-45f0-b6f3-a07b88687872.gif">


This staggered Animation can also be implemented using `Interval`!

In conclusion, all you have to do is, create an `Interval` that defines the timing of each widget's movement, and attach the generated `Animation` class to each widget.

The only difference with the previous use case is that you are binding `Animation` to different widgets.

```dart
class _StaggeredAnimationState extends State<StaggeredAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Tween<Offset> offsetTween;
  late Animation<Offset> offsetAnimation1;
  late Animation<Offset> offsetAnimation2;
  late Animation<Offset> offsetAnimation3;
  bool animateCompleted = false;

  @override
  void initState() {
    controller = AnimationController(
        duration: const Duration(milliseconds: 1500), vsync: this);

    // prepare single Tween that would be applied to all of the Animations
    offsetTween = Tween(begin: const Offset(-1000, 0), end: Offset.zero);

    offsetAnimation1 = CurvedAnimation(
      parent: controller,
      // 1. define start and end time for each Intervals
      curve: const Interval(0, 0.3, curve: Curves.ease),
    ).drive(offsetTween);

    // 2. bind Interval to AnimationController using CurvedAnimation
    offsetAnimation2 = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.3, 0.7, curve: Curves.ease),
    ).drive(offsetTween);

    // 3. create Animation from AnimationController and Tween
    offsetAnimation3 = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.7, 1, curve: Curves.ease),
    ).drive(offsetTween);

    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.purple[300],
        title: const Text('Staggered Animation'),
      ),
      drawer: const MainDrawer(),
      body: AnimatedBuilder(
        animation: controller,
        builder: (context, _) {
          return Container(
            width: double.infinity,
            padding: const EdgeInsets.symmetric(vertical: 200, horizontal: 60),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Transform.translate(
                  offset: offsetAnimation1
                      .value, // 4.  bind Animation created with each Interval
                  child: const Text('Hello world!'),
                ),
                Transform.translate(
                  offset: offsetAnimation2.value, // 4.
                  child: const Text('My name is ...'),
                ),
                Transform.translate(
                  offset: offsetAnimation3.value, // 4.
                  child: const Text('heyhey1028!!'),
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (!animateCompleted) {
            controller.forward().whenComplete(() {
              setState(() => animateCompleted = true);
            });
            return;
          }
          controller.reverse().whenComplete(
            () {
              setState(() => animateCompleted = false);
            },
          );
        },
        backgroundColor: Colors.yellow[700],
        child: const Icon(
          Icons.bolt,
          color: Colors.black,
        ),
      ),
    );
  }
}
```

You can adjust the start and end timing defined in `Interval` to control how far apart the chain moves.

# Animate multiple Widgets separately
In the previous examples, a single `AnimationController` handled multiple widgets or animation effects

So by forwarding that `AnimationController`, all animations moved at the same time. But what if you want to move multiple animations separately?

<img width="350" src="https://user-images.githubusercontent.com/44666053/199262937-380cff11-3b88-475b-857c-7be57d0a1460.gif">



## Using TickerProviderStateMixin
In these cases, you need to prepare multiple `AnimationControllers`

However, `SingleTickerProviderStateMixin` we have been using until now do not allow us to generate multiple `AnimationControllers`.

In these cases, you can mixin `TickerProviderStateMixin`, which can supply multiple `TickerProviders`, to `State` class.

## Use `Listenable.merge`
Another point is to use `Listenable.merge` on the `animation` property of the `AnimatedBuilder`. 

When there are multiple `AnimationControllers` like this one, the `AnimatedBuilder` needs to monitor the state of all of them and redraw the child widgets, so use `Listenable.merge` to make the `AnimationController` into a single class

```dart
//  mixin TickerProviderStateMixin that could provide multiple tickers
class _MultipleTickerProviderState extends State<MultipleTickerProvider>
    with TickerProviderStateMixin {
  // to animate separately, prepare a multiple AnimationControllers
  late AnimationController alignController;
  late AnimationController rotateController;
  late TweenSequence<Alignment> alignTween;
  late Tween<double> rotateTween;
  late Animation<Alignment> alignmAnimation;
  late Animation<double> rotateAnimation;
  bool animatingAlign = false;
  bool animatingRotation = false;

  @override
  void initState() {
    // define duration and vsync for each AnimationController separately
    rotateController = AnimationController(
        duration: const Duration(milliseconds: 1500), vsync: this);

    alignController =
        AnimationController(duration: const Duration(seconds: 3), vsync: this);

    // define Tween for each Animation
    rotateTween = Tween(begin: 0, end: 8 * pi);
    alignTween = TweenSequence<Alignment>(
      [
        TweenSequenceItem(
          tween: Tween(
            begin: Alignment.center,
            end: Alignment.topCenter,
          ),
          weight: 0.3,
        ),
        TweenSequenceItem(
          tween: Tween(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ),
          weight: 0.4,
        ),
        TweenSequenceItem(
          tween: Tween(
            begin: Alignment.bottomCenter,
            end: Alignment.center,
          ),
          weight: 0.3,
        ),
      ],
    );

    // create Animation using each AnimationController
    alignmAnimation = alignController.drive(alignTween);
    rotateAnimation = rotateController.drive(rotateTween);

    super.initState();
  }

  @override
  void dispose() {
    rotateController.dispose();
    alignController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.brown[300],
        title: const Text('Multiple Ticker Provider'),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      drawer: const MainDrawer(),
      body: AnimatedBuilder(
        // wrap Animations with Listenable.merge to listen to multiple animation
        animation: Listenable.merge([
          rotateController,
          alignController,
        ]),
        builder: (context, _) {
          return Stack(
            fit: StackFit.expand,
            children: [
              Align(
                alignment: alignmAnimation.value, // bind animation to widget
                child: Transform.rotate(
                  angle: rotateAnimation.value,
                  child: const Text('Hello world!!'),
                ),
              )
            ],
          );
        },
      ),
      // trigger multiple AnimationControllers separately
      floatingActionButton: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              onPressed: () {
                if (!animatingAlign) {
                  alignController.repeat();
                  setState(() => animatingAlign = true);
                  return;
                }
                alignController.stop();
                setState(() => animatingAlign = false);
              },
              heroTag: 'align',
              backgroundColor: Colors.yellow[700],
              child: const Icon(
                Icons.double_arrow,
                color: Colors.black,
              ),
            ),
            const SizedBox(width: 20),
            FloatingActionButton(
              onPressed: () {
                if (!animatingRotation) {
                  rotateController.repeat();
                  setState(() => animatingRotation = true);
                  return;
                }
                rotateController.stop();
                setState(() => animatingRotation = false);
              },
              heroTag: 'rotate',
              backgroundColor: Colors.yellow[700],
              child: const Icon(
                Icons.cyclone,
                color: Colors.black,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

### Sample Repository
https://github.com/heyhey1028/flutter_samples/tree/main/samples/master_animation

## That's it!!
I have explained five use cases for complex animations. I believe that many complex animations can be implemented by combining these use cases as a basis.

After going through examples, you might have understood that animation is quite straight forward.

## What's next?
However, when you actually start implementing animations, often times, you feel overwhelmed.

In the next article, we will discuss why animation can get overwhelming easily.

https://dev.to/heyhey1028/flutter-mastering-animation-4-the-confusing-parts-of-animations-3cp3

### Reference
- https://www.youtube.com/watch?v=-_de6IfY2RU (en)
- https://docs.flutter.dev/development/ui/animations/staggered-animations (en)