Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: State Machine Input onChanged events needed #315

Open
BobaTrek opened this issue May 20, 2023 · 6 comments
Open

Feature: State Machine Input onChanged events needed #315

BobaTrek opened this issue May 20, 2023 · 6 comments
Labels
enhancement New feature or request triage

Comments

@BobaTrek
Copy link

Description

Rive supports Listeners that can change the values of artboard Inputs.
Rive-Flutter supports SMIInput classes that allow flutter access to these inputs.

Currently, when a Listener fires within Rive and it changes one or more artboard Inputs, there are no onChanged events fired back to flutter to allow the flutter application to react due to the Input change(s).

This would be a nice feature to have to keep flutter in sync with Rive.

Example

An example would be having a flutter slider that controls the height of an object in Rive using a Rive artboard input named "height". The SMIInput propagates slider changes from flutter to Rive. Let's say to 50%. But then a Rive Listener internally changes the "height" value to 80%. The flutter slider should reflect the change and also go to 80%, but it does not have a mechanism to listen for Input changes (unless it did a Timer to frequently check the valkue, but that would be poor style)

@BobaTrek BobaTrek added the bug Something isn't working label May 20, 2023
@HayesGordon
Copy link
Contributor

Hi @BobaTrek, this is something we could potentially add - something like a listener on an input. I'll bring it up with our engineering team.

If you'd like to though, you can achieve this result by implementing a custom StateMachineController, see this code:

import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:rive/src/rive_core/state_machine_controller.dart' as core;

class InputListenerExample extends StatefulWidget {
  const InputListenerExample({Key? key}) : super(key: key);

  @override
  State<InputListenerExample> createState() => _InputListenerExampleState();
}

class _InputListenerExampleState extends State<InputListenerExample> {
  SMIInput<double>? numberInput;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RiveAnimation.asset(
          'assets/rotate.riv',
          fit: BoxFit.cover,
          onInit: (artboard) {
            final controller = CustomStateMachineController.fromArtboard(
              artboard,
              'State Machine 1',
              onInputChanged: (id, value) {
                print('callback id: $id');
                print('numberInput id: ${numberInput?.id}');
                if (id == numberInput?.id) {
                  print('My numberInput changed to $value');
                  // Do something
                }
              },
            );
            artboard.addController(controller!);

            numberInput = controller.findInput('Number 1');
          },
        ),
      ),
    );
  }
}

typedef InputChanged = void Function(int id, dynamic value);

class CustomStateMachineController extends StateMachineController {
  CustomStateMachineController(
    super.stateMachine, {
    core.OnStateChange? onStateChange,
    required this.onInputChanged,
  });

  final InputChanged onInputChanged;

  @override
  void setInputValue(int id, value) {
    print('Changed id: $id,  value: $value');
    for (final input in stateMachine.inputs) {
      if (input.id == id) {
        // Do something with the input
        print('Found input: $input');
      }
    }
    // Or just pass it back to the calling widget
    onInputChanged.call(id, value);
    super.setInputValue(id, value);
  }

  static CustomStateMachineController? fromArtboard(
    Artboard artboard,
    String stateMachineName, {
    core.OnStateChange? onStateChange,
    required InputChanged onInputChanged,
  }) {
    for (final animation in artboard.animations) {
      if (animation is StateMachine && animation.name == stateMachineName) {
        return CustomStateMachineController(
          animation,
          onStateChange: onStateChange,
          onInputChanged: onInputChanged,
        );
      }
    }
    return null;
  }
}

rotate.zip

This .riv file has two input numbers, it changes one of the input value when you hover over the rectangle.

CleanShot.2023-05-22.at.11.35.44.mp4

@HayesGordon HayesGordon added enhancement New feature or request and removed bug Something isn't working labels May 22, 2023
@BobaTrek
Copy link
Author

Thanks Gordon! Works like a charm!

I did have to make one change to the CustomStateMachineController code you provided in order for it to also work with the onStateChange() mechanism:

Original:

class CustomStateMachineController extends StateMachineController {
  CustomStateMachineController(
      super.stateMachine, {
        core.OnStateChange? onStateChange,
        required this.onInputChanged,
      });

Modification:

class CustomStateMachineController extends StateMachineController {
  CustomStateMachineController(
      StateMachine stateMachine, {  // Change here
        core.OnStateChange? onStateChange,
        required this.onInputChanged,
      }) : super( stateMachine, onStateChange: onStateChange, );  // and here

Recommendations:

I have some minor recommendations as well if and when this goes into the main code base. (I am not at all trying to be picky, just trying to be helpful):

  1. Just for naming consistency, name the various elements of the InputChanged mechanism the same as the OnStateChange mechanism. For example:

a) Change member variable name from InputChanged to onInputChange to match onStateChange naming convention. Note that adds 'on" in front and removes the 'd' on the end.

b) Change typedef from:

typedef InputChanged = void Function(int id, dynamic value);

to:

typedef OnInputChange = void Function(int id, dynamic value);
  1. Make the OnInputChange input be a named optional parameter so that it can be null

Conclusion:

Again, thanks so so much for this! It has greatly reduced the effort required to get keystrokes from the user running Rive. No state machine is required at all now just to get keystrokes on shapes.

I have a large SVG file with over 50 buttons that trigger animations in Rive, but also trigger behavior in my flutter code. Since I am still in development, I am making changes to the SVG file in AI, and then deleting the asset in Rive and re-adding the updated SVG into Rive. This process is necessary until I am done with changes to the SVG, but every pass disconnects the SVG's shape targets from all of my state machines. Using InputChange is much easier to maintain!

BTW: To minimize the breakage per pass, I have partitioned my SVG file into multiple layers and then save the layers out individually for replacing into Rive and then proceeding with reconnecting the disconnected shape targets.

It would be GREAT if there was a way to "update" an SVG resource in Rive and re-establish the prior connections. But that I am sure is a tall order.

@HayesGordon
Copy link
Contributor

Glad it worked! And thanks for the thoughtful suggestions.

The code I shared was just me hacking something together - thanks for fixing it. Don't think we would add this as is to the runtime, we'll probably want to have that be on the actual input itself, to avoid you having to validate the input IDs. Something like myInput.addListener

@BobaTrek
Copy link
Author

That approach would allow having a dedicated Listener function per input as well. Nice!

@richardgazdik
Copy link

Hey @HayesGordon, I have a question about the Rive embed page inputs. I noticed that they can listen to state changes, but I couldn't find any information about it in the API documentation. Is there an undocumented input event listener that I should be aware of, or do you check the state change by listening to the advance event?
https://share.cleanshot.com/FqKxwvDH

@HayesGordon
Copy link
Contributor

Hi @richardgazdik, the video you shared sets the input from a listener in the state machine. For example, that listener is a pointer enter/exit (or it could also be set from a Rive event).

You can't listen to input changes at runtime, but you can send events to runtime to notify essential changes; see our docs on events.

As for tracking input changes, we're working on a new feature called data binding, which will greatly improve the input system (both editor and at runtime). I can't say when this will be completed but the team is working on it. For the time being, the best option is to use a combination of inputs/events.

Once data binding is out, I'll close this issue as completed, as data binding will supersede the need for listening to input changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request triage
Projects
None yet
Development

No branches or pull requests

3 participants