refactor: hit testing

- Moves the hit testing logic from `RiveAnimation` to the `Rive` render box instead. Setting the default to be false, and users have to opt in to enable it. This is to ensure we don't break code for anyone that currently handles their own pointer logic, and also to avoid unnecessarily doing work if it's not needed.
- Add onEnter and onExit to resolve https://github.com/rive-app/rive-flutter/issues/284
- Adds ability to set cursor (this had to be overridden to allow for onEnter/onExit) and makes sense to expose publicly. Note though that this will be for the entire render box area. It won't work for "part" of an artboard that has a listener. Though we could look into adding that in the future.

Diffs=
2eb7308d2 refactor: hit testing (#5731)

Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
HayesGordon
2023-08-08 13:21:09 +00:00
parent 7bf4ec8fda
commit 59b73de9b7
6 changed files with 176 additions and 85 deletions

View File

@ -1 +1 @@
d072cbde0977cca635db6427795b36aea914b82d
2eb7308d297bd1b829c6aa829df6470823e76671

View File

@ -1,3 +1,7 @@
## Upcoming
- 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).
## 0.11.13
- Initializes Rive's text engine only when necessary when calling any of `RiveFile.asset`, `RiveFile.network`, or `RiveFile.file`.

View File

@ -1,4 +1,8 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/src/controllers/state_machine_controller.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_render_box.dart';
import 'package:rive/src/runtime_artboard.dart';
@ -38,6 +42,22 @@ class Rive extends LeafRenderObjectWidget {
/// Enables/disables antialiasing
final bool antialiasing;
/// Enable input listeners (such as hover, pointer down, etc.) on the
/// artboard.
///
/// Default `false`.
final bool enablePointerEvents;
/// The mouse cursor for mouse pointers that are hovering over the region.
///
/// When a mouse enters the region, its cursor will be changed to the [cursor].
/// When the mouse leaves the region, the cursor will be decided by the region
/// found at the new location.
///
/// The [cursor] defaults to [MouseCursor.defer], deferring the choice of
/// cursor to the next region behind it in hit-test order.
final MouseCursor cursor;
/// {@template Rive.clipRect}
/// Clip the artboard to this rect.
///
@ -51,6 +71,8 @@ class Rive extends LeafRenderObjectWidget {
required this.artboard,
this.useArtboardSize = false,
this.antialiasing = true,
this.enablePointerEvents = false,
this.cursor = MouseCursor.defer,
BoxFit? fit,
Alignment? alignment,
this.clipRect,
@ -66,7 +88,9 @@ class Rive extends LeafRenderObjectWidget {
..alignment = alignment
..artboardSize = Size(artboard.width, artboard.height)
..useArtboardSize = useArtboardSize
..clipRect = clipRect;
..clipRect = clipRect
..enableHitTests = enablePointerEvents
..cursor = cursor;
}
@override
@ -79,13 +103,20 @@ class Rive extends LeafRenderObjectWidget {
..alignment = alignment
..artboardSize = Size(artboard.width, artboard.height)
..useArtboardSize = useArtboardSize
..clipRect = clipRect;
..clipRect = clipRect
..enableHitTests = enablePointerEvents
..cursor = cursor;
}
}
class RiveRenderObject extends RiveRenderBox {
class RiveRenderObject extends RiveRenderBox implements MouseTrackerAnnotation {
RuntimeArtboard _artboard;
RiveRenderObject(this._artboard) {
RiveRenderObject(
this._artboard, {
MouseCursor cursor = MouseCursor.defer,
bool validForMouseTracker = true,
}) : _cursor = cursor,
_validForMouseTracker = validForMouseTracker {
_artboard.redraw.addListener(scheduleRepaint);
}
@ -101,6 +132,110 @@ class RiveRenderObject extends RiveRenderBox {
markNeedsLayout();
}
/// Local offset to global artboard position
Vec2D _toArtboard(Offset local) {
final globalCoordinates = localToGlobal(local);
return globalToArtboard(globalCoordinates);
}
/// Helper to manage hit testing
void _hitHelper(PointerEvent event,
void Function(StateMachineController, Vec2D) callback) {
final artboardPosition = _toArtboard(event.localPosition);
final stateMachineControllers =
_artboard.animationControllers.whereType<StateMachineController>();
for (final stateMachineController in stateMachineControllers) {
callback(stateMachineController, artboardPosition);
}
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (!enableHitTests) {
return;
}
if (event is PointerDownEvent) {
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerDown(artboardPosition, event),
);
}
if (event is PointerUpEvent) {
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerUp(artboardPosition),
);
}
if (event is PointerMoveEvent) {
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerMove(artboardPosition),
);
}
if (event is PointerHoverEvent) {
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerMove(artboardPosition),
);
}
}
@override
PointerEnterEventListener? get onEnter => (event) {
if (!enableHitTests) return;
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerEnter(artboardPosition),
);
};
@override
PointerExitEventListener? get onExit => (event) {
if (!enableHitTests) return;
_hitHelper(
event,
(controller, artboardPosition) =>
controller.pointerExit(artboardPosition),
);
};
@override
MouseCursor get cursor => _cursor;
MouseCursor _cursor;
set cursor(MouseCursor value) {
if (_cursor != value) {
_cursor = value;
// A repaint is needed in order to trigger a device update of
// [MouseTracker] so that this new value can be found.
markNeedsPaint();
}
}
@override
bool get validForMouseTracker => _validForMouseTracker;
bool _validForMouseTracker;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_validForMouseTracker = true;
}
@override
void detach() {
// It's possible that the renderObject be detached during mouse events
// dispatching, set the [MouseTrackerAnnotation.validForMouseTracker] false to prevent
// the callbacks from being called.
_validForMouseTracker = false;
super.detach();
}
@override
void dispose() {
_artboard.redraw.removeListener(scheduleRepaint);

View File

@ -482,6 +482,16 @@ class StateMachineController extends RiveAnimationController<CoreContext> {
position,
hitEvent: ListenerType.up,
);
void pointerExit(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.exit,
);
void pointerEnter(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.enter,
);
}
/// Representation of a Shape from the Artboard Instance and all the events it

