Multiple controllers with AnimatedBuilder in flutter

Issue

I was searching for a way to combine repetitive and not repetitive animations in Flutter. For example, start opening animation (not repetitive) and then show some repetitive animation, like bouncing animation. Currently, I’ve used 2 animated controllers and 2 animatedBuilders above one widget. Here is my sample code:

   @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _outerAnimationController, //starting animation, not repetitive
        builder: (context, _) {
          return AnimatedBuilder( 
              animation: _innerCurvedAnimation, //repetitive animation, bouncing
              builder: (context, _) {
                return CustomPaint(
                  size: Size(MediaQuery.of(context).size.width,
                      MediaQuery.of(context).size.height),
                  painter: ShowCasePainter(
                    centerPosition: Offset(
                        MediaQuery.of(context).size.width / 2,
                        MediaQuery.of(context).size.height / 2),
                    innerCircleRadius: widget.innerCircleRadius +
                        (_innerCurvedAnimation.value * PaddingSmall), //repetitive animation value, bouncing
                    outerCircleRadius: _outerAnimationTween.value, //starting animation value, not repetitive
                  ),
                );
              });
        });
  }

Is it a good practice on using multiple controllers in this way? How to influence animatedBuilder from two controllers representing different animations?

Thanks for your help!

Solution

With the help of @pskink I’ve finally found solution. No need in use multiple AnimatedBuilders here, Listenable.merge with multiple AnimationControllers will be enought. Merging possible because AnimationControllers extends from Listenable class. Here is correct code:

@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: Listenable.merge(
            [_innerAnimationController, _outerAnimationController]),
        builder: (context, _) {
          return CustomPaint(
            size: Size(MediaQuery.of(context).size.width,
                MediaQuery.of(context).size.height),
            painter: ShowCasePainter(
              centerPosition: Offset(MediaQuery.of(context).size.width / 2,
                  MediaQuery.of(context).size.height / 2),
              innerCircleRadius: widget.innerCircleRadius +
                  (_innerCurvedAnimation.value * PaddingSmall),
              outerCircleRadius: _outerAnimationTween.value,
            ),
          );
        });
  }

This solution will be ok with any widget, but with CustomPainter there is a better solution. In case of Custom painter, animationControllers can be passed throught constructor into CustomPainter and inside of this class should be merged.
Parent build method:

 @override
  Widget build(BuildContext context) {
    return CustomPaint(
      willChange: true,
      size: Size(MediaQuery.of(context).size.width,
          MediaQuery.of(context).size.height),
      painter: ShowCasePainter(
        centerPosition: Offset(MediaQuery.of(context).size.width / 2,
            MediaQuery.of(context).size.height / 2),
        innerCircleRadius: widget.innerCircleRadius,
        outerCircleRadius: widget.outerCircleRadius,
        innerAnimationController: _innerAnimationController,
        outerAnimationController: _outerAnimationController,
      ),
    );
  }

As you see, I passed only immutable values, instead of using values from controllers/animations.
And here is CustomPainter constructor:

class ShowCasePainter extends CustomPainter {
  final Offset centerPosition;
  final double innerCircleRadius;
  final double outerCircleRadius;
  final Color backgroundColor;
  final Color ringColor;
  final Animation<double> innerAnimationController;
  final Animation<double> outerAnimationController;
  Animation<double> _innerCurvedAnimation;
  Animation<double> _outerAnimationTween;
  Animation<Color> _backgroudColorTween;
  ShowCasePainter(
      {this.innerAnimationController,
      this.outerAnimationController,
      this.centerPosition,
      this.innerCircleRadius = 32.0,
      this.outerCircleRadius = 128.0,
      this.backgroundColor,
      this.ringColor})
      : super(
            repaint: Listenable.merge(
                [innerAnimationController, outerAnimationController])) {
    _innerCurvedAnimation =
        CurvedAnimation(parent: innerAnimationController, curve: Curves.easeIn);

    _outerAnimationTween =
        Tween(begin: innerCircleRadius, end: outerCircleRadius)
            .animate(outerAnimationController);

    _backgroudColorTween = ColorTween(
            begin: Colors.transparent,
            end: backgroundColor ?? Colors.black.withOpacity(0.2))
        .animate(outerAnimationController);
  }

Now animations will work correct.

Answered By – Atamyrat Babayev

Answer Checked By – Katrina (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.