mirror of
https://github.com/rive-app/rive-flutter
synced 2025-07-14 21:55:57 +00:00
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:
@ -1 +1 @@
|
||||
d072cbde0977cca635db6427795b36aea914b82d
|
||||
2eb7308d297bd1b829c6aa829df6470823e76671
|
||||
|
@ -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`.
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user