1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'app.dart';
9/// @docImport 'gesture_detector.dart';
10library;
11
12import 'package:flutter/foundation.dart';
13import 'package:flutter/gestures.dart';
14import 'package:flutter/rendering.dart';
15
16import 'editable_text.dart';
17import 'framework.dart';
18import 'routes.dart';
19
20// Enable if you want verbose logging about tap region changes.
21const bool _kDebugTapRegion = false;
22
23bool _tapRegionDebug(String message, [Iterable<String>? details]) {
24 if (_kDebugTapRegion) {
25 debugPrint('TAP REGION: $message');
26 if (details != null && details.isNotEmpty) {
27 for (final String detail in details) {
28 debugPrint(' $detail');
29 }
30 }
31 }
32 // Return true so that it can be easily used inside of an assert.
33 return true;
34}
35
36/// Signature for a callback called for a [PointerDownEvent] relative to a [TapRegion].
37///
38/// See also:
39///
40/// * [TapRegion.onTapOutside], which is of this type.
41/// * [TapRegion.onTapInside], which is of this type.
42/// * [TapRegionUpCallback], which is similar but for [PointerUpEvent]s.
43typedef TapRegionCallback = void Function(PointerDownEvent event);
44
45/// Signature for a callback called for a [PointerUpEvent] relative to a [TapRegion].
46///
47/// See also:
48///
49/// * [TapRegion.onTapUpOutside], which is of this type.
50/// * [TapRegion.onTapUpInside], which is of this type.
51/// * [TapRegionCallback], which is similar but for [PointerDownEvent]s.
52typedef TapRegionUpCallback = void Function(PointerUpEvent event);
53
54/// An interface for registering and unregistering a [RenderTapRegion]
55/// (typically created with a [TapRegion] widget) with a
56/// [RenderTapRegionSurface] (typically created with a [TapRegionSurface]
57/// widget).
58abstract class TapRegionRegistry {
59 /// Register the given [RenderTapRegion] with the registry.
60 void registerTapRegion(RenderTapRegion region);
61
62 /// Unregister the given [RenderTapRegion] with the registry.
63 void unregisterTapRegion(RenderTapRegion region);
64
65 /// Allows finding of the nearest [TapRegionRegistry], such as a
66 /// [RenderTapRegionSurface].
67 ///
68 /// Will throw if a [TapRegionRegistry] isn't found.
69 static TapRegionRegistry of(BuildContext context) {
70 final TapRegionRegistry? registry = maybeOf(context);
71 assert(() {
72 if (registry == null) {
73 throw FlutterError(
74 'TapRegionRegistry.of() was called with a context that does not contain a TapRegionSurface widget.\n'
75 'No TapRegionSurface widget ancestor could be found starting from the context that was passed to '
76 'TapRegionRegistry.of().\n'
77 'The context used was:\n'
78 ' $context',
79 );
80 }
81 return true;
82 }());
83 return registry!;
84 }
85
86 /// Allows finding of the nearest [TapRegionRegistry], such as a
87 /// [RenderTapRegionSurface].
88 static TapRegionRegistry? maybeOf(BuildContext context) {
89 return context.findAncestorRenderObjectOfType<RenderTapRegionSurface>();
90 }
91}
92
93/// A widget that provides notification of a tap inside or outside of a set of
94/// registered regions, without participating in the [gesture
95/// disambiguation](https://flutter.dev/to/gesture-disambiguation)
96/// system.
97///
98/// The regions are defined by adding [TapRegion] widgets to the widget tree
99/// around the regions of interest, and they will register with this
100/// [TapRegionSurface]. Each of the tap regions can optionally belong to a group
101/// by assigning a [TapRegion.groupId], where all the regions with the same
102/// groupId act as if they were all one region.
103///
104/// When a tap down or tap up outside of a registered region or region group is
105/// detected, its [TapRegion.onTapOutside] or [TapRegion.onTapUpOutside]
106/// callback is called, respectively. If the tap is outside one member of a
107/// group, but inside another, no notification is made.
108///
109/// When a tap down or tap up inside of a registered region or region group is
110/// detected, its [TapRegion.onTapInside] or [TapRegion.onTapUpInside]
111/// callback is called, respectively. If the tap is inside one member of a
112/// group, all members are notified.
113///
114/// The [TapRegionSurface] should be defined at the highest level needed to
115/// encompass the entire area where taps should be monitored. This is typically
116/// around the entire app. If the entire app isn't covered, then taps outside of
117/// the [TapRegionSurface] will be ignored and no [TapRegion.onTapOutside] or
118/// [TapRegion.onTapUpOutside] calls will be made for those events. The
119/// [WidgetsApp], [MaterialApp] and [CupertinoApp] automatically include a
120/// [TapRegionSurface] around their entire app.
121///
122/// [TapRegionSurface] does not participate in the [gesture
123/// disambiguation](https://flutter.dev/to/gesture-disambiguation)
124/// system, so if multiple [TapRegionSurface]s are active at the same time, they
125/// will all fire, and so will any other gestures recognized by a
126/// [GestureDetector] or other pointer event handlers.
127///
128/// [TapRegion]s register only with the nearest ancestor [TapRegionSurface].
129///
130/// See also:
131///
132/// * [RenderTapRegionSurface], the render object that is inserted into the
133/// render tree by this widget.
134/// * <https://flutter.dev/to/gesture-disambiguation> for more
135/// information about the gesture system and how it disambiguates inputs.
136class TapRegionSurface extends SingleChildRenderObjectWidget {
137 /// Creates a const [RenderTapRegionSurface].
138 ///
139 /// The [child] attribute is required.
140 const TapRegionSurface({super.key, required Widget super.child});
141
142 @override
143 RenderObject createRenderObject(BuildContext context) {
144 return RenderTapRegionSurface();
145 }
146
147 @override
148 void updateRenderObject(BuildContext context, RenderProxyBoxWithHitTestBehavior renderObject) {}
149}
150
151/// A render object that provides notification of a tap inside or outside of a
152/// set of registered regions, without participating in the [gesture
153/// disambiguation](https://flutter.dev/to/gesture-disambiguation) system
154/// (other than to consume tap down events if [TapRegion.consumeOutsideTaps] is
155/// true).
156///
157/// The regions are defined by adding [RenderTapRegion] render objects in the
158/// render tree around the regions of interest, and they will register with this
159/// [RenderTapRegionSurface]. Each of the tap regions can optionally belong to a
160/// group by assigning a [RenderTapRegion.groupId], where all the regions with
161/// the same groupId act as if they were all one region.
162///
163/// When a tap down or tap up outside of a registered region or region group is
164/// detected, its [TapRegion.onTapOutside] or [TapRegion.onTapUpOutside]
165/// callback is called, respectively. If the tap is outside one member of a
166/// group, but inside another, no notification is made.
167///
168/// When a tap down or tap up inside of a registered region or region group is
169/// detected, its [TapRegion.onTapInside] or [TapRegion.onTapUpInside]
170/// callback is called, respectively. If the tap is inside one member of a
171/// group, all members are notified.
172///
173/// The [RenderTapRegionSurface] should be defined at the highest level needed
174/// to encompass the entire area where taps should be monitored. This is
175/// typically around the entire app. If the entire app isn't covered, then taps
176/// outside of the [RenderTapRegionSurface] will be ignored and no
177/// [RenderTapRegion.onTapOutside] or [RenderTapRegion.onTapUpOutside] calls
178/// will be made for those events. The [WidgetsApp], [MaterialApp] and
179/// [CupertinoApp] automatically include a [RenderTapRegionSurface] around the
180/// entire app.
181///
182/// [RenderTapRegionSurface] does not participate in the [gesture
183/// disambiguation](https://flutter.dev/to/gesture-disambiguation)
184/// system, so if multiple [RenderTapRegionSurface]s are active at the same
185/// time, they will all fire, and so will any other gestures recognized by a
186/// [GestureDetector] or other pointer event handlers.
187///
188/// [RenderTapRegion]s register only with the nearest ancestor
189/// [RenderTapRegionSurface].
190///
191/// See also:
192///
193/// * [TapRegionSurface], a widget that inserts a [RenderTapRegionSurface] into
194/// the render tree.
195/// * [TapRegionRegistry.of], which can find the nearest ancestor
196/// [RenderTapRegionSurface], which is a [TapRegionRegistry].
197class RenderTapRegionSurface extends RenderProxyBoxWithHitTestBehavior
198 implements TapRegionRegistry {
199 final Expando<BoxHitTestResult> _cachedResults = Expando<BoxHitTestResult>();
200 final Set<RenderTapRegion> _registeredRegions = <RenderTapRegion>{};
201 final Map<Object?, Set<RenderTapRegion>> _groupIdToRegions = <Object?, Set<RenderTapRegion>>{};
202
203 @override
204 void registerTapRegion(RenderTapRegion region) {
205 assert(_tapRegionDebug('Region $region registered.'));
206 assert(!_registeredRegions.contains(region));
207 _registeredRegions.add(region);
208 if (region.groupId != null) {
209 _groupIdToRegions[region.groupId] ??= <RenderTapRegion>{};
210 _groupIdToRegions[region.groupId]!.add(region);
211 }
212 }
213
214 @override
215 void unregisterTapRegion(RenderTapRegion region) {
216 assert(_tapRegionDebug('Region $region unregistered.'));
217 assert(_registeredRegions.contains(region));
218 _registeredRegions.remove(region);
219 if (region.groupId != null) {
220 assert(_groupIdToRegions.containsKey(region.groupId));
221 _groupIdToRegions[region.groupId]!.remove(region);
222 if (_groupIdToRegions[region.groupId]!.isEmpty) {
223 _groupIdToRegions.remove(region.groupId);
224 }
225 }
226 }
227
228 @override
229 bool hitTest(BoxHitTestResult result, {required Offset position}) {
230 if (!size.contains(position)) {
231 return false;
232 }
233
234 final bool hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
235
236 if (hitTarget) {
237 final BoxHitTestEntry entry = BoxHitTestEntry(this, position);
238 _cachedResults[entry] = result;
239 result.add(entry);
240 }
241
242 return hitTarget;
243 }
244
245 @override
246 void handleEvent(PointerEvent event, HitTestEntry entry) {
247 assert(debugHandleEvent(event, entry));
248 assert(() {
249 for (final RenderTapRegion region in _registeredRegions) {
250 if (!region.enabled) {
251 return false;
252 }
253 }
254 return true;
255 }(), 'A RenderTapRegion was registered when it was disabled.');
256
257 if (event is! PointerDownEvent && event is! PointerUpEvent) {
258 return;
259 }
260
261 if (_registeredRegions.isEmpty) {
262 assert(_tapRegionDebug('Ignored tap event because no regions are registered.'));
263 return;
264 }
265
266 final BoxHitTestResult? result = _cachedResults[entry];
267
268 if (result == null) {
269 assert(_tapRegionDebug('Ignored tap event because no surface descendants were hit.'));
270 return;
271 }
272
273 // A child was hit, so we need to call onTapOutside / onTapUpOutside for
274 // those regions or groups of regions that were not hit.
275 final Set<RenderTapRegion> hitRegions = _getRegionsHit(
276 _registeredRegions,
277 result.path,
278 ).cast<RenderTapRegion>().toSet();
279 assert(_tapRegionDebug('Tap event hit ${hitRegions.length} descendants.'));
280
281 final Set<RenderTapRegion> insideRegions = <RenderTapRegion>{
282 for (final RenderTapRegion region in hitRegions)
283 if (region.groupId == null)
284 region
285 // Adding all grouped regions, so they act as a single region.
286 else
287 ..._groupIdToRegions[region.groupId]!,
288 };
289 // If they're not inside, then they're outside.
290 final Set<RenderTapRegion> outsideRegions = _registeredRegions.difference(insideRegions);
291
292 bool consumeOutsideTaps = false;
293 for (final RenderTapRegion region in outsideRegions) {
294 if (event is PointerDownEvent) {
295 assert(_tapRegionDebug('Calling onTapOutside for $region'));
296 region.onTapOutside?.call(event);
297 } else if (event is PointerUpEvent) {
298 assert(_tapRegionDebug('Calling onTapUpOutside for $region'));
299 region.onTapUpOutside?.call(event);
300 }
301
302 if (region.consumeOutsideTaps) {
303 assert(
304 _tapRegionDebug('Stopping tap propagation for $region (and all of ${region.groupId})'),
305 );
306 consumeOutsideTaps = true;
307 }
308 }
309 for (final RenderTapRegion region in insideRegions) {
310 if (event is PointerDownEvent) {
311 assert(_tapRegionDebug('Calling onTapInside for $region'));
312 region.onTapInside?.call(event);
313 } else if (event is PointerUpEvent) {
314 assert(_tapRegionDebug('Calling onTapUpInside for $region'));
315 region.onTapUpInside?.call(event);
316 }
317 }
318
319 // If any of the "outside" regions have consumeOutsideTaps set, then stop
320 // the propagation of the event through the gesture recognizer by adding it
321 // to the recognizer and immediately resolving it.
322 if (consumeOutsideTaps && event is PointerDownEvent) {
323 GestureBinding.instance.gestureArena
324 .add(event.pointer, _DummyTapRecognizer())
325 .resolve(GestureDisposition.accepted);
326 }
327 }
328
329 // Returns the registered regions that are in the hit path.
330 Set<HitTestTarget> _getRegionsHit(
331 Set<RenderTapRegion> detectors,
332 Iterable<HitTestEntry> hitTestPath,
333 ) {
334 return <HitTestTarget>{
335 for (final HitTestEntry<HitTestTarget> entry in hitTestPath)
336 if (entry.target case final HitTestTarget target)
337 if (_registeredRegions.contains(target)) target,
338 };
339 }
340}
341
342// A dummy tap recognizer so that we don't have to deal with the lifecycle of
343// TapGestureRecognizer, since we're just going to immediately resolve it
344// anyhow.
345class _DummyTapRecognizer extends GestureArenaMember {
346 @override
347 void acceptGesture(int pointer) {}
348
349 @override
350 void rejectGesture(int pointer) {}
351}
352
353/// A widget that defines a region that can detect taps inside or outside of
354/// itself and any group of regions it belongs to, without participating in the
355/// [gesture
356/// disambiguation](https://flutter.dev/to/gesture-disambiguation) system
357/// (other than to consume tap down events if [consumeOutsideTaps] is true).
358///
359/// This widget indicates to the nearest ancestor [TapRegionSurface] that the
360/// region occupied by its child will participate in the tap detection for that
361/// surface.
362///
363/// If this region belongs to a group (by virtue of its [groupId]), all the
364/// regions in the group will act as one.
365///
366/// If there is no [TapRegionSurface] ancestor, [TapRegion] will do nothing.
367///
368/// [TapRegion] is aware of the [Route]s in the [Navigator], so that [onTapOutside]
369/// or [onTapUpOutside] isn't called after the user navigates to a different page.
370class TapRegion extends SingleChildRenderObjectWidget {
371 /// Creates a const [TapRegion].
372 ///
373 /// The [child] argument is required.
374 const TapRegion({
375 super.key,
376 required super.child,
377 this.enabled = true,
378 this.behavior = HitTestBehavior.deferToChild,
379 this.onTapOutside,
380 this.onTapInside,
381 this.onTapUpOutside,
382 this.onTapUpInside,
383 this.groupId,
384 this.consumeOutsideTaps = false,
385 String? debugLabel,
386 }) : debugLabel = kReleaseMode ? null : debugLabel;
387
388 /// Whether or not this [TapRegion] is enabled as part of the composite region.
389 final bool enabled;
390
391 /// How to behave during hit testing when deciding how the hit test propagates
392 /// to children and whether to consider targets behind this [TapRegion].
393 ///
394 /// Defaults to [HitTestBehavior.deferToChild].
395 ///
396 /// See [HitTestBehavior] for the allowed values and their meanings.
397 final HitTestBehavior behavior;
398
399 /// A callback to be invoked when a tap down is detected outside of this
400 /// [TapRegion] and any other region with the same [groupId], if any.
401 ///
402 /// The [PointerDownEvent] passed to the function is the event that caused the
403 /// notification. If this region is part of a group (i.e. [groupId] is set),
404 /// then it's possible that the event may be outside of this immediate region,
405 /// although it will be within the region of one of the group members.
406 ///
407 /// See also:
408 /// * [onTapUpOutside], which is called when a tap up is detected outside
409 /// of this region.
410 final TapRegionCallback? onTapOutside;
411
412 /// A callback to be invoked when a tap down is detected inside of this
413 /// [TapRegion], or any other tap region with the same [groupId], if any.
414 ///
415 /// The [PointerDownEvent] passed to the function is the event that caused the
416 /// notification. If this region is part of a group (i.e. [groupId] is set),
417 /// then it's possible that the event may be outside of this immediate region,
418 /// although it will be within the region of one of the group members.
419 ///
420 /// See also:
421 /// * [onTapUpInside], which is called when a tap up is detected inside
422 /// of this region.
423 final TapRegionCallback? onTapInside;
424
425 /// A callback to be invoked when a tap up is detected outside of this
426 /// [TapRegion] and any other region with the same [groupId], if any.
427 ///
428 /// The [PointerUpEvent] passed to the function is the event that caused the
429 /// notification. If this region is part of a group (i.e. [groupId] is set),
430 /// then it's possible that the event may be outside of this immediate region,
431 /// although it will be within the region of one of the group members.
432 ///
433 /// See also:
434 /// * [onTapOutside], which is called when a tap down is detected outside
435 /// of this region.
436 final TapRegionUpCallback? onTapUpOutside;
437
438 /// A callback to be invoked when a tap up is detected inside of this
439 /// [TapRegion], or any other tap region with the same [groupId], if any.
440 ///
441 /// The [PointerUpEvent] passed to the function is the event that caused the
442 /// notification. If this region is part of a group (i.e. [groupId] is set),
443 /// then it's possible that the event may be outside of this immediate region,
444 /// although it will be within the region of one of the group members.
445 ///
446 /// See also:
447 /// * [onTapInside], which is called when a tap down is detected inside
448 /// of this region.
449 final TapRegionUpCallback? onTapUpInside;
450
451 /// An optional group ID that groups [TapRegion]s together so that they
452 /// operate as one region. If any member of a group is hit by a particular
453 /// tap, then the [onTapOutside] / [onTapUpOutside] will not be called for
454 /// any members of the group. If any member of the group is hit, then all
455 /// members will have their [onTapInside] / [onTapUpInside] called.
456 ///
457 /// If the group id is null, then only this region is hit tested.
458 final Object? groupId;
459
460 /// If true, then the group that this region belongs to will stop the
461 /// propagation of all events in the gesture arena.
462 ///
463 /// This is useful if you want to block events from being given to a
464 /// [GestureDetector] when [onTapOutside] is called.
465 ///
466 /// If other [TapRegion]s with the same [groupId] have [consumeOutsideTaps]
467 /// set to false, but this one is true, then this one will take precedence,
468 /// and the event will be consumed.
469 ///
470 /// Defaults to false.
471 final bool consumeOutsideTaps;
472
473 /// An optional debug label to help with debugging in debug mode.
474 ///
475 /// Will be null in release mode.
476 final String? debugLabel;
477
478 @override
479 RenderObject createRenderObject(BuildContext context) {
480 final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
481
482 return RenderTapRegion(
483 registry: TapRegionRegistry.maybeOf(context),
484 enabled: enabled,
485 consumeOutsideTaps: isCurrent && consumeOutsideTaps,
486 behavior: behavior,
487 onTapOutside: isCurrent ? onTapOutside : null,
488 onTapInside: onTapInside,
489 onTapUpOutside: isCurrent ? onTapUpOutside : null,
490 onTapUpInside: onTapUpInside,
491 groupId: groupId,
492 debugLabel: debugLabel,
493 );
494 }
495
496 @override
497 void updateRenderObject(BuildContext context, covariant RenderTapRegion renderObject) {
498 final bool isCurrent = ModalRoute.isCurrentOf(context) ?? true;
499
500 renderObject
501 ..registry = TapRegionRegistry.maybeOf(context)
502 ..enabled = enabled
503 ..consumeOutsideTaps = isCurrent && consumeOutsideTaps
504 ..behavior = behavior
505 ..groupId = groupId
506 ..onTapOutside = isCurrent ? onTapOutside : null
507 ..onTapInside = onTapInside
508 ..onTapUpOutside = isCurrent ? onTapUpOutside : null
509 ..onTapUpInside = onTapUpInside;
510 if (!kReleaseMode) {
511 renderObject.debugLabel = debugLabel;
512 }
513 }
514
515 @override
516 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
517 super.debugFillProperties(properties);
518 properties.add(
519 FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true),
520 );
521 properties.add(
522 DiagnosticsProperty<HitTestBehavior>(
523 'behavior',
524 behavior,
525 defaultValue: HitTestBehavior.deferToChild,
526 ),
527 );
528 properties.add(DiagnosticsProperty<Object?>('debugLabel', debugLabel, defaultValue: null));
529 properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null));
530 }
531}
532
533/// A render object that defines a region that can detect taps inside or outside
534/// of itself and any group of regions it belongs to, without participating in
535/// the [gesture
536/// disambiguation](https://flutter.dev/to/gesture-disambiguation) system.
537///
538/// This render object indicates to the nearest ancestor [TapRegionSurface] that
539/// the region occupied by its child (or itself if [behavior] is
540/// [HitTestBehavior.opaque]) will participate in the tap detection for that
541/// surface.
542///
543/// If this region belongs to a group (by virtue of its [groupId]), all the
544/// regions in the group will act as one.
545///
546/// If there is no [RenderTapRegionSurface] ancestor in the render tree,
547/// [RenderTapRegion] will do nothing.
548///
549/// The [behavior] attribute describes how to behave during hit testing when
550/// deciding how the hit test propagates to children and whether to consider
551/// targets behind the tap region. Defaults to [HitTestBehavior.deferToChild].
552/// See [HitTestBehavior] for the allowed values and their meanings.
553///
554/// See also:
555///
556/// * [TapRegion], a widget that inserts a [RenderTapRegion] into the render
557/// tree.
558class RenderTapRegion extends RenderProxyBoxWithHitTestBehavior {
559 /// Creates a [RenderTapRegion].
560 RenderTapRegion({
561 TapRegionRegistry? registry,
562 bool enabled = true,
563 bool consumeOutsideTaps = false,
564 this.onTapOutside,
565 this.onTapInside,
566 this.onTapUpOutside,
567 this.onTapUpInside,
568 super.behavior = HitTestBehavior.deferToChild,
569 Object? groupId,
570 String? debugLabel,
571 }) : _registry = registry,
572 _enabled = enabled,
573 _consumeOutsideTaps = consumeOutsideTaps,
574 _groupId = groupId,
575 debugLabel = kReleaseMode ? null : debugLabel;
576
577 bool _isRegistered = false;
578
579 /// A callback to be invoked when a tap down is detected outside of this
580 /// [RenderTapRegion] and any other region with the same [groupId], if any.
581 ///
582 /// The [PointerDownEvent] passed to the function is the event that caused the
583 /// notification. If this region is part of a group (i.e. [groupId] is set),
584 /// then it's possible that the event may be outside of this immediate region,
585 /// although it will be within the region of one of the group members.
586 TapRegionCallback? onTapOutside;
587
588 /// A callback to be invoked when a tap down is detected inside of this
589 /// [RenderTapRegion], or any other tap region with the same [groupId], if any.
590 ///
591 /// The [PointerDownEvent] passed to the function is the event that caused the
592 /// notification. If this region is part of a group (i.e. [groupId] is set),
593 /// then it's possible that the event may be outside of this immediate region,
594 /// although it will be within the region of one of the group members.
595 TapRegionCallback? onTapInside;
596
597 /// A callback to be invoked when a tap up is detected outside of this
598 /// [RenderTapRegion] and any other region with the same [groupId], if any.
599 ///
600 /// The [PointerUpEvent] passed to the function is the event that caused the
601 /// notification. If this region is part of a group (i.e. [groupId] is set),
602 /// then it's possible that the event may be outside of this immediate region,
603 /// although it will be within the region of one of the group members.
604 TapRegionUpCallback? onTapUpOutside;
605
606 /// A callback to be invoked when a tap up is detected inside of this
607 /// [RenderTapRegion], or any other tap region with the same [groupId], if any.
608 ///
609 /// The [PointerUpEvent] passed to the function is the event that caused the
610 /// notification. If this region is part of a group (i.e. [groupId] is set),
611 /// then it's possible that the event may be outside of this immediate region,
612 /// although it will be within the region of one of the group members.
613 TapRegionUpCallback? onTapUpInside;
614
615 /// A label used in debug builds. Will be null in release builds.
616 String? debugLabel;
617
618 /// Whether or not this region should participate in the composite region.
619 bool get enabled => _enabled;
620 bool _enabled;
621 set enabled(bool value) {
622 if (_enabled != value) {
623 _enabled = value;
624 markNeedsLayout();
625 }
626 }
627
628 /// Whether or not the tap event that triggers a call to [onTapOutside]
629 /// or [onTapUpOutside] will continue on to participate in the gesture arena.
630 ///
631 /// If any [RenderTapRegion] in the same group has [consumeOutsideTaps] set to
632 /// true, then the tap down event will be consumed before other gesture
633 /// recognizers can process them.
634 bool get consumeOutsideTaps => _consumeOutsideTaps;
635 bool _consumeOutsideTaps;
636 set consumeOutsideTaps(bool value) {
637 if (_consumeOutsideTaps != value) {
638 _consumeOutsideTaps = value;
639 markNeedsLayout();
640 }
641 }
642
643 /// An optional group ID that groups [RenderTapRegion]s together so that they
644 /// operate as one region. If any member of a group is hit by a particular
645 /// tap, then the [onTapOutside] / [onTapUpOutside] will not be called for
646 /// any members of the group. If any member of the group is hit, then all
647 /// members will have their [onTapInside] / [onTapUpInside] called.
648 ///
649 /// If the group id is null, then only this region is hit tested.
650 Object? get groupId => _groupId;
651 Object? _groupId;
652 set groupId(Object? value) {
653 if (_groupId != value) {
654 // If the group changes, we need to unregister and re-register under the
655 // new group. The re-registration happens automatically in layout().
656 if (_isRegistered) {
657 _registry!.unregisterTapRegion(this);
658 _isRegistered = false;
659 }
660 _groupId = value;
661 markNeedsLayout();
662 }
663 }
664
665 /// The registry that this [RenderTapRegion] should register with.
666 ///
667 /// If the [registry] is null, then this region will not be registered
668 /// anywhere, and will not do any tap detection.
669 ///
670 /// A [RenderTapRegionSurface] is a [TapRegionRegistry].
671 TapRegionRegistry? get registry => _registry;
672 TapRegionRegistry? _registry;
673 set registry(TapRegionRegistry? value) {
674 if (_registry != value) {
675 if (_isRegistered) {
676 _registry!.unregisterTapRegion(this);
677 _isRegistered = false;
678 }
679 _registry = value;
680 markNeedsLayout();
681 }
682 }
683
684 @override
685 void layout(Constraints constraints, {bool parentUsesSize = false}) {
686 super.layout(constraints, parentUsesSize: parentUsesSize);
687 if (_registry == null) {
688 return;
689 }
690 if (_isRegistered) {
691 _registry!.unregisterTapRegion(this);
692 }
693 final bool shouldBeRegistered = _enabled && _registry != null;
694 if (shouldBeRegistered) {
695 _registry!.registerTapRegion(this);
696 }
697 _isRegistered = shouldBeRegistered;
698 }
699
700 @override
701 void dispose() {
702 if (_isRegistered) {
703 _registry!.unregisterTapRegion(this);
704 }
705 super.dispose();
706 }
707
708 @override
709 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
710 super.debugFillProperties(properties);
711 properties.add(DiagnosticsProperty<String?>('debugLabel', debugLabel, defaultValue: null));
712 properties.add(DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null));
713 properties.add(
714 FlagProperty('enabled', value: enabled, ifFalse: 'DISABLED', defaultValue: true),
715 );
716 }
717}
718
719/// A [TapRegion] that adds its children to the tap region group for widgets
720/// based on the [EditableText] text editing widget, such as [TextField] and
721/// [CupertinoTextField].
722///
723/// Widgets that are wrapped with a [TextFieldTapRegion] are considered to be
724/// part of a text field for purposes of unfocus behavior. So, when the user
725/// taps on them, the currently focused text field won't be unfocused by
726/// default. This allows controls like spinners, copy buttons, and formatting
727/// buttons to be associated with a text field without causing the text field to
728/// lose focus when they are interacted with.
729///
730/// {@tool dartpad}
731/// This example shows how to use a [TextFieldTapRegion] to wrap a set of
732/// "spinner" buttons that increment and decrement a value in the text field
733/// without causing the text field to lose keyboard focus.
734///
735/// This example includes a generic `SpinnerField<T>` class that you can copy/paste
736/// into your own project and customize.
737///
738/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
739/// {@end-tool}
740///
741/// See also:
742///
743/// * [TapRegion], the widget that this widget uses to add widgets to the group
744/// of text fields.
745class TextFieldTapRegion extends TapRegion {
746 /// Creates a const [TextFieldTapRegion].
747 ///
748 /// The [child] field is required.
749 const TextFieldTapRegion({
750 super.key,
751 required super.child,
752 super.enabled,
753 super.onTapOutside,
754 super.onTapInside,
755 super.onTapUpOutside,
756 super.onTapUpInside,
757 super.consumeOutsideTaps,
758 super.debugLabel,
759 super.groupId = EditableText,
760 });
761}
762