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