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 469 forks source link

Functionality of the advance method in Flare controller #217

Closed jcairo closed 4 years ago

jcairo commented 4 years ago

Hi guys. Thanks for the great tool.

I'm working on a Flutter animation that requires a portion of the animation to be replayed while a network request is made. From my understanding this requires a custom controller.

I'm trying to create a basic custom controller to get started but I'm having issues with the advance method.

It seems as though although I always return true from the method, its only called for the first render of the widget its contained in. Any idea why this would be?

class SubUnsubButton extends StatefulWidget {
  final Fight fight;
  SubUnsubButton({
    Key key,
    @required this.fight,
  }) : super(key: key);

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

class _SubUnsubButtonState extends State<SubUnsubButton>
    with TickerProviderStateMixin
    implements FlareController {
  // Add a state for the button
  FlareController flareController = FlareControls();
  String _buttonState;
  bool _waitingOnNetwork;
  @override
  initState() {
    super.initState();
    _buttonState =
        widget.fight.userIsSubscribed ? 'Subscribed' : 'Unsubscribed';
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(
                top: kBaseUnitSize * 2, bottom: kBaseUnitSize * 2),
            height: 30,
            child: FlatButton(
              splashColor: Colors.transparent,
              highlightColor: Colors.transparent,
              padding: EdgeInsets.all(0.0),
              child: FlareActor(
                "assets/animations/FightBell.flr",
                artboard: "RoundButtonV2",
                controller: this,
                fit: BoxFit.contain,
                sizeFromArtboard: true,
              ),
              onPressed: () async {
                // Check we have a user
                FirebaseUser user = Provider.of<FirebaseUser>(context);
                // If no user send to login screen
                if (user == null) {
                  Navigator.of(context).push(
                      MaterialPageRoute(builder: (context) => LoginScreen()));
                }
                if (!widget.fight.userIsSubscribed) {
                  attemptSubscribe(user.uid);
                } else {
                  attemptUnsubscribe(user.uid);
                }
              },
            ),
          ),
        ],
      ),
    );
  }
// Try to subscribe the user
  void attemptSubscribe(String userUID) {
    this.play('Sub');
    _waitingOnNetwork = true;
    Provider.of<FirestoreService>(context)
        .addSubscriptionForFight(
            widget.fight.eventUID, widget.fight.fightUID, userUID)
        .then((onValue) {
      widget.fight.incrementSubscriptions();
      // setState(() {
      //   //TODO: play animation to end
      // });
      Vibration.vibrate();
      _waitingOnNetwork = false;
    }).catchError((e) {
      //TODO: reset animation to beginning
      print(e);
      Provider.of<FlushBarService>(context).showBar(
          "You are not connected to the internet",
          "Connect to the internet to subscribe to this fight");

      _waitingOnNetwork = false;
    });
  }

// Try to unsubscribe the user
  void attemptUnsubscribe(String userUID) {
    this.play('Unsub');
    _waitingOnNetwork = true;
    Provider.of<FirestoreService>(context)
        .removeSubscriptionForFight(
            widget.fight.eventUID, widget.fight.fightUID, userUID)
        .then((onValue) {
      widget.fight.decrementSubscriptions();
      // setState(() {
      //   //TODO: play animation to end
      // });
      Vibration.vibrate();
      _waitingOnNetwork = false;
    }).catchError((e) {
      //TODO: reset animation to beginning
      _waitingOnNetwork = false;
      print(e);
      Provider.of<FlushBarService>(context).showBar(
          "You are not connected to the internet",
          "Connect to the internet to unsubscribe from this fight");
    });
  }

  // FLARE CONTROLLER 
  FlutterActorArtboard _artboard;
  double _animationTime = 0.0;
  bool _completed = false;
  bool _hasActiveAnimation;
  ActorAnimation _activeAnimation;

  @override
  bool advance(FlutterActorArtboard artboard, double elapsed) {
    // debugger(when: _hasActiveAnimation == false);
    print('Advance called');
    if (_hasActiveAnimation) {
      print(_hasActiveAnimation);
    }
    if (_hasActiveAnimation) {
      if (_animationTime > _activeAnimation.duration) {
        _hasActiveAnimation = false;
        // _activeAnimation = null;
        _animationTime = 0;
        _completed = true;
      } else {
        _animationTime += elapsed;
        _activeAnimation.apply(_animationTime, _artboard, 1);
      }

    }
    return true;
  }

  // Snaps an animation to the end
  // Used to setup the button on first render so the buttons aren't animated in when the screen loads
  void snapToEnd(String animationName) {
    var snapAnimation = _artboard.getAnimation(animationName);
    var animationDuration = snapAnimation.duration;
    snapAnimation.apply(animationDuration, _artboard, 1.0);
  }
  /// Add the [FlareAnimationLayer] of the animation named [name],
  /// to the end of the list of currently playing animation layers.
  void play(String animationName) {
    print("Play called");
    _activeAnimation = _artboard.getAnimation(animationName);
    _hasActiveAnimation = true;
    _completed = false;
  }

  @override
  void initialize(FlutterActorArtboard artboard) {
    _artboard = artboard;
    widget.fight.userIsSubscribed ? this.snapToEnd('Sub') : this.snapToEnd('Unsub');
    _hasActiveAnimation = false;
  }

  @override
  void setViewTransform(Mat2D viewTransform) {}

  @override
  ValueNotifier<bool> isActive;
}
luigi-rosso commented 4 years ago

Hi @jcairo! That does seem strange. Can you post the matching .flr file? If it's private, could you email it to me at luigi@rive.app? I'll follow up here with findings!

jcairo commented 4 years ago

Hey @luigi-rosso! Thanks for the quick reply.

File and link attached.

FightBell.flr2d.zip

https://rive.app/a/jonnyc/files/flare/fight-bell

luigi-rosso commented 4 years ago

I see what happened!

You're inheriting from FlareController with the implements keyword which requires you to implement everything yourself (including the ValueNotifier<bool> isActive = ValueNotifier<bool>(true); which isn't initialized in your code).

Easy fix is to change it to a mixin and remove the ValueNotifier in your code:

Change definition to mixin:

class _SubUnsubButtonState extends State<SubUnsubButton>
    with TickerProviderStateMixin, FlareController {

Delete this:

  @override
  ValueNotifier<bool> isActive;
jcairo commented 4 years ago

Hey @luigi-rosso!

That was it! Works perfect.

When I return false from the advance function it appears its never called again.

Is there a way to get the advance function to rerun again after false is returned? Is it bad practise to always return true?

Thanks again for the help. Much appreciated.

luigi-rosso commented 4 years ago

Great! Yes, you can reactivate the controller by setting isActive.value = true.

jcairo commented 4 years ago

Perfect!