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