2d-inc / Flare-Flutter

Load and get full control of your Rive files in a Flutter project using this library.
https://rive.app/
MIT License
2.55k stars 468 forks source link

Control Speed of Animation Programatically? #167

Open ccadieux opened 5 years ago

ccadieux commented 5 years ago

How do you control the speed of the animation programatically?

If not currently possible can this feature be added?

umberto-sonnino commented 5 years ago

A recent issue touched a similar subject, you can take a look at this response

luigi-rosso commented 5 years ago

We could also look into adding a speed multiplier to FlareActor.

I worry that FlareActor is getting a little bloated. We'll be doing some writeups in the upcoming weeks that'll help devs understand how FlareActor works and help encourage you to write your own Flare widgets with special functionality. That'll help keep our codebase lean and unblock devs who want access to features like this without writing custom FlareActor controllers.

workerbee22 commented 5 years ago

@umberto-sonnino the related issue is a more complicated example of starting X time into an animation then playing to completion. I think we just want the ability to programmatically make sure an animation plays for a duration of X seconds. We can either do this, or spend lots of time editing our Flare animations to set the duration precisely.

@luigi-rosso without adding a speed multiplier to FlareActor (which I agree is getting big), could you please provide some example code on how we might use FlareControls() to actually change just the elapsed/duration time for an animation. Perhaps it might be better to extend FlareControls, so we can specify a controller on FlareActor() and set the animation duration that way.

But in the absence of that, we'd really appreciate example code to do what feels like a very basic function of animations we'd like to control programmatically. I was shocked to find this is not easy to do. With all the other great stuff flare can do ... this feels like a big gap in functionality.

luigi-rosso commented 5 years ago

Here's a quick example of how a custom controller could do this:

class SlowMoController extends FlareController {
  final String animationName;
  ActorAnimation _animation;
  double speed;
  double _time = 0;

  SlowMoController(this.animationName, {this.speed = 1});

  @override
  bool advance(FlutterActorArtboard artboard, double elapsed) {
    if (_animation == null) {
      return false;
    }
    if (_animation.isLooping) {
      _time %= _animation.duration;
    }
    _animation.apply(_time, artboard, 1.0);
    _time += elapsed * speed;
    // Stop advancing if animation is done and we're not looping.
    return _animation.isLooping || _time < _animation.duration;
  }

  @override
  void initialize(FlutterActorArtboard artboard) {
    _animation = artboard.getAnimation(animationName);
  }

  @override
  void setViewTransform(Mat2D viewTransform) {
    // intentionally empty, we don't need the viewTransform in this controller
  }
}

You can instance that controller with an animation name and speed value, you can then change the speed value while the animation is playing back via a gesture callback in whatever widget holds the controller.

class _SomeWidgetState extends State<SomeWidget> {
  SlowMoController _controller = SlowMoController("idle", speed: 0.5);

  @override
  Widget build(BuildContext context) {
    return FlareActor(
      "assets/Filip.flr",
      controller: _controller,
      alignment: Alignment.center,
      fit: BoxFit.contain,
    );
  }
}

If you want to roll your own FlareControls with something similar added, take a look at this gist: https://gist.github.com/luigi-rosso/6c7b382efb925dc6feb9795ae38ae5be

workerbee22 commented 5 years ago

Thanks @luigi-rosso but I actually want to speed up the animation, not slow it down. So based on your example code above, even with speed: 2.0 (anything above 1.0) does not render anything on screen at all. Anything < 1.0 does play at reduced speed.

I've been doing Flutter dev for 18 months, but note I'm having lots of trouble with getting to know Flare, mainly around the animation concepts which are a whole world in themselves. For example looking at your code, I really need an explanation of what exactly is 'mix' ??? Is the concept explained anywhere?

See full code simplified example. Note the flare animation called "in" is valid:

import 'package:flutter/material.dart';

import 'dart:async';   // for Timer

import 'package:flutter/material.dart';

import 'package:flare_flutter/flare.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_controls.dart';   // to have a FlareController so can check when completed
import 'package:flare_flutter/flare_controller.dart';

import 'package:flare_dart/math/mat2d.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flare Single Digit',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flare Single Digit'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {

  SlowMoController _controller = SlowMoController("in", speed: 2.0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 150,
              width: 80,
              // Use controller to specify which animation and to initiate play, but the artboard ie. the digit is
              // passed in here
              child: FlareActor(
                "assets/io_2018_digits.flr",
                controller: _controller,
                artboard: "0",
                fit: BoxFit.contain,
              ),
            ),

          ],
        ),
      ),
    );
  }
}

