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 | import 'package:flutter/services.dart'; |
9 | |
10 | import 'basic.dart'; |
11 | import 'debug.dart'; |
12 | import 'framework.dart'; |
13 | import 'gesture_detector.dart'; |
14 | import 'navigator.dart'; |
15 | import 'transitions.dart'; |
16 | |
17 | /// A widget that modifies the size of the [SemanticsNode.rect] created by its |
18 | /// child widget. |
19 | /// |
20 | /// It clips the focus in potentially four directions based on the |
21 | /// specified [EdgeInsets]. |
22 | /// |
23 | /// The size of the accessibility focus is adjusted based on value changes |
24 | /// inside the given [ValueNotifier]. |
25 | /// |
26 | /// See also: |
27 | /// |
28 | /// * [ModalBarrier], which utilizes this widget to adjust the barrier focus |
29 | /// size based on the size of the content layer rendered on top of it. |
30 | class _SemanticsClipper extends SingleChildRenderObjectWidget{ |
31 | /// creates a [SemanticsClipper] that updates the size of the |
32 | /// [SemanticsNode.rect] of its child based on the value inside the provided |
33 | /// [ValueNotifier], or a default value of [EdgeInsets.zero]. |
34 | const _SemanticsClipper({ |
35 | super.child, |
36 | required this.clipDetailsNotifier, |
37 | }); |
38 | |
39 | /// The [ValueNotifier] whose value determines how the child's |
40 | /// [SemanticsNode.rect] should be clipped in four directions. |
41 | final ValueNotifier<EdgeInsets> clipDetailsNotifier; |
42 | |
43 | @override |
44 | _RenderSemanticsClipper createRenderObject(BuildContext context) { |
45 | return _RenderSemanticsClipper(clipDetailsNotifier: clipDetailsNotifier,); |
46 | } |
47 | |
48 | @override |
49 | void updateRenderObject(BuildContext context, _RenderSemanticsClipper renderObject) { |
50 | renderObject.clipDetailsNotifier = clipDetailsNotifier; |
51 | } |
52 | } |
53 | /// Updates the [SemanticsNode.rect] of its child based on the value inside |
54 | /// provided [ValueNotifier]. |
55 | class _RenderSemanticsClipper extends RenderProxyBox { |
56 | /// Creates a [RenderProxyBox] that Updates the [SemanticsNode.rect] of its child |
57 | /// based on the value inside provided [ValueNotifier]. |
58 | _RenderSemanticsClipper({ |
59 | required ValueNotifier<EdgeInsets> clipDetailsNotifier, |
60 | RenderBox? child, |
61 | }) : _clipDetailsNotifier = clipDetailsNotifier, |
62 | super(child); |
63 | |
64 | ValueNotifier<EdgeInsets> _clipDetailsNotifier; |
65 | |
66 | /// The getter and setter retrieves / updates the [ValueNotifier] associated |
67 | /// with this clipper. |
68 | ValueNotifier<EdgeInsets> get clipDetailsNotifier => _clipDetailsNotifier; |
69 | set clipDetailsNotifier (ValueNotifier<EdgeInsets> newNotifier) { |
70 | if (_clipDetailsNotifier == newNotifier) { |
71 | return; |
72 | } |
73 | if (attached) { |
74 | _clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate); |
75 | } |
76 | _clipDetailsNotifier = newNotifier; |
77 | _clipDetailsNotifier.addListener(markNeedsSemanticsUpdate); |
78 | markNeedsSemanticsUpdate(); |
79 | } |
80 | |
81 | @override |
82 | Rect get semanticBounds { |
83 | final EdgeInsets clipDetails = _clipDetailsNotifier.value; |
84 | final Rect originalRect = super.semanticBounds; |
85 | final Rect clippedRect = Rect.fromLTRB( |
86 | originalRect.left + clipDetails.left, |
87 | originalRect.top + clipDetails.top, |
88 | originalRect.right - clipDetails.right, |
89 | originalRect.bottom - clipDetails.bottom, |
90 | ); |
91 | return clippedRect; |
92 | } |
93 | |
94 | @override |
95 | void attach(PipelineOwner owner) { |
96 | super.attach(owner); |
97 | clipDetailsNotifier.addListener(markNeedsSemanticsUpdate); |
98 | } |
99 | |
100 | @override |
101 | void detach() { |
102 | clipDetailsNotifier.removeListener(markNeedsSemanticsUpdate); |
103 | super.detach(); |
104 | } |
105 | |
106 | @override |
107 | void describeSemanticsConfiguration(SemanticsConfiguration config) { |
108 | super.describeSemanticsConfiguration(config); |
109 | config.isSemanticBoundary = true; |
110 | } |
111 | } |
112 | |
113 | /// A widget that prevents the user from interacting with widgets behind itself. |
114 | /// |
115 | /// The modal barrier is the scrim that is rendered behind each route, which |
116 | /// generally prevents the user from interacting with the route below the |
117 | /// current route, and normally partially obscures such routes. |
118 | /// |
119 | /// For example, when a dialog is on the screen, the page below the dialog is |
120 | /// usually darkened by the modal barrier. |
121 | /// |
122 | /// See also: |
123 | /// |
124 | /// * [ModalRoute], which indirectly uses this widget. |
125 | /// * [AnimatedModalBarrier], which is similar but takes an animated [color] |
126 | /// instead of a single color value. |
127 | class ModalBarrier extends StatelessWidget { |
128 | /// Creates a widget that blocks user interaction. |
129 | const ModalBarrier({ |
130 | super.key, |
131 | this.color, |
132 | this.dismissible = true, |
133 | this.onDismiss, |
134 | this.semanticsLabel, |
135 | this.barrierSemanticsDismissible = true, |
136 | this.clipDetailsNotifier, |
137 | this.semanticsOnTapHint, |
138 | }); |
139 | |
140 | /// If non-null, fill the barrier with this color. |
141 | /// |
142 | /// See also: |
143 | /// |
144 | /// * [ModalRoute.barrierColor], which controls this property for the |
145 | /// [ModalBarrier] built by [ModalRoute] pages. |
146 | final Color? color; |
147 | |
148 | /// Specifies if the barrier will be dismissed when the user taps on it. |
149 | /// |
150 | /// If true, and [onDismiss] is non-null, [onDismiss] will be called, |
151 | /// otherwise the current route will be popped from the ambient [Navigator]. |
152 | /// |
153 | /// If false, tapping on the barrier will do nothing. |
154 | /// |
155 | /// See also: |
156 | /// |
157 | /// * [ModalRoute.barrierDismissible], which controls this property for the |
158 | /// [ModalBarrier] built by [ModalRoute] pages. |
159 | final bool dismissible; |
160 | |
161 | /// {@template flutter.widgets.ModalBarrier.onDismiss} |
162 | /// Called when the barrier is being dismissed. |
163 | /// |
164 | /// If non-null [onDismiss] will be called in place of popping the current |
165 | /// route. It is up to the callback to handle dismissing the barrier. |
166 | /// |
167 | /// If null, the ambient [Navigator]'s current route will be popped. |
168 | /// |
169 | /// This field is ignored if [dismissible] is false. |
170 | /// {@endtemplate} |
171 | final VoidCallback? onDismiss; |
172 | |
173 | /// Whether the modal barrier semantics are included in the semantics tree. |
174 | /// |
175 | /// See also: |
176 | /// |
177 | /// * [ModalRoute.semanticsDismissible], which controls this property for |
178 | /// the [ModalBarrier] built by [ModalRoute] pages. |
179 | final bool? barrierSemanticsDismissible; |
180 | |
181 | /// Semantics label used for the barrier if it is [dismissible]. |
182 | /// |
183 | /// The semantics label is read out by accessibility tools (e.g. TalkBack |
184 | /// on Android and VoiceOver on iOS) when the barrier is focused. |
185 | /// |
186 | /// See also: |
187 | /// |
188 | /// * [ModalRoute.barrierLabel], which controls this property for the |
189 | /// [ModalBarrier] built by [ModalRoute] pages. |
190 | final String? semanticsLabel; |
191 | |
192 | /// {@template flutter.widgets.ModalBarrier.clipDetailsNotifier} |
193 | /// Contains a value of type [EdgeInsets] that specifies how the |
194 | /// [SemanticsNode.rect] of the widget should be clipped. |
195 | /// |
196 | /// See also: |
197 | /// |
198 | /// * [_SemanticsClipper], which utilizes the value inside to update the |
199 | /// [SemanticsNode.rect] for its child. |
200 | /// {@endtemplate} |
201 | final ValueNotifier<EdgeInsets>? clipDetailsNotifier; |
202 | |
203 | /// {@macro flutter.material.ModalBottomSheetRoute.barrierOnTapHint} |
204 | final String? semanticsOnTapHint; |
205 | |
206 | @override |
207 | Widget build(BuildContext context) { |
208 | assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context)); |
209 | final bool platformSupportsDismissingBarrier; |
210 | switch (defaultTargetPlatform) { |
211 | case TargetPlatform.fuchsia: |
212 | case TargetPlatform.linux: |
213 | case TargetPlatform.windows: |
214 | platformSupportsDismissingBarrier = false; |
215 | case TargetPlatform.android: |
216 | case TargetPlatform.iOS: |
217 | case TargetPlatform.macOS: |
218 | platformSupportsDismissingBarrier = true; |
219 | } |
220 | final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier; |
221 | final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible; |
222 | |
223 | void handleDismiss() { |
224 | if (dismissible) { |
225 | if (onDismiss != null) { |
226 | onDismiss!(); |
227 | } else { |
228 | Navigator.maybePop(context); |
229 | } |
230 | } else { |
231 | SystemSound.play(SystemSoundType.alert); |
232 | } |
233 | } |
234 | |
235 | Widget barrier = Semantics( |
236 | onTapHint: semanticsOnTapHint, |
237 | onTap: semanticsDismissible && semanticsLabel != null ? handleDismiss : null, |
238 | onDismiss: semanticsDismissible && semanticsLabel != null ? handleDismiss : null, |
239 | label: semanticsDismissible ? semanticsLabel : null, |
240 | textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, |
241 | child: MouseRegion( |
242 | cursor: SystemMouseCursors.basic, |
243 | child: ConstrainedBox( |
244 | constraints: const BoxConstraints.expand(), |
245 | child: color == null ? null : ColoredBox( |
246 | color: color!, |
247 | ), |
248 | ), |
249 | ), |
250 | ); |
251 | |
252 | // Developers can set [dismissible: true] and [barrierSemanticsDismissible: true] |
253 | // to allow assistive technology users to dismiss a modal BottomSheet by |
254 | // tapping on the Scrim focus. |
255 | // On iOS, some modal barriers are not dismissible in accessibility mode. |
256 | final bool excluding = !semanticsDismissible || !modalBarrierSemanticsDismissible; |
257 | |
258 | if (!excluding && clipDetailsNotifier != null) { |
259 | barrier = _SemanticsClipper( |
260 | clipDetailsNotifier: clipDetailsNotifier!, |
261 | child: barrier, |
262 | ); |
263 | } |
264 | |
265 | return BlockSemantics( |
266 | child: ExcludeSemantics( |
267 | excluding: excluding, |
268 | child: _ModalBarrierGestureDetector( |
269 | onDismiss: handleDismiss, |
270 | child: barrier, |
271 | ), |
272 | ), |
273 | ); |
274 | } |
275 | } |
276 | |
277 | /// A widget that prevents the user from interacting with widgets behind itself, |
278 | /// and can be configured with an animated color value. |
279 | /// |
280 | /// The modal barrier is the scrim that is rendered behind each route, which |
281 | /// generally prevents the user from interacting with the route below the |
282 | /// current route, and normally partially obscures such routes. |
283 | /// |
284 | /// For example, when a dialog is on the screen, the page below the dialog is |
285 | /// usually darkened by the modal barrier. |
286 | /// |
287 | /// This widget is similar to [ModalBarrier] except that it takes an animated |
288 | /// [color] instead of a single color. |
289 | /// |
290 | /// See also: |
291 | /// |
292 | /// * [ModalRoute], which uses this widget. |
293 | class AnimatedModalBarrier extends AnimatedWidget { |
294 | /// Creates a widget that blocks user interaction. |
295 | const AnimatedModalBarrier({ |
296 | super.key, |
297 | required Animation<Color?> color, |
298 | this.dismissible = true, |
299 | this.semanticsLabel, |
300 | this.barrierSemanticsDismissible, |
301 | this.onDismiss, |
302 | this.clipDetailsNotifier, |
303 | this.semanticsOnTapHint, |
304 | }) : super(listenable: color); |
305 | |
306 | /// If non-null, fill the barrier with this color. |
307 | /// |
308 | /// See also: |
309 | /// |
310 | /// * [ModalRoute.barrierColor], which controls this property for the |
311 | /// [AnimatedModalBarrier] built by [ModalRoute] pages. |
312 | Animation<Color?> get color => listenable as Animation<Color?>; |
313 | |
314 | /// Whether touching the barrier will pop the current route off the [Navigator]. |
315 | /// |
316 | /// See also: |
317 | /// |
318 | /// * [ModalRoute.barrierDismissible], which controls this property for the |
319 | /// [AnimatedModalBarrier] built by [ModalRoute] pages. |
320 | final bool dismissible; |
321 | |
322 | /// Semantics label used for the barrier if it is [dismissible]. |
323 | /// |
324 | /// The semantics label is read out by accessibility tools (e.g. TalkBack |
325 | /// on Android and VoiceOver on iOS) when the barrier is focused. |
326 | /// See also: |
327 | /// |
328 | /// * [ModalRoute.barrierLabel], which controls this property for the |
329 | /// [ModalBarrier] built by [ModalRoute] pages. |
330 | final String? semanticsLabel; |
331 | |
332 | /// Whether the modal barrier semantics are included in the semantics tree. |
333 | /// |
334 | /// See also: |
335 | /// |
336 | /// * [ModalRoute.semanticsDismissible], which controls this property for |
337 | /// the [ModalBarrier] built by [ModalRoute] pages. |
338 | final bool? barrierSemanticsDismissible; |
339 | |
340 | /// {@macro flutter.widgets.ModalBarrier.onDismiss} |
341 | final VoidCallback? onDismiss; |
342 | |
343 | /// {@macro flutter.widgets.ModalBarrier.clipDetailsNotifier} |
344 | final ValueNotifier<EdgeInsets>? clipDetailsNotifier; |
345 | |
346 | /// This hint text instructs users what they are able to do when they tap on |
347 | /// the [ModalBarrier] |
348 | /// |
349 | /// E.g. If the hint text is 'close bottom sheet", it will be announced as |
350 | /// "Double tap to close bottom sheet". |
351 | /// |
352 | /// If this value is null, the default onTapHint will be applied, resulting |
353 | /// in the announcement of 'Double tap to activate'. |
354 | final String? semanticsOnTapHint; |
355 | |
356 | @override |
357 | Widget build(BuildContext context) { |
358 | return ModalBarrier( |
359 | color: color.value, |
360 | dismissible: dismissible, |
361 | semanticsLabel: semanticsLabel, |
362 | barrierSemanticsDismissible: barrierSemanticsDismissible, |
363 | onDismiss: onDismiss, |
364 | clipDetailsNotifier: clipDetailsNotifier, |
365 | semanticsOnTapHint: semanticsOnTapHint, |
366 | ); |
367 | } |
368 | } |
369 | |
370 | // Recognizes tap down by any pointer button. |
371 | // |
372 | // It is similar to [TapGestureRecognizer.onTapDown], but accepts any single |
373 | // button, which means the gesture also takes parts in gesture arenas. |
374 | class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer { |
375 | _AnyTapGestureRecognizer(); |
376 | |
377 | VoidCallback? onAnyTapUp; |
378 | |
379 | @protected |
380 | @override |
381 | bool isPointerAllowed(PointerDownEvent event) { |
382 | if (onAnyTapUp == null) { |
383 | return false; |
384 | } |
385 | return super.isPointerAllowed(event); |
386 | } |
387 | |
388 | @protected |
389 | @override |
390 | void handleTapDown({PointerDownEvent? down}) { |
391 | // Do nothing. |
392 | } |
393 | |
394 | @protected |
395 | @override |
396 | void handleTapUp({PointerDownEvent? down, PointerUpEvent? up}) { |
397 | if (onAnyTapUp != null) { |
398 | invokeCallback('onAnyTapUp' , onAnyTapUp!); |
399 | } |
400 | } |
401 | |
402 | @protected |
403 | @override |
404 | void handleTapCancel({PointerDownEvent? down, PointerCancelEvent? cancel, String? reason}) { |
405 | // Do nothing. |
406 | } |
407 | |
408 | @override |
409 | String get debugDescription => 'any tap' ; |
410 | } |
411 | |
412 | class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> { |
413 | const _AnyTapGestureRecognizerFactory({this.onAnyTapUp}); |
414 | |
415 | final VoidCallback? onAnyTapUp; |
416 | |
417 | @override |
418 | _AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer(); |
419 | |
420 | @override |
421 | void initializer(_AnyTapGestureRecognizer instance) { |
422 | instance.onAnyTapUp = onAnyTapUp; |
423 | } |
424 | } |
425 | |
426 | // A GestureDetector used by ModalBarrier. It only has one callback, |
427 | // [onAnyTapDown], which recognizes tap down unconditionally. |
428 | class _ModalBarrierGestureDetector extends StatelessWidget { |
429 | const _ModalBarrierGestureDetector({ |
430 | required this.child, |
431 | required this.onDismiss, |
432 | }); |
433 | |
434 | /// The widget below this widget in the tree. |
435 | /// See [RawGestureDetector.child]. |
436 | final Widget child; |
437 | |
438 | /// Immediately called when an event that should dismiss the modal barrier |
439 | /// has happened. |
440 | final VoidCallback onDismiss; |
441 | |
442 | @override |
443 | Widget build(BuildContext context) { |
444 | final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{ |
445 | _AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapUp: onDismiss), |
446 | }; |
447 | |
448 | return RawGestureDetector( |
449 | gestures: gestures, |
450 | behavior: HitTestBehavior.opaque, |
451 | child: child, |
452 | ); |
453 | } |
454 | } |
455 | |