rive-app / rive-flutter

Flutter runtime for Rive
https://rive.app
MIT License
1.21k stars 191 forks source link

Detecting Progress of Animation [help-needed] #31

Closed Joker9090 closed 3 years ago

Joker9090 commented 3 years ago

Hello! I Can't figured out how to detect the progress percentage of the animations. Is there any why to detect whe animation finish? Thx!

luigi-rosso commented 3 years ago

Hi! We currently only expose lower level access to the api in order to figure out what our high level api will look like.

That means it's a little bit verbose to figure this out right now. One way is to write your own controller, inspired from our SimpleController example. When the animation advance returns false, it has completed (if it's not looping): https://github.com/rive-app/rive-flutter/blob/39eddb4fd627d51348dedea112cec9d8c8ff90f4/lib/src/controllers/simple_controller.dart#L34

You can write your own controller that is called back as time changes. You can use that to manually advance and track time and apply/mix animations as you need. Let me know if that helps or if you'd like a specific example...

Joker9090 commented 3 years ago

Great!! For now, I can use this flag for what I need. But it will be awesome if there will be a listener with de progress percentage!

My code for now _animationController1.isActiveChanged.addListener(() { if (_animationController1.isActive == false) { _playAnimation2(); } });

Thx for all!

luigi-rosso commented 3 years ago

That totally works! We're trying to figure out if we want to supply such example cases as complementary example controllers that end users can use with their projects or if we need to provide some kind of higher-level controller in the main library itself. We're still deciding, please let us know if you have any thoughts.

noobloser commented 3 years ago

@Joker9090 your code helped a lot, thx for that. I am wondering if you know a way to call setState in the listener.

I want to add flutter overlay over my animation once it is finished, but when I call setState within the listener I get the error, that another rebuild can't be scheduled. This hints that the widget rebuild is still active, even though the animation is marked finished.

Example Code ```dart animationOpen.isActiveChanged.addListener(() { if (animationOpen.isActive == false) { setState(() => isOpen = true); } }); . . . @override Widget build(BuildContext context) { return Stack(children: [ Rive( artboard: _riveArtboard, fit: BoxFit.cover, ), isOpen ? Container(color: Colors.blue) : Container(color: Colors.red) ]); } ```
Joker9090 commented 3 years ago

Hi @noobloser. Glad to help! In my case, I dont need to fire setState inside de listener, but there should be no error on calling setState inside the listener. Can you please provide the full Code? and also the errro!

Here is my fullCode for 3 animations chained

My Example Code