/// From Support Guy
class SlowMoController extends FlareController {

  final String animationName;
  ActorAnimation _animation;
  double speed;
  double _time = 0;

  SlowMoController(this.animationName, {this.speed = 1});

  @override
  bool advance(FlutterActorArtboard artboard, double elapsed) {

    if (_animation == null) {
      return false;
    }
    if (_animation.isLooping) {
      _time %= _animation.duration;
    }
    _animation.apply(_time, artboard, 1.0);
    _time += elapsed * speed;

    // Stop advancing if animation is done and we're not looping.
    return _animation.isLooping || _time < _animation.duration;
  }

  @override
  void initialize(FlutterActorArtboard artboard) {
    _animation = artboard.getAnimation(animationName);
  }

  @override
  void setViewTransform(Mat2D viewTransform) {
    // intentionally empty, we don't need the viewTransform in this controller
  }
}
workerbee22 commented 5 years ago

Oh and the flare file is here:

io_2018_digits.flr.zip

luigi-rosso commented 5 years ago

The mix value controls whether the incoming animation values get mixed over the existing values. Animations apply new values to existing ones. One good example is when you are mixing two animations together, like a run and a walk cycle. You might want to smoothly transition between the two, so you first apply your walk animation with mix 1 and then your run with a mix value between 0-1. This effectively lets you interpolate between animations (instead of just popping from one to another). This is more advanced functionality that is usually only used by custom controllers.

I tried setting the speed to 2 and it's working here, I'll try with your file in a sec...The only thing I can think of is that it's a single shot (not looping) animation and it's completing before it fully draws a frame.

luigi-rosso commented 5 years ago

I only see an "Untitled" animation in that file. I actually don't see anything in that file at all for some reason (regardless of which animation or controller is being used). Could you share the source by any chance?

workerbee22 commented 5 years ago

So its like an audio crossfade? Ok so I get the concept at a high level, but exactly does mix vary between the two animations, time? opacity of both animations? So if we have mix values for run and walk, how does 1 vs 0 - 1 apply? If we just cut from one to another is that equivalent to mix values of 1 and 1 ? I realise this is just second nature to you, but even as a dev having done animations in Flutter I still don't fully get it. I feel like this needs a diagram.

btw. yes that animation is a single shot animation for approx. 2 seconds (not looping), so if I supply a speed of 2.0, I'd assume the duration was then approx. 1 second and so I'd expect to see something on screen for that 1 second.

luigi-rosso commented 5 years ago

It's exactly like an audio crossfade! One way to wire up a controller is to basically act as a mixer for a bunch of animation channels. Regarding 0-1 it's essentially just like a lerp function, 0 means use only existing, and 1 means completely replace with incoming, anything in-between is interpolated. Every keyframed value gets affected (translation, rotation, color, opacity, constraint strengths, etc).

workerbee22 commented 5 years ago

So that flare file has Artboards for digits 0 - 9. Each artboard has "in", "out" and "Animations" animations. Note the digits are all imported Lottie files.

Flare file is public here: https://www.2dimensions.com/a/mikeyman/files/flare/io-2018-digits

workerbee22 commented 5 years ago

btw. For background, I am trying to achieve the Google I/O app CountDown, which can be seen animating on this page about half way down: https://medium.com/androiddevelopers/animating-on-a-schedule-8a90d812ae4

luigi-rosso commented 5 years ago

Awesome! Ok so I tried it out, it seems to work for me, I'm not sure why you would see it with a value lower than 1 but not for one greater than 1. It only advances time once the animation is available, so I'm really stumped as to why you're not seeing it. Here's a video: https://drive.google.com/file/d/16UzC048sMQEf91-e_KeQcoGZCJzNnlAn/view?usp=sharing

workerbee22 commented 5 years ago

Thanks @luigi-rosso for taking such a close look. I'm stumped as well, so trying it on other emulators and devices now. Can you pls post your exact code, including pubspec.yaml. I might be able to spot any differences. btw. Im using packages

flare_flutter: ^1.7.3 flare_dart: ^2.2.5

luigi-rosso commented 5 years ago

I put the full source here: https://github.com/luigi-rosso/flare_control_speed_example

I'm also using flare_flutter 1.7.3 (you don't need to specify flare_dart, btw). I'm on Flutter master btw, not that it should make a difference...what channel are you on?

workerbee22 commented 5 years ago

