| 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'; |
| 10 | library; |
| 11 | |
| 12 | import 'package:flutter/foundation.dart'; |
| 13 | import 'package:flutter/gestures.dart'; |
| 14 | import 'package:flutter/rendering.dart'; |
| 15 | |
| 16 | import 'editable_text.dart'; |
| 17 | import 'framework.dart'; |
| 18 | import 'routes.dart'; |
| 19 | |
| 20 | // Enable if you want verbose logging about tap region changes. |
| 21 | const bool _kDebugTapRegion = false; |
| 22 | |
| 23 | bool _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. |
| 43 | typedef 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. |
| 52 | typedef 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). |
| 58 | abstract 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. |
| 136 | class 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]. |
| 197 | class 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. |
| 345 | class _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. |
| 370 | class 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. |
| 558 | class 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. |
| 745 | class 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 | |