```dart import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rive/rive.dart'; class CommonAnimation2 extends StatefulWidget { const CommonAnimation2({Key key}) : super(key: key); @override _CommonAnimation2State createState() => _CommonAnimation2State(); } class _CommonAnimation2State extends State { RiveAnimationController _animation1, _animation2, _animation3; int cant = 1; RiveAnimationController _animationRemove1, _animationRemove2, _animationRemove3; RiveAnimationController _animationAdd1, _animationAdd2, _animationAdd3; void _togglePlay(controller) { setState(() => controller.isActive = !controller.isActive); } void _playOpen() { setState(() => status = 1); _riveArtboard.removeController(_animation3); _riveArtboard.removeController(_animation1); _riveArtboard.addController(_animation1); } void _playClose() { setState(() => status = 0); _riveArtboard.removeController(_animation3); _riveArtboard.removeController(_animation2); _riveArtboard.addController(_animation2); } void _playRemove() { if (cant == 1) { _playClose(); _riveArtboardRemove.removeController(_animationRemove1); _riveArtboardRemove.removeController(_animationRemove2); _riveArtboardRemove.removeController(_animationRemove3); _riveArtboardRemove.addController(_animationRemove1); _riveArtboardAdd.removeController(_animationAdd1); _riveArtboardAdd.removeController(_animationAdd2); _riveArtboardAdd.removeController(_animationAdd3); _riveArtboardAdd.addController(_animationAdd1); setState(() { cant = 1; }); } else { _riveArtboardRemove.removeController(_animationRemove2); _riveArtboardRemove.removeController(_animationRemove1); _riveArtboardRemove.removeController(_animationRemove3); _riveArtboardRemove.addController(_animationRemove2); setState(() { cant = cant - 1; }); } } void _playAdd() { _riveArtboardAdd.removeController(_animationAdd2); _riveArtboardAdd.removeController(_animationAdd1); _riveArtboardAdd.removeController(_animationAdd3); _riveArtboardAdd.addController(_animationAdd2); setState(() { cant = cant + 1; }); } void _showButtons() { _riveArtboardAdd.removeController(_animationAdd1); _riveArtboardAdd.removeController(_animationAdd2); _riveArtboardAdd.addController(_animationAdd3); _riveArtboardRemove.removeController(_animationRemove1); _riveArtboardRemove.removeController(_animationRemove2); _riveArtboardRemove.addController(_animationRemove3); } void _hideButtons() { _riveArtboardAdd.addController(_animationAdd1); _riveArtboardAdd.removeController(_animationAdd3); _riveArtboardRemove.addController(_animationRemove1); _riveArtboardRemove.removeController(_animationRemove3); } /// We track if the animation is playing by whether or not the controller is /// running. Artboard _riveArtboard; Artboard _riveArtboardAdd; Artboard _riveArtboardRemove; int status = 0; @override void initState() { super.initState(); // Load the animation file from the bundle, note that you could also // download this. The RiveFile just expects a list of bytes. rootBundle.load('assets/counterBtn.riv').then( // off_road_car (data) async { var file = RiveFile(); // Load the RiveFile from the binary data. var success = file.import(data); if (success) { // The artboard is the root of the animation and is what gets drawn // into the Rive widget. var artboard = file.mainArtboard; // Add a controller to play back a known animation on the main/default // artboard.We store a reference to it so we can toggle playback. _animation1 = SimpleAnimation('open'); _animation2 = SimpleAnimation('close'); _animation3 = SimpleAnimation('idle'); artboard.addController(_animation3); _animation1.isActiveChanged.addListener(() { if (_animation1.isActive == false) { _showButtons(); } }); _animation2.isActiveChanged.addListener(() { if (_animation2.isActive == false) { _hideButtons(); } }); setState(() => _riveArtboard = artboard); } }, ); rootBundle.load('assets/counterRemove.riv').then( // off_road_car (data) async { var file = RiveFile(); // Load the RiveFile from the binary data. var success = file.import(data); if (success) { // The artboard is the root of the animation and is what gets drawn // into the Rive widget. var artboard = file.mainArtboard; // Add a controller to play back a known animation on the main/default // artboard.We store a reference to it so we can toggle playback. _animationRemove1 = SimpleAnimation('idle'); _animationRemove2 = SimpleAnimation('click'); _animationRemove3 = SimpleAnimation('active'); artboard.addController(_animationRemove1); setState(() => _riveArtboardRemove = artboard); } }, ); rootBundle.load('assets/counterAdd.riv').then( // off_road_car (data) async { var file = RiveFile(); // Load the RiveFile from the binary data. var success = file.import(data); if (success) { // The artboard is the root of the animation and is what gets drawn // into the Rive widget. var artboard = file.mainArtboard; // Add a controller to play back a known animation on the main/default // artboard.We store a reference to it so we can toggle playback. _animationAdd1 = SimpleAnimation('idle'); _animationAdd2 = SimpleAnimation('click'); _animationAdd3 = SimpleAnimation('active'); artboard.addController(_animationAdd1); setState(() => _riveArtboardAdd = artboard); } }, ); } @override Widget build(BuildContext context) { return Container( height: 45, width: 200, child: Stack( children: [ Container( alignment: Alignment.center, child: GestureDetector( onTap: () { if (status == 0) { _playOpen(); } }, child: Rive(artboard: _riveArtboard), ), ), Container( alignment: Alignment.center, width: 200, child: (status == 1) ? Text(cant.toString(), style: TextStyle(fontSize: 18)) : SizedBox(), ), Align( alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.only(left: 18), width: 50, child: GestureDetector( onTap: () { if (status == 1) { _playRemove(); } }, child: Rive(artboard: _riveArtboardRemove), ), ), ), Align( alignment: Alignment.centerRight, child: Container( margin: EdgeInsets.only(right: 18), width: 50, child: GestureDetector( onTap: () { if (status == 1) { _playAdd(); } }, child: Rive(artboard: _riveArtboardAdd), ), ), ) ], ), ); } } ```

noobloser commented 3 years ago

@Joker9090 wow, thank you for your quick answer :D I found the solution myself, as it had nothing to do with rive. But I am sure, your full code example will help others too, bc of the limited docu for rive2 :/

anyway, I solved it by wrapping my setState in a scheduler

animationOpen.isActiveChanged.addListener(() {
          if (animationOpen.isActive == false) {
            SchedulerBinding.instance.addPostFrameCallback((_) {
              setState(() => isOpen = true);
            });
          }
        });