Thanks I'll have a look at your code shortly. But I've been testing and it does work, but with some interesting results. So I thought I'd document as it may help.

Just playing the "0" digit one-shot animation, but with speed: 2.0. When the "in" animation runs, it finishes showing the digit at the end.

  1. Physical device Android Oreo 8.1 on a very slow device, run app in debug and it does not render at all. I end up with this:

image

  1. Android emulator Pie 9.0, run app in debug and it works and I see it animate to this:

image

  1. Same Android Oreo 8.1 on a very slow device, run app in release mode this time it works and animates to this:

image

... but if I increase the speed to say speed: 5.0 and run in release mode, then again it renders nothing again.

So it definitely works @luigi-rosso thank you so much, but looks like there are some practical limits around timing depending on the speed of the device and how many times we multiple the speed up. Luckily I'll only be using a speed of about 2.0 to 2.5 to get the whole animation to work 👍I'll close the issue 😃

workerbee22 commented 5 years ago

btw. I'm on flutter stable, so Flutter 1.9.1+hotfix.6.

Had a look at your example code and its a nice example. Hopefully will help others too 😃👍

workerbee22 commented 5 years ago

@luigi-rosso The speed example which is working great using your example code, but having an issue with it when animations are played one after another.

Example full simplified code below (using your speed example code, but rename to SpeedController).

I can play the first animation at slower/faster speed. But subsequent 'next' animations are not initialising properly and do not play. They show a new artboard 'digit' but do not animate, they simply show their last frame ie. the digit is shown but does not animate in.

If I don't use the new SpeedController code and just use FlareActor() then it works fine, each digit animation plays.

import 'package:flutter/material.dart';

import 'dart:async';   // for Timer

import 'package:flutter/material.dart';

import 'package:flare_flutter/flare.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flare_flutter/flare_controller.dart';

import 'package:flare_dart/math/mat2d.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flare Single Digit',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flare Single Digit'),
    );
  }
}

/// Flare related -----------------------------------------------------------------------------------------

/// From Flare contact, see issue https://github.com/2d-inc/Flare-Flutter/issues/167
/// Sample code here: https://github.com/luigi-rosso/flare_control_speed_example/blob/master/lib/main.dart
class SpeedController extends FlareController {

  final String animationName;
  ActorAnimation _animation;
  double speed;
  double _time = 0;

  // Optional param spped defaults to 1 if not provided
  SpeedController(this.animationName, {this.speed = 1});

  @override
  bool advance(FlutterActorArtboard artboard, double elapsed) {

    if (_animation == null) {
      return false;
    }
    if (_animation.isLooping) {
      _time %= _animation.duration;
    }
    _animation.apply(_time, artboard, 1.0);
    _time += elapsed * speed;

    // Stop advancing if animation is done and we're not looping.
    return _animation.isLooping || _time < _animation.duration;
  }

  @override
  void initialize(FlutterActorArtboard artboard) {
    _animation = artboard.getAnimation(animationName);
  }

  @override
  void setViewTransform(Mat2D viewTransform) {
    // intentionally empty, we don't need the viewTransform in this controller
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {

  int digit = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 150,
              width: 80,

              /// FlareActor()
              // Use controller to specify which animation and see above controls speed and thus duration
              // but the artboard ie. the digit is passed in here
              child: Digit(digit),
            ),

            RaisedButton(
              child: Text(
                "Next",
              ),
              elevation: 2.0,
              onPressed: () {
                setState(() {
                  if(digit == 9) {
                    digit = 0;
                  } else {
                    digit = digit + 1;
                  }
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

// Digit - single digit using the flare SpeedController
class Digit extends StatefulWidget {

  final int digitNew;

  Digit(this.digitNew);

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

class _DigitState extends State<Digit> {

  // Instantiate a speed controlled controller for the animation
  SpeedController _controller = SpeedController("in", speed: 1.5);

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

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

  @override
  Widget build(BuildContext context) {

    return Container(
      margin: const EdgeInsets.all(0.0),
      padding: const EdgeInsets.all(0.0),
      child: Stack(
          children: <Widget>[
            SizedBox(
              height: 150,
              width: 80,

              /// FlareActor()
              // Use controller to specify which animation and see above controls speed and thus duration
              // but the artboard ie. the digit is passed in here
              child: FlareActor(
                "assets/io_2018_digits.flr",
                controller: _controller,
                artboard: widget.digitNew.toString(),
                fit: BoxFit.contain,
              ),
            ),

          ]
      ),
    );
  }
}