Search code examples
flutteranimationcontrollerrive

FLUTTER - How to prevent a RiveAnimationController from reset when the playback finishes?


I am using the Rive package in order to have some nice animations within my Flutter application and I have 2 doubts:

I have a simple animation where some docs gets animated. I want to play this animation on Tap of it, so I'm using OneShotAnimation. The play on tap works, however when the animation ends, it immediately gets reset to the first frame.
When I load the page, in addition, the animation is loaded from the last frame.
How to avoid those 2 problems?

My code:

import 'package:flutter/material.dart';
import 'package:rive/rive.dart';

class Square extends StatefulWidget {
  final Widget page;
  final String title;

  const Square({
    required this.page,
    required this.title,
    Key? key,
  }) : super(key: key);

  @override
  State<Square> createState() => _SquareState();
}

class _SquareState extends State<Square> {
  late RiveAnimationController _esamiController;
  bool _isPlaying = false;

  @override
  void initState() {
    _esamiController = OneShotAnimation(
      'Animation 1',
      autoplay: false,
      onStart: () => setState(() => _isPlaying = true),
      onStop: () => setState(() => _isPlaying = false),
    );
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () => _isPlaying ? null : _esamiController.isActive = true,
      child: SizedBox(
        width: 192,
        height: 192,
        child: Card(
          color: Colors.black26,
          elevation: 10,
          child: Center(
            child: RiveAnimation.asset(
              'assets/animations/esami.riv',
              controllers: [_esamiController],
              onInit: (_) => setState(() {}),
            ),
          ),
        ),
      ),
    );
  }
}

Animation not working As you can see the sheets should start unordered and end ordered, while here I get the opposite.


Solution

  • I have used your code and a sample rive's community-made animation to reproduce the issue. If I understood your needs right, here are the two solutions:

    1- If you take a look at the source code of rive's controller(OneShotAnimation) that you use in your application:

    one_shot_controller.dart

    import 'package:flutter/widgets.dart';
    import 'package:rive/src/controllers/simple_controller.dart';
    
    /// Controller tailered for managing one-shot animations
    class OneShotAnimation extends SimpleAnimation {
      /// Fires when the animation stops being active
      final VoidCallback? onStop;
    
      /// Fires when the animation starts being active
      final VoidCallback? onStart;
    
      OneShotAnimation(
        String animationName, {
        double mix = 1,
        bool autoplay = true,
        this.onStop,
        this.onStart,
      }) : super(animationName, mix: mix, autoplay: autoplay) {
        isActiveChanged.addListener(onActiveChanged);
      }
    
      /// Dispose of any callback listeners
      @override
      void dispose() {
        super.dispose();
        isActiveChanged.removeListener(onActiveChanged);
      }
    
      /// Perform tasks when the animation's active state changes
      void onActiveChanged() {
        // If the animation stops and it is at the end of the one-shot, reset the
        // animation back to the starting time
        if (!isActive) {
          reset();
        }
        // Fire any callbacks
        isActive
            ? onStart?.call()
            // onStop can fire while widgets are still drawing
            : WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
      }
    }
    

    As we can see in the source code, it resets the animation when it ends. So there is nothing to do if you want to use OneShotAnimation. The only way is that you can fork the source code and change the related line. And add a modified version to your project.

    one_shot_controller.dart

    import 'package:flutter/widgets.dart';
    import 'package:rive/src/controllers/simple_controller.dart';
    
    /// Controller tailered for managing one-shot animations
    class OneShotAnimation extends SimpleAnimation {
      /// Fires when the animation stops being active
      final VoidCallback? onStop;
    
      /// Fires when the animation starts being active
      final VoidCallback? onStart;
    
      OneShotAnimation(
        String animationName, {
        double mix = 1,
        bool autoplay = true,
        this.onStop,
        this.onStart,
      }) : super(animationName, mix: mix, autoplay: autoplay) {
        isActiveChanged.addListener(onActiveChanged);
      }
    
      /// Dispose of any callback listeners
      @override
      void dispose() {
        super.dispose();
        isActiveChanged.removeListener(onActiveChanged);
      }
    
      /// Perform tasks when the animation's active state changes
      void onActiveChanged() {
        // Fire any callbacks
        isActive
            ? onStart?.call()
            // onStop can fire while widgets are still drawing
            : WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
      }
    }
    

    I have already tried it out, and it works as you wanted. But running animation for the second time could be problematic. For that, please check the second solution.

    2- You can use SimpleAnimation. Please check the following solution:

    class Square extends StatefulWidget {
      final String title;
    
      const Square({
        required this.title,
        Key? key,
      }) : super(key: key);
    
      @override
      State<Square> createState() => _SquareState();
    }
    
    class _SquareState extends State<Square> {
      late SimpleAnimation _esamiController;
    
      bool get isPlaying => _esamiController.isActive;
    
      @override
      void initState() {
        _esamiController = SimpleAnimation('bell', autoplay: false);
    
        super.initState();
      }
    
      void _reset() {
        if (!isPlaying) {
          _esamiController.reset();
        }
      }
    
      Future<void> _togglePlay() async {
        if (isPlaying) return;
    
        _reset();
        _esamiController.isActive = true;
    
        await Future.delayed(
          const Duration(milliseconds: 20),
        );
    
        _esamiController.isActive = true;
      }
    
      @override
      void dispose() {
        _esamiController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) => Scaffold(
            body: Container(
              alignment: Alignment.center,
              child: InkWell(
                onTap: _togglePlay,
                child: SizedBox(
                  width: 192,
                  height: 192,
                  child: Card(
                    color: Colors.black26,
                    elevation: 10,
                    child: Center(
                      child: RiveAnimation.asset(
                        'assets/alarm.riv',
                        controllers: [_esamiController],
                        onInit: (_) => setState(() {}),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          );
    }
    

    As you can see, I have changed RiveAnimationController with the SimpleAnimation to access the reset method. Because another way, once the animation runs, there is no way to run it for a second time through RiveAnimationController.

    If you have further problems, please don't hesitate to write in the comments.

    Note: Don't forget to modify animationName and provide a correct asset directory in RiveAnimation.asset widget.