Choose events to fire on State and Transition start/end.

Allows selection of event and when to fire it per state and transition.

On a state:
![CleanShot 2023-08-16 at 22 28 32@2x](https://github.com/rive-app/rive/assets/454182/34c49212-6638-4187-91ed-d99042cc5a2a)

On a transition:
![CleanShot 2023-08-16 at 22 28 53@2x](https://github.com/rive-app/rive/assets/454182/d7bcbcbf-6719-4c5f-9a2a-cfee491ac718)

I also kept getting the annoying combo-box stuck at the bottom of the screen with these combos so low to the bottom right, so I fixed that too. Which helps the text combos too. Popups with predictable heights will try to open in the direction where they'll have most vertical space:
<img width="570" alt="CleanShot 2023-08-16 at 22 30 02@2x" src="https://github.com/rive-app/rive/assets/454182/88802e09-04df-4256-b4c1-2cc2bf490fcd">

Diffs=
ad4236501 Choose events to fire on State and Transition start/end. (#5830)
abe5aab14 Added a Vello back-end with a custom winit viewer. (#5786)
adeebb26a Implement drawImage() in PLS (#5780)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
This commit is contained in:
luigi-rosso
2023-08-18 19:49:16 +00:00
parent 4780d19c3a
commit 278695858d
15 changed files with 296 additions and 11 deletions

View File

@ -1 +1 @@
8caa7d377f368af8c667ca209a2ce7aef8679be2
ad4236501bdf0218046a93c54f2ee4a379747890

View File

@ -1,3 +1,7 @@
## 0.11.15
- New event system! Listen to events reported by a StateMachine via StateMachineController.addEventListener.
## 0.11.14
- Refactor how hit testing is performed in `RiveAnimation` and `Rive` widgets. Pointer events (listeners) can now be enabled on the `Rive` widget by setting `enablePointerEvents` to `true` (default is false).

View File

@ -19,11 +19,13 @@ export 'package:rive/src/core/importers/layer_state_importer.dart';
export 'package:rive/src/core/importers/linear_animation_importer.dart';
export 'package:rive/src/core/importers/nested_state_machine_importer.dart';
export 'package:rive/src/core/importers/state_machine_importer.dart';
export 'package:rive/src/core/importers/state_machine_layer_component_importer.dart';
export 'package:rive/src/core/importers/state_machine_layer_importer.dart';
export 'package:rive/src/core/importers/state_machine_listener_importer.dart';
export 'package:rive/src/core/importers/state_transition_importer.dart';
export 'package:rive/src/event_list.dart';
export 'package:rive/src/generated/rive_core_context.dart';
export 'package:rive/src/layer_component_events.dart';
export 'package:rive/src/listener_actions.dart';
export 'package:rive/src/runtime_artboard.dart';
export 'package:rive/src/state_machine_components.dart';

View File

@ -0,0 +1,12 @@
import 'package:rive/src/core/importers/artboard_import_stack_object.dart';
import 'package:rive/src/rive_core/animation/state_machine_fire_event.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer_component.dart';
class StateMachineLayerComponentImporter extends ArtboardImportStackObject {
final StateMachineLayerComponent component;
StateMachineLayerComponentImporter(this.component);
void addFireEvent(StateMachineFireEvent event) {
component.internalAddFireEvent(event);
}
}

View File

@ -7,7 +7,6 @@ class StateMachineListenerImporter extends ArtboardImportStackObject {
StateMachineListenerImporter(this.listener);
void addAction(ListenerAction change) {
// listener.context.addObject(change);
listener.internalAddAction(change);
}
}

View File

@ -0,0 +1,71 @@
// Core automatically generated
// lib/src/generated/animation/state_machine_fire_event_base.dart.
// Do not modify manually.
import 'package:rive/src/core/core.dart';
abstract class StateMachineFireEventBase<T extends CoreContext>
extends Core<T> {
static const int typeKey = 169;
@override
int get coreType => StateMachineFireEventBase.typeKey;
@override
Set<int> get coreTypes => {StateMachineFireEventBase.typeKey};
/// --------------------------------------------------------------------------
/// EventId field with key 392.
static const int eventIdInitialValue = -1;
int _eventId = eventIdInitialValue;
static const int eventIdPropertyKey = 392;
/// Id of the Event referenced.
int get eventId => _eventId;
/// Change the [_eventId] field value.
/// [eventIdChanged] will be invoked only if the field's value has changed.
set eventId(int value) {
if (_eventId == value) {
return;
}
int from = _eventId;
_eventId = value;
if (hasValidated) {
eventIdChanged(from, value);
}
}
void eventIdChanged(int from, int to);
/// --------------------------------------------------------------------------
/// OccursValue field with key 393.
static const int occursValueInitialValue = 0;
int _occursValue = occursValueInitialValue;
static const int occursValuePropertyKey = 393;
/// When the event fires.
int get occursValue => _occursValue;
/// Change the [_occursValue] field value.
/// [occursValueChanged] will be invoked only if the field's value has
/// changed.
set occursValue(int value) {
if (_occursValue == value) {
return;
}
int from = _occursValue;
_occursValue = value;
if (hasValidated) {
occursValueChanged(from, value);
}
}
void occursValueChanged(int from, int to);
@override
void copy(Core source) {
if (source is StateMachineFireEventBase) {
_eventId = source._eventId;
_occursValue = source._occursValue;
}
}
}

View File

@ -69,6 +69,7 @@ import 'package:rive/src/rive_core/animation/nested_state_machine.dart';
import 'package:rive/src/rive_core/animation/nested_trigger.dart';
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine_bool.dart';
import 'package:rive/src/rive_core/animation/state_machine_fire_event.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
import 'package:rive/src/rive_core/animation/state_machine_listener.dart';
import 'package:rive/src/rive_core/animation/state_machine_number.dart';
@ -225,6 +226,8 @@ class RiveCoreContext {
return KeyFrameColor();
case StateMachineBase.typeKey:
return StateMachine();
case StateMachineFireEventBase.typeKey:
return StateMachineFireEvent();
case EntryStateBase.typeKey:
return EntryState();
case StateMachineTriggerBase.typeKey:
@ -851,6 +854,16 @@ class RiveCoreContext {
object.value = value;
}
break;
case StateMachineFireEventBase.eventIdPropertyKey:
if (object is StateMachineFireEventBase && value is int) {
object.eventId = value;
}
break;
case StateMachineFireEventBase.occursValuePropertyKey:
if (object is StateMachineFireEventBase && value is int) {
object.occursValue = value;
}
break;
case NestedNumberBase.nestedValuePropertyKey:
if (object is NestedNumberBase && value is double) {
object.nestedValue = value;
@ -1686,6 +1699,8 @@ class RiveCoreContext {
case StateTransitionBase.exitTimePropertyKey:
case StateTransitionBase.interpolationTypePropertyKey:
case StateTransitionBase.interpolatorIdPropertyKey:
case StateMachineFireEventBase.eventIdPropertyKey:
case StateMachineFireEventBase.occursValuePropertyKey:
case BlendState1DBase.inputIdPropertyKey:
case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey:
case StrokeBase.capPropertyKey:
@ -2010,6 +2025,10 @@ class RiveCoreContext {
return (object as StateTransitionBase).interpolationType;
case StateTransitionBase.interpolatorIdPropertyKey:
return (object as StateTransitionBase).interpolatorId;
case StateMachineFireEventBase.eventIdPropertyKey:
return (object as StateMachineFireEventBase).eventId;
case StateMachineFireEventBase.occursValuePropertyKey:
return (object as StateMachineFireEventBase).occursValue;
case BlendState1DBase.inputIdPropertyKey:
return (object as BlendState1DBase).inputId;
case BlendStateTransitionBase.exitBlendAnimationIdPropertyKey:
@ -2710,6 +2729,16 @@ class RiveCoreContext {
object.interpolatorId = value;
}
break;
case StateMachineFireEventBase.eventIdPropertyKey:
if (object is StateMachineFireEventBase) {
object.eventId = value;
}
break;
case StateMachineFireEventBase.occursValuePropertyKey:
if (object is StateMachineFireEventBase) {
object.occursValue = value;
}
break;
case BlendState1DBase.inputIdPropertyKey:
if (object is BlendState1DBase) {
object.inputId = value;

View File

@ -0,0 +1,22 @@
import 'dart:collection';
import 'package:rive/src/rive_core/animation/state_machine_fire_event.dart';
class LayerComponentEvents extends ListBase<StateMachineFireEvent> {
final List<StateMachineFireEvent?> _values = [];
List<StateMachineFireEvent> get values =>
_values.cast<StateMachineFireEvent>();
@override
int get length => _values.length;
@override
set length(int value) => _values.length = value;
@override
StateMachineFireEvent operator [](int index) => _values[index]!;
@override
void operator []=(int index, StateMachineFireEvent value) =>
_values[index] = value;
}

View File

@ -0,0 +1,38 @@
library rive_core;
import 'package:rive/src/core/core.dart';
import 'package:rive/src/generated/animation/state_machine_fire_event_base.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer_component.dart';
export 'package:rive/src/generated/animation/state_machine_fire_event_base.dart';
enum StateMachineFireOccurance {
atStart,
atEnd,
}
class StateMachineFireEvent extends StateMachineFireEventBase {
@override
void onAddedDirty() {}
@override
void eventIdChanged(int from, int to) {}
@override
void occursValueChanged(int from, int to) {}
StateMachineFireOccurance get occurs =>
StateMachineFireOccurance.values[occursValue];
@override
bool import(ImportStack importStack) {
var importer = importStack.latest<StateMachineLayerComponentImporter>(
StateMachineLayerComponentBase.typeKey);
if (importer == null) {
return false;
}
importer.addFireEvent(this);
return super.import(importStack);
}
}

View File

@ -2,10 +2,28 @@
// linter happy...
// ignore: unused_import
import 'package:rive/src/core/core.dart';
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:rive/src/core/core.dart';
import 'package:rive/src/generated/animation/state_machine_layer_component_base.dart';
import 'package:rive/src/rive_core/animation/state_machine_fire_event.dart';
export 'package:rive/src/generated/animation/state_machine_layer_component_base.dart';
abstract class StateMachineLayerComponent
extends StateMachineLayerComponentBase<RuntimeArtboard> {}
extends StateMachineLayerComponentBase<RuntimeArtboard> {
final LayerComponentEvents _events = LayerComponentEvents();
LayerComponentEvents get events => _events;
void internalAddFireEvent(StateMachineFireEvent event) {
assert(!_events.contains(event), 'shouldn\'t already contain the event');
_events.add(event);
}
Iterable<StateMachineFireEvent> eventsAt(
StateMachineFireOccurance occurence) =>
_events
.where((fireEvent) => fireEvent.occurs == occurence)
.whereNotNull();
}

View File

@ -15,6 +15,7 @@ import 'package:rive/src/rive_core/animation/linear_animation.dart';
import 'package:rive/src/rive_core/animation/nested_state_machine.dart';
import 'package:rive/src/rive_core/animation/state_instance.dart';
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine_fire_event.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
import 'package:rive/src/rive_core/animation/state_machine_listener.dart';
import 'package:rive/src/rive_core/animation/state_machine_trigger.dart';
@ -46,6 +47,7 @@ class LayerController {
StateInstance? _stateFrom;
bool _holdAnimationFrom = false;
StateTransition? _transition;
bool _transitionCompleted = false;
double _mix = 1.0;
double _mixFrom = 1.0;
@ -65,15 +67,35 @@ class LayerController {
_changeState(layer.entryState);
}
void _fireEvents(Iterable<StateMachineFireEvent> fireEvents) {
for (final fireEvent in fireEvents) {
Event event = core.resolveWithDefault(fireEvent.eventId, Event.unknown);
if (event != Event.unknown) {
controller.reportEvent(event);
}
}
}
bool _changeState(LayerState? state, {StateTransition? transition}) {
assert(state is! AnyState,
'We don\'t allow making the AnyState an active state.');
if (state == _currentState?.state) {
return false;
}
_currentState?.dispose();
var currentState = _currentState;
if (currentState != null) {
_fireEvents(currentState.state.eventsAt(StateMachineFireOccurance.atEnd));
currentState.dispose();
}
var nextState = state;
if (nextState != null) {
_currentState = nextState.makeInstance();
_fireEvents(nextState.eventsAt(StateMachineFireOccurance.atStart));
} else {
_currentState = null;
}
_currentState = state?.makeInstance();
return true;
}
@ -89,12 +111,16 @@ class LayerController {
_mix != 1;
void _updateMix(double elapsedSeconds) {
if (_transition != null &&
_stateFrom != null &&
_transition!.duration != 0) {
_mix = (_mix + elapsedSeconds / _transition!.mixTime(_stateFrom!.state))
var transition = _transition;
if (transition != null && _stateFrom != null && transition.duration != 0) {
_mix = (_mix + elapsedSeconds / transition.mixTime(_stateFrom!.state))
.clamp(0, 1)
.toDouble();
if (_mix == 1 && !_transitionCompleted) {
_transitionCompleted = true;
_fireEvents(transition.eventsAt(StateMachineFireOccurance.atEnd));
}
} else {
_mix = 1;
}
@ -191,6 +217,15 @@ class LayerController {
// Take transition
_transition = transition;
_fireEvents(transition.eventsAt(StateMachineFireOccurance.atStart));
// Immediately fire end events if transition has no duration.
if (transition.duration == 0) {
_transitionCompleted = true;
_fireEvents(transition.eventsAt(StateMachineFireOccurance.atEnd));
} else {
_transitionCompleted = false;
}
_stateFrom = outState;
// If we had an exit time and wanted to pause on exit, make sure to hold

View File

@ -25,6 +25,7 @@ import 'package:rive/src/rive_core/animation/linear_animation.dart';
import 'package:rive/src/rive_core/animation/nested_state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer_component.dart';
import 'package:rive/src/rive_core/animation/state_machine_listener.dart';
import 'package:rive/src/rive_core/animation/state_transition.dart';
import 'package:rive/src/rive_core/artboard.dart';
@ -260,6 +261,14 @@ class RiveFile {
if (!importStack.makeLatest(stackType, stackObject)) {
throw const RiveFormatErrorException('Rive file is corrupt.');
}
// Special case for StateMachineLayerComponents as the concrete types also
// add importers.
if (object is StateMachineLayerComponent) {
if (!importStack.makeLatest(StateMachineLayerComponentBase.typeKey,
StateMachineLayerComponentImporter(object))) {
throw const RiveFormatErrorException('Rive file is corrupt.');
}
}
// Store all as some may fail to import (will be set to null, but we still
// want them to occupy an id).

View File

@ -1,5 +1,5 @@
name: rive
version: 0.11.14
version: 0.11.15
homepage: https://rive.app
description: Rive 2 Flutter Runtime. This package provides runtime functionality for playing back and interacting with animations built with the Rive editor available at https://rive.app.
repository: https://github.com/rive-app/rive-flutter

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f360a9ba8e224871161a4945d24959c75fba97d9ca81f33be0f73252c31b3c4
size 486

View File

@ -305,5 +305,48 @@ void main() {
expect(receivedEvents, contains('Footstep'));
expect(receivedEvents, contains('Event 3'));
});
testWidgets('State & Transition events report',
(WidgetTester tester) async {
// Build our app and trigger a frame.
final riveBytes = loadFile('assets/events_on_states.riv');
final riveFile = RiveFile.import(riveBytes);
var controller = StateMachineController.fromArtboard(
riveFile.mainArtboard, 'State Machine 1');
expect(controller, isNotNull);
Set<String> receivedEvents = {};
controller!.addEventListener((event) {
receivedEvents.add(event.name);
});
BoxFit fit = BoxFit.contain;
Alignment alignment = Alignment.topLeft;
bool anitaliasing = true;
Widget placeholder = _placeHolderWidgetOne;
const riveKey = Key('riveWidgetKey');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RiveAnimation.direct(
riveFile,
key: riveKey,
controllers: [controller],
fit: fit,
alignment: alignment,
antialiasing: anitaliasing,
placeHolder: placeholder,
),
),
),
),
);
await tester.pump(Duration.zero);
expect(receivedEvents, contains('First'));
});
});
}