Flutter staggered animation on same property

Viewed 415

I want to animate the scale of a widget. On tap down it should scale down and on tap up, scale up.

Problem is how can I use staggered animations on the same property?
There is an older question here on SO (Chaining seperate animations that work on the same properties) but it has no working answers.

Ive tried to adapt its solution but the problem is each animation listener is called even if the specified Interval has not been reached.

Ive added a condition which checks if _animationFuture is set but with this pattern you have to keep a value for any running animation you have in your widget which is a bit cumbersome.

Is there a "native" solution where you dont need a work around or are multiple animations on same value just not supported?

I do not want to reverse() the animation. Each tween must be able to have a custom curve, begin and end values.

Heres my widget:


class AnimatedIconButton extends StatefulWidget {

  final Widget child;

  final Function onPress;

  AnimatedIconButton({@required this.child, this.onPress}): assert(child != null);

  @override
  _AnimatedIconButtonState createState() => _AnimatedIconButtonState();
}

class _AnimatedIconButtonState extends State<AnimatedIconButton> with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  Animation<double> _animationIn;
  Animation<double> _animationOut;

  double _scale = 1;

  TickerFuture _animationFuture;


  Function get onPress => widget.onPress ?? () => null;

  _tapDown(TapDownDetails details) {
    print("_tapDown progress=${_animationController.value} scale=$_scale");
    _animationFuture = _animationController.animateTo(0.5);
  }

  _tapUp(TapUpDetails details) {
    assert(_animationFuture != null);
    print("_tapUp progress=${_animationController.value} scale=$_scale");

    _animationFuture.then((_) {
      _animationFuture = null;
      _animationController.animateTo(1);
    });

  }

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

    _animationController = AnimationController(value: 0, vsync: this, duration: const Duration(milliseconds: 1500), animationBehavior: AnimationBehavior.preserve);

    _animationIn = Tween<double>(begin: 1.0, end: 0.9).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
        0, 0.5, curve: Curves.easeOut,
      ),
    ))..addListener(() {
      if (_animationFuture == null) return;
      print("_animationIn.value ${_animationOut.value}");
      _scale = _animationIn.value;
    });

    _animationOut = Tween<double>(begin: 0.9, end: 1).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
          0.5, 1, curve: Curves.elasticOut,
      ),
    ))..addListener(() {
      if (_animationFuture != null) return;
      print("_animationOut.value ${_animationOut.value}");
       _scale = _animationOut.value;
    });



  }

  @override
  void dispose() {
    _animationController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return GestureDetector(
      onTapUp: _tapUp,
      onTapDown: _tapDown,
      child: AnimatedBuilder(
        animation: _animationController,
        child: widget.child,
        builder: (_, child) {
          return Transform.scale(
            scale: _scale,
            transformHitTests: false,
            child: child,
          );
        },
      ),
    );

  }
}

1 Answers

You can use a TweenSequence to stagger multiple animations behind each other. Borrowing the example from the documentation and changing it a bit, here's a copy-paste widget that animates a blue container from 100x100 to 200x200, pauses for a bit, and animates it back:

class _StaggeredState extends State<Staggered>
    with SingleTickerProviderStateMixin {
  /// Controller managing the animation
  late final AnimationController controller;

  /// The aninmation of our TweenSequence
  late final Animation<double> animation;

  /// An Animatable consisting of a series of tweens
  final Animatable<double> tweenSequence = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      // Animate from .5 to 1 in the first 40/80th of this animation
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 100, end: 200)
            .chain(CurveTween(curve: Curves.ease)),
        weight: 40.0,
      ),
      // Maintain still at 1.0 for 20/80th
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(200),
        weight: 20.0,
      ),
      // Animate back from 1 to 0.5 for the last 40/80th
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 200, end: 100)
            .chain(CurveTween(curve: Curves.ease)),
        weight: 40.0,
      ),
    ],
  );

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

    // Animate our TweenSequence using our controller
    animation = tweenSequence.animate(controller);

    // Add a listener that calls [setState] so our widget rebuilds
    // when the animation value changes.
    controller.addListener(() => setState(() {}));

    // Set the controller to repeat indefinitely
    controller.repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Container(
            width: animation.value,
            height: animation.value,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}
Related