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 {
RenderBox? 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;
@override
......
......@@ -970,10 +970,14 @@ class GestureDetector extends StatelessWidget {
/// detecting screens.
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
/// [HitTestBehavior.translucent] if child is null.
///
/// See [HitTestBehavior] for the allowed values and their meanings.
final HitTestBehavior? behavior;
/// Whether to exclude these gestures from the semantics tree. For
......
......@@ -312,6 +312,7 @@ class TapRegion extends SingleChildRenderObjectWidget {
super.key,
required super.child,
this.enabled = true,
this.behavior = HitTestBehavior.deferToChild,
this.onTapOutside,
this.onTapInside,
this.groupId,
......@@ -321,6 +322,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// Whether or not this [TapRegion] is enabled as part of the composite region.
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
/// [TapRegion] and any other region with the same [groupId], if any.
///
......@@ -358,6 +367,7 @@ class TapRegion extends SingleChildRenderObjectWidget {
return RenderTapRegion(
registry: TapRegionRegistry.maybeOf(context),
enabled: enabled,
behavior: behavior,
onTapOutside: onTapOutside,
onTapInside: onTapInside,
groupId: groupId,
......@@ -367,12 +377,14 @@ class TapRegion extends SingleChildRenderObjectWidget {
@override
void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
renderObject.registry = TapRegionRegistry.maybeOf(context);
renderObject.enabled = enabled;
renderObject.groupId = groupId;
renderObject.onTapOutside = onTapOutside;
renderObject.onTapInside = onTapInside;
if (kReleaseMode) {
renderObject
..registry = TapRegionRegistry.maybeOf(context)
..enabled = enabled
..behavior = behavior
..groupId = groupId
..onTapOutside = onTapOutside
..onTapInside = onTapInside;
if (!kReleaseMode) {
renderObject.debugLabel = debugLabel;
}
}
......@@ -380,9 +392,10 @@ class TapRegion extends SingleChildRenderObjectWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder 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?>('groupId', groupId, defaultValue: null));
properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true));
}
}
......@@ -393,8 +406,9 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// system.
///
/// This render object indicates to the nearest ancestor [TapRegionSurface] that
/// the region occupied by its child will participate in the tap detection for
/// that surface.
/// the region occupied by its child (or itself if [behavior] is
/// [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
/// regions in the group will act as one.
......@@ -402,17 +416,23 @@ class TapRegion extends SingleChildRenderObjectWidget {
/// If there is no [RenderTapRegionSurface] ancestor in the render tree,
/// [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:
///
/// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render
/// tree.
class RenderTapRegion extends RenderProxyBox {
class RenderTapRegion extends RenderProxyBoxWithHitTestBehavior {
/// Creates a [RenderTapRegion].
RenderTapRegion({
TapRegionRegistry? registry,
bool enabled = true,
this.onTapOutside,
this.onTapInside,
super.behavior = HitTestBehavior.deferToChild,
Object? groupId,
String? debugLabel,
}) : _registry = registry,
......
......@@ -4,6 +4,7 @@
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -99,6 +100,7 @@ void main() {
await click(find.text('Outside Surface'));
expect(tappedOutside, isEmpty);
});
testWidgets('TapRegionSurface detects inside taps', (WidgetTester tester) async {
final Set<String> tappedInside = <String>{};
await tester.pumpWidget(
......@@ -185,6 +187,94 @@ void main() {
await click(find.text('Outside Surface'));
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 {
final Set<String> tappedOutside = <String>{};
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