SEG.Veenstra
SEG.Veenstra

Reputation: 728

What would be the proper way to update animation values in a Flutter animation?

So I'm trying to create an animation in Flutter that requires a different outcome every time the user presses a button.

I've implemented the following code according to the Flutter Animations tutorial and created a function to update it.

class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
   Animation<double> _animation;
   Tween<double> _tween;
   AnimationController _animationController;

   int position = 0;

   @override
   void initState() {
      super.initState();
      _animationController =
          AnimationController(duration: Duration(seconds: 2), vsync: this);
      _tween = Tween(begin: 0.0, end: 100.0);
      _animation = _tween.animate(_animationController)
          ..addListener(() {
              setState(() {});
          });
   }

   void setNewPosition(int newPosition) {
      _tween = Tween(
        begin: 0.0,
        end: math.pi*2/25*newPosition);
      _animationController.reset();
      _tween.animate(_animationController);
      _animationController.forward();
   }

   @override
   Widget build(BuildContext context) {
      return Container(
         child: Column(
            children: <Widget>[
               Center(
                  child: Transform.rotate(
                     angle: _animationController.value,
                     child: Icon(
                        Icons.arrow_upward,
                     size: 250.0,
                  ),
               )),
               Expanded(
                  child: Container(),
               ),
               RaisedButton(
                  child: Text('SPIN'),
                  onPressed: () {
                     setState(() {
                        setNewPosition(math.Random().nextInt(25));
                     });
                  },
               )
            ],
         )
      );
   }
}

As you can see I'm updating the _tween's begin: and end: but this doesn't seem to change the animation.

So what should I be doing to create a 'different' animation every time the users presses the button?

The general idea is to make the animations build upon each other with a random new value so for example:

So I wondered if I could update the animation, or should I create a new animation every time? Another thing I could think of was tracking the positions and use the same animation every time (0.0 -> 100.0) to act as a percentage of the transfer.

So instead of creating a new animation from 10 -> 15 I would be doing something like: currentValue = 10 + (15-10)/100*_animationController.value

Upvotes: 20

Views: 25799

Answers (3)

ANDYNVT
ANDYNVT

Reputation: 671

If you want to reverse your animation with a different path (go/back way). Try this.

In your setNewPosition function, just define new begin/end value for _tween.

   void setNewPosition() {
      _tween.begin = 0; //new begin int value
      _tween.end = 1; //new end int value
      _animationController.reverse();
   }

Upvotes: 0

R&#233;mi Rousselet
R&#233;mi Rousselet

Reputation: 277717

While working, in no situation will you actually want to make these animations within your layout as explained by @filleduchaos.

This is under optimized, as you're rebuilding far more than you should for the animation. And it's a pain to write yourself.

You'll want to use the AnimatedWidget family for this. They are divided into two kinds:

  • XXTransition
  • AnimatedXX

The first is a low layer that consumes an Animation and listens to it so that you don't need to do that ugly :

..addListener(() {
  setState(() {});
});

The second handles the remaining pieces: AnimationController, TickerProvider and Tween.

This makes using animations much easier as it's almost entirely automatical.

In your case a rotation example would be as followed:

class RotationExample extends StatefulWidget {
  final Widget child;

  const RotationExample({
    Key key,
    this.child,
  }) : super(key: key);

  @override
  RotationExampleState createState() {
    return new RotationExampleState();
  }
}

class RotationExampleState extends State<RotationExample> {
  final _random = math.Random();
  double rad = 0.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _rotate,
      child: AnimatedTransform(
        duration: const Duration(seconds: 1),
        alignment: Alignment.center,
        transform: Matrix4.rotationZ(rad),
        child: Container(
          color: Colors.red,
          height: 42.0,
          width: 42.0,
        ),
      ),
    );
  }

  void _rotate() {
    setState(() {
      rad = math.pi * 2 / 25 * _random.nextInt(25);
    });
  }
}

Easier right?

The irony is that Flutter forgot to provide an AnimatedTransform (even although we have many others !). But no worries, I made it for you!

The AnimatedTransform implementation is as followed :

class AnimatedTransform extends ImplicitlyAnimatedWidget {
  final Matrix4 transform;
  final AlignmentGeometry alignment;
  final bool transformHitTests;
  final Offset origin;
  final Widget child;

  const AnimatedTransform({
    Key key,
    @required this.transform,
    @required Duration duration,
    this.alignment,
    this.transformHitTests = true,
    this.origin,
    this.child,
    Curve curve = Curves.linear,
  })  : assert(transform != null),
        assert(duration != null),
        super(
          key: key,
          duration: duration,
          curve: curve,
        );

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

class _AnimatedTransformState
    extends AnimatedWidgetBaseState<AnimatedTransform> {
  Matrix4Tween _transform;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _transform = visitor(_transform, widget.transform,
        (dynamic value) => Matrix4Tween(begin: value));
  }

  @override
  Widget build(BuildContext context) {
    return Transform(
      alignment: widget.alignment,
      transform: _transform.evaluate(animation),
      transformHitTests: widget.transformHitTests,
      origin: widget.origin,
      child: widget.child,
    );
  }
}

I will submit a pull request so that in the future you won't need this bit of code.

Upvotes: 6

filleduchaos
filleduchaos

Reputation: 646

I'm going to skip your code a bit, and focus on what you're really asking:

The general idea is to make the animations build upon each other with a random new value so for example:

  • first spin: 0 -> 10

  • second spin: 10 -> 13

  • third spin: 13 -> 18

  • ... etc

With an explicit animation like this, there are three objects you are interested in:

  • a controller, which is a special kind of Animation that simply generates values linearly from its lower to its upper bound (both doubles, typically 0.0 and 1.0). You can control the flow of the animation - send it running forward, reverse it, stop it, or reset it.

  • a tween, which isn't an Animation but rather an Animatable. A tween defines the interpolation between two values, which don't even have to be numbers. It implements a transform method under the hood that takes in the current value of an animation and spits out the actual value you want to work with: another number, a color, a linear gradient, even a whole widget. This is what you should use to generate your angles of rotation.

  • an animation, which is the animation whose value you're actually going to work with (so this is where you'd grab values to build with). You get this by giving your tween a parent Animation to transform - this might be your controller directly but can also be some other sort of animation you've built on it (like a CurvedAnimation, which would give you easing or bouncy/elastic curves and so on). Flutter's animations are highly composable that way.

Your code is failing largely because you're not actually using the top-level animation you created in your build method and you're creating a new tween and animation every time you call setNewPosition. You can use the same tween and animation for multiple animation "cycles" - simply change the begin and end properties of the existing tween and it bubbles up to the animation. That ends up something like this:

class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
   Animation<double> _animation;
   Tween<double> _tween;
   AnimationController _animationController;
   math.Random _random = math.Random();

   int position = 0;

   double getRandomAngle() {
      return math.pi * 2 / 25 * _random.nextInt(25);
   }

   @override
   void initState() {
      super.initState();
      _animationController =
          AnimationController(duration: Duration(seconds: 2), vsync: this);
      _tween = Tween(begin: 0.0, end: getRandomAngle());
      _animation = _tween.animate(_animationController)
          ..addListener(() {
              setState(() {});
          });
   }

   void setNewPosition() {
      _tween.begin = _tween.end;
      _animationController.reset();
      _tween.end = getRandomAngle();
      _animationController.forward();
   }

   @override
   Widget build(BuildContext context) {
      return Container(
         child: Column(
            children: <Widget>[
               Center(
                  child: Transform.rotate(
                     angle: _animation.value,
                     child: Icon(
                        Icons.arrow_upward,
                     size: 250.0,
                  ),
               )),
               Expanded(
                  child: Container(),
               ),
               RaisedButton(
                  child: Text('SPIN'),
                  onPressed: setNewPosition,
               )
            ],
         )
      );
   }
}

Hope that helps!

Upvotes: 39

Related Questions