mirror of
https://github.com/rive-app/rive-flutter.git
synced 2025-05-17 05:16:05 +08:00

This PR adds hit testing to Flutter by overriding `hitTestSelf` on the Rive RenderObject. Currently, the hit area is the entire bounding box of the canvas. This means that when a Rive animation is rendered above any other Flutter content (for example, a Stack) all hits are absorbed by Rive and do not pass through. With this change, Rive will only absorb hits if the pointer comes in contact with a hittable Rive element. With this change, `handleEvent` will only be called if `hitTestSelf` returns true. There is some duplicate work here as `_processEvent` already performs similar hit test logic, which we can look at optimizing. But `hitTest` needed to be separate method call, as `hitTestSelf` is called before `handleEvent` and `handleEvent` sends additional information (whether it's a pointer down/up etc.). Diffs= 95beaa4f5 feat: add flutter hit test self on rive render object (#6341) bd71143bc chore: fix broken docs link (#6360) Co-authored-by: Gordon <pggordonhayes@gmail.com>
159 lines
4.0 KiB
Dart
159 lines
4.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rive/rive.dart';
|
|
|
|
import 'src/utils.dart';
|
|
|
|
/// `hit_test_pass_through.riv` does not have any listeners. Only a
|
|
/// [RiveHitTestBehavior] of type `opaque` should block content underneath.
|
|
///
|
|
/// `hit_test_consumer.riv` has a listener that covers the entire artboard.
|
|
/// Only a [RiveHitTestBehavior] of type `transparent` should allow hits
|
|
/// for content underneath.
|
|
|
|
void main() {
|
|
testWidgets('Hit test pass through artboard to widget beneath',
|
|
(tester) async {
|
|
final riveBytes = loadFile('assets/hit_test_pass_through.riv');
|
|
final riveFile = RiveFile.import(riveBytes);
|
|
|
|
int count = 0;
|
|
await tester.pumpWidget(HitTestWidget(
|
|
file: riveFile,
|
|
behavior: RiveHitTestBehavior.translucent,
|
|
onTap: () {
|
|
count++;
|
|
},
|
|
));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
final titleFinder = find.text(textButtonTitle);
|
|
await tester.tap(titleFinder);
|
|
|
|
expect(count, 1);
|
|
});
|
|
|
|
testWidgets('Hit test blocked by default opaque behavior', (tester) async {
|
|
final riveBytes = loadFile('assets/hit_test_pass_through.riv');
|
|
final riveFile = RiveFile.import(riveBytes);
|
|
|
|
int count = 0;
|
|
await tester.pumpWidget(HitTestWidget(
|
|
file: riveFile,
|
|
behavior: RiveHitTestBehavior.opaque, // default
|
|
onTap: () {
|
|
count++;
|
|
},
|
|
));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
final titleFinder = find.text(textButtonTitle);
|
|
await tester.tap(titleFinder);
|
|
|
|
expect(count, 0);
|
|
});
|
|
|
|
testWidgets('Hit test artboard consume hit with opaque', (tester) async {
|
|
final riveBytes = loadFile('assets/hit_test_consume.riv');
|
|
final riveFile = RiveFile.import(riveBytes);
|
|
|
|
int count = 0;
|
|
await tester.pumpWidget(HitTestWidget(
|
|
file: riveFile,
|
|
behavior: RiveHitTestBehavior.opaque,
|
|
onTap: () {
|
|
count++;
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final titleFinder = find.text(textButtonTitle);
|
|
await tester.tap(titleFinder);
|
|
|
|
expect(count, 0);
|
|
});
|
|
|
|
testWidgets('Hit test artboard consume hit with translucent', (tester) async {
|
|
final riveBytes = loadFile('assets/hit_test_consume.riv');
|
|
final riveFile = RiveFile.import(riveBytes);
|
|
|
|
int count = 0;
|
|
await tester.pumpWidget(HitTestWidget(
|
|
file: riveFile,
|
|
behavior: RiveHitTestBehavior.translucent,
|
|
onTap: () {
|
|
count++;
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final titleFinder = find.text(textButtonTitle);
|
|
await tester.tap(titleFinder);
|
|
|
|
expect(count, 0);
|
|
});
|
|
|
|
testWidgets('Hit test artboard pass through transparent behavior',
|
|
(tester) async {
|
|
final riveBytes = loadFile('assets/hit_test_consume.riv');
|
|
final riveFile = RiveFile.import(riveBytes);
|
|
|
|
int count = 0;
|
|
await tester.pumpWidget(HitTestWidget(
|
|
file: riveFile,
|
|
behavior: RiveHitTestBehavior.transparent,
|
|
onTap: () {
|
|
count++;
|
|
},
|
|
));
|
|
await tester.pumpAndSettle();
|
|
final titleFinder = find.text(textButtonTitle);
|
|
await tester.tap(titleFinder);
|
|
|
|
expect(count, 1);
|
|
});
|
|
}
|
|
|
|
const textButtonTitle = "Widget to click";
|
|
|
|
class HitTestWidget extends StatelessWidget {
|
|
const HitTestWidget({
|
|
required this.file,
|
|
required this.onTap,
|
|
required this.behavior,
|
|
super.key,
|
|
});
|
|
|
|
final RiveFile file;
|
|
final VoidCallback onTap;
|
|
final RiveHitTestBehavior behavior;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 500,
|
|
height: 500,
|
|
child: Stack(
|
|
children: [
|
|
Center(
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: const Text(textButtonTitle),
|
|
),
|
|
),
|
|
RiveAnimation.direct(
|
|
file,
|
|
stateMachines: const ['State Machine 1'],
|
|
fit: BoxFit.cover,
|
|
behavior: behavior,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|