View File

@ -19,6 +19,7 @@ abstract class RiveRenderBox extends RenderBox {
Alignment _alignment = Alignment.center;
bool _useArtboardSize = false;
Rect? _clipRect;
bool _enableHitTests = false;
bool get useArtboardSize => _useArtboardSize;
@ -73,6 +74,14 @@ abstract class RiveRenderBox extends RenderBox {
}
}
bool get enableHitTests => _enableHitTests;
set enableHitTests(bool value) {
if (value != _enableHitTests) {
_enableHitTests = value;
}
}
@override
bool get sizedByParent => !useArtboardSize;

View File

@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
import 'package:rive_common/math.dart';
/// Specifies whether a source is from an asset bundle or http
enum _Source {
@ -311,89 +310,23 @@ class RiveAnimationState extends State<RiveAnimation> {
super.dispose();
}
Vec2D? _toArtboard(Offset local) {
RiveRenderObject? riveRenderer;
var renderObject = context.findRenderObject();
if (renderObject is! RenderBox) {
return null;
}
renderObject.visitChildren(
(child) {
if (child is RiveRenderObject) {
riveRenderer = child;
}
},
);
if (riveRenderer == null) {
return null;
}
var globalCoordinates = renderObject.localToGlobal(local);
return riveRenderer!.globalToArtboard(globalCoordinates);
}
Widget _optionalHitTester(BuildContext context, Widget child) {
assert(_artboard != null);
var hasHitTesting = _artboard!.animationControllers.any(
(controller) =>
controller is StateMachineController &&
(controller.hitShapes.isNotEmpty ||
controller.hitNestedArtboards.isNotEmpty),
);
if (hasHitTesting) {
void hitHelper(PointerEvent event,
void Function(StateMachineController, Vec2D) callback) {
var artboardPosition = _toArtboard(event.localPosition);
if (artboardPosition != null) {
var stateMachineControllers = _artboard!.animationControllers
.whereType<StateMachineController>();
for (final stateMachineController in stateMachineControllers) {
callback(stateMachineController, artboardPosition);
}
}
}
return Listener(
onPointerDown: (details) => hitHelper(
details,
(controller, artboardPosition) =>
controller.pointerDown(artboardPosition, details),
),
onPointerUp: (details) => hitHelper(
details,
(controller, artboardPosition) =>
controller.pointerUp(artboardPosition),
),
onPointerHover: (details) => hitHelper(
details,
(controller, artboardPosition) =>
controller.pointerMove(artboardPosition),
),
onPointerMove: (details) => hitHelper(
details,
(controller, artboardPosition) =>
controller.pointerMove(artboardPosition),
),
child: child,
bool get _shouldAddHitTesting => _artboard!.animationControllers.any(
(controller) =>
controller is StateMachineController &&
(controller.hitShapes.isNotEmpty ||
controller.hitNestedArtboards.isNotEmpty),
);
}
return child;
}
@override
Widget build(BuildContext context) => _artboard != null
? _optionalHitTester(
context,
Rive(
artboard: _artboard!,
fit: widget.fit,
alignment: widget.alignment,
antialiasing: widget.antialiasing,
useArtboardSize: widget.useArtboardSize,
clipRect: widget.clipRect,
),
? Rive(
artboard: _artboard!,
fit: widget.fit,
alignment: widget.alignment,
antialiasing: widget.antialiasing,
useArtboardSize: widget.useArtboardSize,
clipRect: widget.clipRect,
enablePointerEvents: _shouldAddHitTesting,
)
: widget.placeHolder ?? const SizedBox();
}