feat: expose wrapper event class to runtime

The goal of this PR is to improve the usability of events for the Flutter runtime and to hide unnecessary editor implementation detail. This also ensures that an event is not modifiable after it has been reported (from the runtime's perspective)

This is achieved by exposing runtime specific classes `RiveEvent`, `RiveOpenURLEvent` and `RiveGeneralEvent` (similar to the other runtimes), and mapping the current `Event` to these classes as an immutable object. It also maps the list of events to a Map called `properties`.

This PR also:
- Adds more event examples and fixes the audio example (`.stop` resulted in issues, and calling dispose, etc.)
- Adds tests for events

TODO:
- Will need to potentially change things (and expose the `delay`) when this lands: https://github.com/rive-app/rive/pull/5951

Diffs=
eae01824d feat: expose wrapper event class to runtime (#5956)

Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
HayesGordon
2023-09-18 17:54:53 +00:00
parent acdcb4ef2f
commit 98040494a8
19 changed files with 370 additions and 20 deletions

View File

@ -1 +1 @@
236d788ea3cf8f184a026a1b69c14af60003169c
eae01824dd9355d8aa80503071e39067600a309b

View File

@ -0,0 +1,25 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()
@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

Binary file not shown.

Binary file not shown.

View File

@ -1,20 +1,39 @@
PODS:
- audio_session (0.0.1):
- Flutter
- Flutter (1.0.0)
- just_audio (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- rive_common (0.0.1):
- Flutter
DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- Flutter (from `Flutter`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rive_common (from `.symlinks/plugins/rive_common/ios`)
EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
Flutter:
:path: Flutter
just_audio:
:path: ".symlinks/plugins/just_audio/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
rive_common:
:path: ".symlinks/plugins/rive_common/ios"
SPEC CHECKSUMS:
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
rive_common: 8a159d68033a8b073e5853acc50f03aa486a2888
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:url_launcher/url_launcher.dart';
/// This example demonstrates how to open a URL from a Rive [RiveOpenUrlEvent]..
class EventOpenUrlButton extends StatefulWidget {
const EventOpenUrlButton({super.key});
@override
State<EventOpenUrlButton> createState() => _EventOpenUrlButtonState();
}
class _EventOpenUrlButtonState extends State<EventOpenUrlButton> {
late StateMachineController _controller;
@override
void initState() {
super.initState();
}
void onInit(Artboard artboard) async {
_controller = StateMachineController.fromArtboard(artboard, 'button')!;
artboard.addController(_controller);
_controller.addEventListener(onRiveEvent);
}
void onRiveEvent(RiveEvent event) {
if (event is RiveOpenURLEvent) {
try {
final Uri url = Uri.parse(event.url);
launchUrl(url);
} on Exception catch (e) {
debugPrint(e.toString());
}
}
}
@override
void dispose() {
_controller.removeEventListener(onRiveEvent);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Event Open URL'),
),
body: Column(
children: [
Expanded(
child: RiveAnimation.asset(
'assets/url_event_button.riv',
onInit: onInit,
),
),
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text('Open URL: https://rive.app'),
),
),
],
),
);
}
}

View File

@ -13,18 +13,41 @@ class EventSounds extends StatefulWidget {
}
class _EventSoundsState extends State<EventSounds> {
final _audioPlayer = AudioPlayer();
late final StateMachineController _controller;
@override
void initState() {
super.initState();
_audioPlayer.setAsset('assets/step.mp3');
}
Future<void> _onRiveInit(Artboard artboard) async {
final audioPlayer = AudioPlayer();
await audioPlayer!.setAsset('assets/step.mp3');
final controller =
StateMachineController.fromArtboard(artboard, 'skill-controller');
artboard.addController(controller!);
controller.addEventListener((event) {
if (event.name == 'Step') {
audioPlayer.stop();
audioPlayer.play();
}
});
_controller =
StateMachineController.fromArtboard(artboard, 'skill-controller')!;
artboard.addController(_controller);
_controller.addEventListener(onRiveEvent);
}
void onRiveEvent(RiveEvent event) {
// Seconds since the event was triggered and it being reported.
// This can be used to scrub the audio forward to the precise locaiton
// if needed.
// ignore: unused_local_variable
var seconds = event.secondsDelay;
if (event.name == 'Step') {
_audioPlayer.seek(Duration.zero);
_audioPlayer.play();
}
}
@override
void dispose() {
_controller.removeEventListener(onRiveEvent);
_controller.dispose();
_audioPlayer.dispose();
super.dispose();
}
@override

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
/// This example demonstrates how to retrieve custom properties set on a Rive
/// event, and update the UI accordingly.
class EventStarRating extends StatefulWidget {
const EventStarRating({super.key});
@override
State<EventStarRating> createState() => _EventStarRatingState();
}
class _EventStarRatingState extends State<EventStarRating> {
late StateMachineController _controller;
@override
void initState() {
super.initState();
}
String ratingValue = 'Rating: 0';
void onInit(Artboard artboard) async {
_controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1')!;
artboard.addController(_controller);
_controller.addEventListener(onRiveEvent);
}
void onRiveEvent(RiveEvent event) {
// Access custom properties defined on the event
var rating = event.properties['rating'] as double;
// Schedule the setState for the next frame, as an event can be
// triggered during a current frame update
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
ratingValue = 'Rating: $rating';
});
});
}
@override
void dispose() {
_controller.removeEventListener(onRiveEvent);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Event Star Rating'),
),
body: Column(
children: [
Expanded(
child: RiveAnimation.asset(
'assets/rating_animation.riv',
onInit: onInit,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
ratingValue,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
),
)
],
),
);
}
}

