Unverified Commit 6d67fbb9 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add HitTestBehavior to TapRegion (#113634)

parent 94d3a802
...@@ -173,7 +173,12 @@ abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox { ...@@ -173,7 +173,12 @@ abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
RenderBox? child, RenderBox? child,
}) : super(child); }) : super(child);
/// How to behave during hit testing. /// How to behave during hit testing when deciding how the hit test propagates
/// to children and whether to consider targets behind this one.
///
/// Defaults to [HitTestBehavior.deferToChild].
///
/// See [HitTestBehavior] for the allowed values and their meanings.
HitTestBehavior behavior; HitTestBehavior behavior;
@override @override
......
...@@ -970,10 +970,14 @@ class GestureDetector extends StatelessWidget { ...@@ -970,10 +970,14 @@ class GestureDetector extends StatelessWidget {
/// detecting screens. /// detecting screens.
final GestureForcePressEndCallback? onForcePressEnd; final GestureForcePressEndCallback? onForcePressEnd;
/// How this gesture detector should behave during hit testing. /// How this gesture detector should behave during hit testing when deciding
/// how the hit test propagates to children and whether to consider targets
/// behind this one.
/// ///
/// This defaults to [HitTestBehavior.deferToChild] if [child] is not null and /// This defaults to [HitTestBehavior.deferToChild] if [child] is not null and
/// [HitTestBehavior.translucent] if child is null. /// [HitTestBehavior.translucent] if child is null.
///
/// See [HitTestBehavior] for the allowed values and their meanings.
final HitTestBehavior? behavior; final HitTestBehavior? behavior;
/// Whether to exclude these gestures from the semantics tree. For /// Whether to exclude these gestures from the semantics tree. For
......
...@@ -312,6 +312,7 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -312,6 +312,7 @@ class TapRegion extends SingleChildRenderObjectWidget {
super.key, super.key,
required super.child, required super.child,
this.enabled = true, this.enabled = true,
this.behavior = HitTestBehavior.deferToChild,
this.onTapOutside, this.onTapOutside,
this.onTapInside, this.onTapInside,
this.groupId, this.groupId,
...@@ -321,6 +322,14 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -321,6 +322,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// Whether or not this [TapRegion] is enabled as part of the composite region. /// Whether or not this [TapRegion] is enabled as part of the composite region.
final bool enabled; final bool enabled;
/// How to behave during hit testing when deciding how the hit test propagates
/// to children and whether to consider targets behind this [TapRegion].
///
/// Defaults to [HitTestBehavior.deferToChild].
///
/// See [HitTestBehavior] for the allowed values and their meanings.
final HitTestBehavior behavior;
/// A callback to be invoked when a tap is detected outside of this /// A callback to be invoked when a tap is detected outside of this
/// [TapRegion] and any other region with the same [groupId], if any. /// [TapRegion] and any other region with the same [groupId], if any.
/// ///
...@@ -358,6 +367,7 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -358,6 +367,7 @@ class TapRegion extends SingleChildRenderObjectWidget {
return RenderTapRegion( return RenderTapRegion(
registry: TapRegionRegistry.maybeOf(context), registry: TapRegionRegistry.maybeOf(context),
enabled: enabled, enabled: enabled,
behavior: behavior,
onTapOutside: onTapOutside, onTapOutside: onTapOutside,
onTapInside: onTapInside, onTapInside: onTapInside,
groupId: groupId, groupId: groupId,
...@@ -367,12 +377,14 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -367,12 +377,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) { void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
renderObject.registry = TapRegionRegistry.maybeOf(context); renderObject
renderObject.enabled = enabled; ..registry = TapRegionRegistry.maybeOf(context)
renderObject.groupId = groupId; ..enabled = enabled
renderObject.onTapOutside = onTapOutside; ..behavior = behavior
renderObject.onTapInside = onTapInside; ..groupId = groupId
if (kReleaseMode) { ..onTapOutside = onTapOutside
..onTapInside = onTapInside;
if (!kReleaseMode) {
renderObject.debugLabel = debugLabel; renderObject.debugLabel = debugLabel;
} }
} }
...@@ -380,9 +392,10 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -380,9 +392,10 @@ class TapRegion extends SingleChildRenderObjectWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true));
properties.add(DiagnosticsProperty<HitTestBehavior>('behavior', behavior, defaultValue: HitTestBehavior.deferToChild));
properties.add(DiagnosticsProperty<Object?>('debugLabel', debugLabel, defaultValue: null)); properties.add(DiagnosticsProperty<Object?>('debugLabel', debugLabel, defaultValue: null));
properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null)); properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null));
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true));
} }
} }
...@@ -393,8 +406,9 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -393,8 +406,9 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// system. /// system.
/// ///
/// This render object indicates to the nearest ancestor [TapRegionSurface] that /// This render object indicates to the nearest ancestor [TapRegionSurface] that
/// the region occupied by its child will participate in the tap detection for /// the region occupied by its child (or itself if [behavior] is
/// that surface. /// [HitTestBehavior.opaque]) will participate in the tap detection for that
/// surface.
/// ///
/// If this region belongs to a group (by virtue of its [groupId]), all the /// If this region belongs to a group (by virtue of its [groupId]), all the
/// regions in the group will act as one. /// regions in the group will act as one.
...@@ -402,17 +416,23 @@ class TapRegion extends SingleChildRenderObjectWidget { ...@@ -402,17 +416,23 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// If there is no [RenderTapRegionSurface] ancestor in the render tree, /// If there is no [RenderTapRegionSurface] ancestor in the render tree,
/// [RenderTapRegion] will do nothing. /// [RenderTapRegion] will do nothing.
/// ///
/// The [behavior] attribute describes how to behave during hit testing when
/// deciding how the hit test propagates to children and whether to consider
/// targets behind the tap region. Defaults to [HitTestBehavior.deferToChild].
/// See [HitTestBehavior] for the allowed values and their meanings.
///
/// See also: /// See also:
/// ///
/// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render /// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render
/// tree. /// tree.
class RenderTapRegion extends RenderProxyBox { class RenderTapRegion extends RenderProxyBoxWithHitTestBehavior {
/// Creates a [RenderTapRegion]. /// Creates a [RenderTapRegion].
RenderTapRegion({ RenderTapRegion({
TapRegionRegistry? registry, TapRegionRegistry? registry,
bool enabled = true, bool enabled = true,
this.onTapOutside, this.onTapOutside,
this.onTapInside, this.onTapInside,
super.behavior = HitTestBehavior.deferToChild,
Object? groupId, Object? groupId,
String? debugLabel, String? debugLabel,
}) : _registry = registry, }) : _registry = registry,
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -99,6 +100,7 @@ void main() { ...@@ -99,6 +100,7 @@ void main() {
await click(find.text('Outside Surface')); await click(find.text('Outside Surface'));
expect(tappedOutside, isEmpty); expect(tappedOutside, isEmpty);
}); });
testWidgets('TapRegionSurface detects inside taps', (WidgetTester tester) async { testWidgets('TapRegionSurface detects inside taps', (WidgetTester tester) async {
final Set<String> tappedInside = <String>{}; final Set<String> tappedInside = <String>{};
await tester.pumpWidget( await tester.pumpWidget(
...@@ -185,6 +187,94 @@ void main() { ...@@ -185,6 +187,94 @@ void main() {
await click(find.text('Outside Surface')); await click(find.text('Outside Surface'));
expect(tappedInside, isEmpty); expect(tappedInside, isEmpty);
}); });
testWidgets('TapRegionSurface detects inside taps correctly with behavior', (WidgetTester tester) async {
final Set<String> tappedInside = <String>{};
const ValueKey<String> noGroupKey = ValueKey<String>('No Group');
const ValueKey<String> group1AKey = ValueKey<String>('Group 1 A');
const ValueKey<String> group1BKey = ValueKey<String>('Group 1 B');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
const Text('Outside Surface'),
TapRegionSurface(
child: Row(
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 100, height: 100),
child: TapRegion(
// ignore: avoid_redundant_argument_values
behavior: HitTestBehavior.deferToChild,
onTapInside: (PointerEvent event) {
tappedInside.add(noGroupKey.value);
},
child: Stack(key: noGroupKey),
),
),
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 100, height: 100),
child: TapRegion(
groupId: 1,
behavior: HitTestBehavior.opaque,
onTapInside: (PointerEvent event) {
tappedInside.add(group1AKey.value);
},
child: Stack(key: group1AKey),
),
),
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 100, height: 100),
child: TapRegion(
groupId: 1,
behavior: HitTestBehavior.translucent,
onTapInside: (PointerEvent event) {
tappedInside.add(group1BKey.value);
},
child: Stack(key: group1BKey),
),
),
],
),
),
],
),
),
);
await tester.pump();
Future<void> click(Finder finder) async {
final TestGesture gesture = await tester.startGesture(
tester.getCenter(finder),
kind: PointerDeviceKind.mouse,
);
await gesture.up();
await gesture.removePointer();
}
expect(tappedInside, isEmpty);
await click(find.byKey(noGroupKey));
expect(tappedInside, isEmpty); // No hittable children, so no hit.
await click(find.byKey(group1AKey));
// No hittable children, but set to opaque, so it hits, triggering the
// group.
expect(tappedInside,
equals(<String>{
'Group 1 A',
'Group 1 B',
}),
);
tappedInside.clear();
await click(find.byKey(group1BKey));
expect(tappedInside, isEmpty); // No hittable children while translucent, so no hit.
tappedInside.clear();
});
testWidgets('Setting the group updates the registration', (WidgetTester tester) async { testWidgets('Setting the group updates the registration', (WidgetTester tester) async {
final Set<String> tappedOutside = <String>{}; final Set<String> tappedOutside = <String>{};
await tester.pumpWidget( await tester.pumpWidget(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment