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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/rendering.dart';
8import 'package:flutter/services.dart';
9
10import 'basic.dart';
11import 'debug.dart';
12import 'framework.dart';
13import 'gesture_detector.dart';
14import 'navigator.dart';
15import '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.
30class _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].
55class _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.
127class 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.
293class 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.
374class _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
412class _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.
428class _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