View File

@ -4,7 +4,9 @@ import 'package:rive_example/custom_asset_loading.dart';
import 'package:rive_example/custom_cached_asset_loading.dart';
import 'package:rive_example/carousel.dart';
import 'package:rive_example/custom_controller.dart';
import 'package:rive_example/event_open_url_button.dart';
import 'package:rive_example/event_sounds.dart';
import 'package:rive_example/event_star_rating.dart';
import 'package:rive_example/example_state_machine.dart';
import 'package:rive_example/liquid_download.dart';
import 'package:rive_example/little_machine.dart';
@ -59,7 +61,9 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
const _Page('Basic Text', BasicText()),
const _Page('Custom Asset Loading', CustomAssetLoading()),
const _Page('Custom Cached Asset Loading', CustomCachedAssetLoading()),
const _Page('Event Open URL Button', EventOpenUrlButton()),
const _Page('Event Sounds', EventSounds()),
const _Page('Event Star Rating', EventStarRating()),
];
@override

View File

@ -9,6 +9,8 @@ PODS:
- FlutterMacOS
- rive_common (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
@ -16,6 +18,7 @@ DEPENDENCIES:
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- rive_common (from `Flutter/ephemeral/.symlinks/plugins/rive_common/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
audio_session:
@ -28,6 +31,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
rive_common:
:path: Flutter/ephemeral/.symlinks/plugins/rive_common/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
@ -35,7 +40,8 @@ SPEC CHECKSUMS:
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
rive_common: acedcab7802c0ece4b0d838b71d7deb637e1309a
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.12.1
COCOAPODS: 1.11.3

View File

@ -9,12 +9,13 @@ environment:
sdk: '>=2.17.0 <3.0.0'
dependencies:
just_audio: ^0.9.34
flutter:
sdk: flutter
rive:
path: ../
http:
just_audio: ^0.9.34
url_launcher: ^6.1.14
dev_dependencies:
flutter_test:

View File

@ -15,6 +15,7 @@ export 'package:rive/src/rive_core/artboard.dart';
export 'package:rive/src/rive_core/assets/font_asset.dart';
export 'package:rive/src/rive_core/assets/image_asset.dart';
export 'package:rive/src/rive_core/nested_artboard.dart';
export 'package:rive/src/rive_core/open_url_target.dart';
export 'package:rive/src/rive_core/rive_animation_controller.dart';
export 'package:rive/src/rive_core/runtime/exceptions/rive_format_error_exception.dart';
export 'package:rive/src/rive_core/runtime/runtime_header.dart'
@ -29,4 +30,5 @@ export 'package:rive/src/rive_core/text/text_value_run.dart';
export 'package:rive/src/rive_file.dart';
export 'package:rive/src/rive_scene.dart';
export 'package:rive/src/runtime_artboard.dart';
export 'package:rive/src/runtime_event.dart';
export 'package:rive/src/widgets/rive_animation.dart';

View File

@ -11,6 +11,9 @@ export 'package:rive/src/generated/event_base.dart';
class Event extends EventBase {
final List<CustomProperty> customProperties = [];
double _secondsDelay = 0.0;
double get secondsDelay => _secondsDelay;
@override
void update(int dirt) {}
@ -47,6 +50,7 @@ class Event extends EventBase {
void trigger(CallbackData data) {
if (data.context is StateMachineController) {
var controller = data.context as StateMachineController;
_secondsDelay = data.delay;
controller.reportEvent(this);
}
}

View File

@ -1,10 +1,9 @@
import 'package:rive/src/generated/open_url_event_base.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_core/open_url_target.dart';
export 'package:rive/src/generated/open_url_event_base.dart';
enum OpenUrlTarget { blank, parent, self, top }
class OpenUrlEvent extends OpenUrlEventBase {
@override
void update(int dirt) {}

View File

@ -0,0 +1,2 @@
/// Open URL event target types.
enum OpenUrlTarget { blank, parent, self, top }

View File

@ -27,6 +27,7 @@ import 'package:rive/src/rive_core/nested_artboard.dart';
import 'package:rive/src/rive_core/node.dart';
import 'package:rive/src/rive_core/rive_animation_controller.dart';
import 'package:rive/src/rive_core/shapes/shape.dart';
import 'package:rive/src/runtime_event.dart';
import 'package:rive_common/math.dart';
/// Callback signature for state machine state changes
@ -37,7 +38,7 @@ typedef OnStateChange = void Function(
typedef OnLayerStateChange = void Function(LayerState);
/// Callback signature for events firing.
typedef OnEvent = void Function(Event);
typedef OnEvent = void Function(RiveEvent);
class LayerController {
final StateMachineLayer layer;
@ -281,7 +282,12 @@ class StateMachineController extends RiveAnimationController<CoreContext>
@Deprecated('Use `addEventListener` instead.') this.onStateChange,
});
/// Adds a Rive event listener to this controller.
///
/// Documentation: https://help.rive.app/runtimes/rive-events
void addEventListener(OnEvent callback) => _eventListeners.add(callback);
/// Removes listener from this controller.
void removeEventListener(OnEvent callback) =>
_eventListeners.remove(callback);
@ -387,6 +393,7 @@ class StateMachineController extends RiveAnimationController<CoreContext>
@override
void dispose() {
_clearLayerControllers();
_eventListeners.clear();
super.dispose();
}
@ -420,7 +427,12 @@ class StateMachineController extends RiveAnimationController<CoreContext>
if (_firedEvents.isNotEmpty) {
var events = _firedEvents.toList(growable: false);
_firedEvents.clear();
_eventListeners.toList().forEach(events.forEach);
_eventListeners.toList().forEach((listener) {
for (final event in events) {
listener(RiveEvent.fromCoreEvent(event));
}
});
}
}

105
lib/src/runtime_event.dart Normal file
View File

@ -0,0 +1,105 @@
import 'package:flutter/widgets.dart';
import 'package:rive/src/rive_core/custom_property_boolean.dart';
import 'package:rive/src/rive_core/custom_property_number.dart';
import 'package:rive/src/rive_core/custom_property_string.dart';
import 'package:rive/src/rive_core/event.dart';
import 'package:rive/src/rive_core/open_url_event.dart';
import 'package:rive/src/rive_core/open_url_target.dart';
/// A Rive Event that is reported from an StateMachineController.
///
/// See:
/// - [RiveGeneralEvent]
/// - [RiveOpenURLEvent]
///
/// For specific event types.
///
/// Documentation: https://help.rive.app/runtimes/rive-events
@immutable
class RiveEvent {
final String name;
final double secondsDelay;
final Map<String, dynamic> properties;
const RiveEvent({
required this.name,
required this.secondsDelay,
required this.properties,
});
factory RiveEvent.fromCoreEvent(Event event) {
final Map<String, dynamic> properties = {};
for (final property in event.customProperties) {
dynamic value;
switch (property.coreType) {
case CustomPropertyNumberBase.typeKey:
value = (property as CustomPropertyNumber).propertyValue;
break;
case CustomPropertyStringBase.typeKey:
value = (property as CustomPropertyString).propertyValue;
break;
case CustomPropertyBooleanBase.typeKey:
value = (property as CustomPropertyBoolean).propertyValue;
break;
}
if (value != null) {
properties[property.name] = value;
}
}
if (event.coreType == OpenUrlEventBase.typeKey) {
event = event as OpenUrlEvent;
return RiveOpenURLEvent(
name: event.name,
url: event.url,
target: event.target,
secondsDelay: event.secondsDelay,
properties: properties,
);
} else {
return RiveGeneralEvent(
name: event.name,
secondsDelay: event.secondsDelay,
properties: properties,
);
}
}
@override
String toString() => 'Rive Event - name: $name, properties: $properties';
}
/// A general Rive event that provides information about the event.
@immutable
class RiveGeneralEvent extends RiveEvent {
const RiveGeneralEvent({
required String name,
required double secondsDelay,
required Map<String, dynamic> properties,
}) : super(name: name, secondsDelay: secondsDelay, properties: properties);
@override
String toString() =>
'Rive GeneralEvent - name: $name, properties: $properties';
}
/// An Open URL Rive event that provides information about the URL and target.
///
/// See:
/// - [url]
/// - [target]
@immutable
class RiveOpenURLEvent extends RiveEvent {
final String url;
final OpenUrlTarget target;
const RiveOpenURLEvent({
required String name,
required double secondsDelay,
required Map<String, dynamic> properties,
required this.target,
required this.url,
}) : super(name: name, secondsDelay: secondsDelay, properties: properties);
@override
String toString() =>
'Rive OpenURLEvent - name: $name, properties: $properties';
}

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43496fca427b3da9640dbb646fe20a6982fdb14586ffd777ccd2f5395aa8b78a
size 415