DEV Community

Cover image for 【Flutter】Mastering Animation ~3. Interval and multiple TickerProviders~
heyhey1028💙🔥
heyhey1028💙🔥

Posted on • Edited on

【Flutter】Mastering Animation ~3. Interval and multiple TickerProviders~

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?

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.

Interval(
    0, // begin
    0.5, // end
    curve: Curves.ease,
)
Enter fullscreen mode Exit fullscreen mode

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.

CurvedAnimation(
    parent: controller,
    curve: Interval(
        0,
        0.5,
        curve: Curves.ease,
    ),
)
Enter fullscreen mode Exit fullscreen mode

3. generate Animation using AnimationController.drive method

Generate Animation using drive method from AnimationController and bind it with Tween.

Animation = CurvedAnimation(
        parent: controller,
        curve: Interval(
            0,
            0.5,
            curve: Curves.ease,
        ),
    ).drive(Tween);
Enter fullscreen mode Exit fullscreen mode

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.

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

Staggered animation of multiple widgets

Animations which animate in staggered manner is called staggered animation

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.

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

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?

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

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

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

Top comments (0)