rive-app / rive-flutter

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

Feature: State Machine Input onChanged events needed #315

Open BobaTrek opened 1 year ago

BobaTrek commented 1 year ago

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)

HayesGordon commented 1 year ago

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.

https://github.com/rive-app/rive-flutter/assets/13705472/37fe6464-1cf3-4ed3-9f67-d7c8eb706c5b

BobaTrek commented 1 year ago

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);

2) 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 commented 1 year ago

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 commented 1 year ago

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

richardgazdik commented 4 months ago

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 commented 4 months ago

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.