5
5
import 'dart:ui' show lerpDouble;
6
6
7
7
import 'package:flutter/foundation.dart' ;
8
+ import 'package:flutter/rendering.dart' ;
8
9
import 'package:flutter/widgets.dart' ;
9
10
10
11
import 'bottom_sheet_theme.dart' ;
@@ -319,16 +320,134 @@ class _BottomSheetState extends State<BottomSheet> {
319
320
320
321
// See scaffold.dart
321
322
323
+ typedef _SizeChangeCallback <Size > = void Function (Size );
322
324
323
- // MODAL BOTTOM SHEETS
324
- class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
325
- _ModalBottomSheetLayout (this .progress, this .isScrollControlled);
325
+ class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
326
326
327
- final double progress;
327
+ const _BottomSheetLayoutWithSizeListener ({
328
+ required this .animationValue,
329
+ required this .isScrollControlled,
330
+ required this .onChildSizeChanged,
331
+ super .child,
332
+ }) : assert (animationValue != null );
333
+
334
+ final double animationValue;
328
335
final bool isScrollControlled;
336
+ final _SizeChangeCallback <Size > onChildSizeChanged;
337
+
338
+ @override
339
+ _RenderBottomSheetLayoutWithSizeListener createRenderObject (BuildContext context) {
340
+ return _RenderBottomSheetLayoutWithSizeListener (
341
+ animationValue: animationValue,
342
+ isScrollControlled: isScrollControlled,
343
+ onChildSizeChanged: onChildSizeChanged,
344
+ );
345
+ }
346
+
347
+ @override
348
+ void updateRenderObject (BuildContext context, _RenderBottomSheetLayoutWithSizeListener renderObject) {
349
+ renderObject.onChildSizeChanged = onChildSizeChanged;
350
+ renderObject.animationValue = animationValue;
351
+ renderObject.isScrollControlled = isScrollControlled;
352
+ }
353
+ }
354
+
355
+ class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
356
+ _RenderBottomSheetLayoutWithSizeListener ({
357
+ RenderBox ? child,
358
+ required _SizeChangeCallback <Size > onChildSizeChanged,
359
+ required double animationValue,
360
+ required bool isScrollControlled,
361
+ }) : assert (animationValue != null ),
362
+ _animationValue = animationValue,
363
+ _isScrollControlled = isScrollControlled,
364
+ _onChildSizeChanged = onChildSizeChanged,
365
+ super (child);
366
+
367
+ Size _lastSize = Size .zero;
368
+
369
+ _SizeChangeCallback <Size > get onChildSizeChanged => _onChildSizeChanged;
370
+ _SizeChangeCallback <Size > _onChildSizeChanged;
371
+ set onChildSizeChanged (_SizeChangeCallback <Size > newCallback) {
372
+ assert (newCallback != null );
373
+ if (_onChildSizeChanged == newCallback) {
374
+ return ;
375
+ }
376
+
377
+ _onChildSizeChanged = newCallback;
378
+ markNeedsLayout ();
379
+ }
380
+
381
+ double get animationValue => _animationValue;
382
+ double _animationValue;
383
+ set animationValue (double newValue) {
384
+ assert (newValue != null );
385
+ if (_animationValue == newValue) {
386
+ return ;
387
+ }
388
+
389
+ _animationValue = newValue;
390
+ markNeedsLayout ();
391
+ }
392
+
393
+ bool get isScrollControlled => _isScrollControlled;
394
+ bool _isScrollControlled;
395
+ set isScrollControlled (bool newValue) {
396
+ assert (newValue != null );
397
+ if (_isScrollControlled == newValue) {
398
+ return ;
399
+ }
400
+
401
+ _isScrollControlled = newValue;
402
+ markNeedsLayout ();
403
+ }
404
+
405
+ Size _getSize (BoxConstraints constraints) {
406
+ return constraints.constrain (constraints.biggest);
407
+ }
408
+
409
+ @override
410
+ double computeMinIntrinsicWidth (double height) {
411
+ final double width = _getSize (BoxConstraints .tightForFinite (height: height)).width;
412
+ if (width.isFinite) {
413
+ return width;
414
+ }
415
+ return 0.0 ;
416
+ }
329
417
330
418
@override
331
- BoxConstraints getConstraintsForChild (BoxConstraints constraints) {
419
+ double computeMaxIntrinsicWidth (double height) {
420
+ final double width = _getSize (BoxConstraints .tightForFinite (height: height)).width;
421
+ if (width.isFinite) {
422
+ return width;
423
+ }
424
+ return 0.0 ;
425
+ }
426
+
427
+ @override
428
+ double computeMinIntrinsicHeight (double width) {
429
+ final double height = _getSize (BoxConstraints .tightForFinite (width: width)).height;
430
+ if (height.isFinite) {
431
+ return height;
432
+ }
433
+ return 0.0 ;
434
+ }
435
+
436
+ @override
437
+ double computeMaxIntrinsicHeight (double width) {
438
+ final double height = _getSize (BoxConstraints .tightForFinite (width: width)).height;
439
+ if (height.isFinite) {
440
+ return height;
441
+ }
442
+ return 0.0 ;
443
+ }
444
+
445
+ @override
446
+ Size computeDryLayout (BoxConstraints constraints) {
447
+ return _getSize (constraints);
448
+ }
449
+
450
+ BoxConstraints _getConstraintsForChild (BoxConstraints constraints) {
332
451
return BoxConstraints (
333
452
minWidth: constraints.maxWidth,
334
453
maxWidth: constraints.maxWidth,
@@ -338,14 +457,26 @@ class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
338
457
);
339
458
}
340
459
341
- @override
342
- Offset getPositionForChild (Size size, Size childSize) {
343
- return Offset (0.0 , size.height - childSize.height * progress);
460
+ Offset _getPositionForChild (Size size, Size childSize) {
461
+ return Offset (0.0 , size.height - childSize.height * animationValue);
344
462
}
345
463
346
464
@override
347
- bool shouldRelayout (_ModalBottomSheetLayout oldDelegate) {
348
- return progress != oldDelegate.progress;
465
+ void performLayout () {
466
+ size = _getSize (constraints);
467
+ if (child != null ) {
468
+ final BoxConstraints childConstraints = _getConstraintsForChild (constraints);
469
+ assert (childConstraints.debugAssertIsValid (isAppliedConstraint: true ));
470
+ child! .layout (childConstraints, parentUsesSize: ! childConstraints.isTight);
471
+ final BoxParentData childParentData = child! .parentData! as BoxParentData ;
472
+ childParentData.offset = _getPositionForChild (size, childConstraints.isTight ? childConstraints.smallest : child! .size);
473
+ final Size childSize = childConstraints.isTight ? childConstraints.smallest : child! .size;
474
+
475
+ if (_lastSize != childSize) {
476
+ _lastSize = childSize;
477
+ _onChildSizeChanged.call (_lastSize);
478
+ }
479
+ }
349
480
}
350
481
}
351
482
@@ -392,6 +523,10 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
392
523
}
393
524
}
394
525
526
+ EdgeInsets _getNewClipDetails (Size topLayerSize) {
527
+ return EdgeInsets .fromLTRB (0 , 0 , 0 , topLayerSize.height);
528
+ }
529
+
395
530
void handleDragStart (DragStartDetails details) {
396
531
// Allow the bottom sheet to track the user's finger accurately.
397
532
animationCurve = Curves .linear;
@@ -443,8 +578,14 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
443
578
label: routeLabel,
444
579
explicitChildNodes: true ,
445
580
child: ClipRect (
446
- child: CustomSingleChildLayout (
447
- delegate: _ModalBottomSheetLayout (animationValue, widget.isScrollControlled),
581
+ child: _BottomSheetLayoutWithSizeListener (
582
+ onChildSizeChanged: (Size size) {
583
+ widget.route._didChangeBarrierSemanticsClip (
584
+ _getNewClipDetails (size),
585
+ );
586
+ },
587
+ animationValue: animationValue,
588
+ isScrollControlled: widget.isScrollControlled,
448
589
child: child,
449
590
),
450
591
),
@@ -516,6 +657,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
516
657
required this .builder,
517
658
this .capturedThemes,
518
659
this .barrierLabel,
660
+ this .barrierOnTapHint,
519
661
this .backgroundColor,
520
662
this .elevation,
521
663
this .shape,
@@ -646,6 +788,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
646
788
/// Default is false.
647
789
final bool useSafeArea;
648
790
791
+ /// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
792
+ /// The semantic hint text that informs users what will happen if they
793
+ /// tap on the widget. Announced in the format of 'Double tap to ...'.
794
+ ///
795
+ /// If the field is null, the default hint will be used, which results in
796
+ /// announcement of 'Double tap to activate'.
797
+ /// {@endtemplate}
798
+ ///
799
+ /// See also:
800
+ ///
801
+ /// * [barrierDismissible] , which controls the behavior of the barrier when
802
+ /// tapped.
803
+ /// * [ModalBarrier] , which uses this field as onTapHint when it has an onTap action.
804
+ final String ? barrierOnTapHint;
805
+
806
+ final ValueNotifier <EdgeInsets > _clipDetailsNotifier = ValueNotifier <EdgeInsets >(EdgeInsets .zero);
807
+
808
+ /// Updates the details regarding how the [SemanticsNode.rect] (focus) of
809
+ /// the barrier for this [ModalBottomSheetRoute] should be clipped.
810
+ ///
811
+ /// returns true if the clipDetails did change and false otherwise.
812
+ bool _didChangeBarrierSemanticsClip (EdgeInsets newClipDetails) {
813
+ if (_clipDetailsNotifier.value == newClipDetails) {
814
+ return false ;
815
+ }
816
+ _clipDetailsNotifier.value = newClipDetails;
817
+ return true ;
818
+ }
819
+
649
820
@override
650
821
Duration get transitionDuration => _bottomSheetEnterDuration;
651
822
@@ -710,6 +881,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
710
881
711
882
return capturedThemes? .wrap (bottomSheet) ?? bottomSheet;
712
883
}
884
+
885
+ @override
886
+ Widget buildModalBarrier () {
887
+ if (barrierColor != null && barrierColor.alpha != 0 && ! offstage) { // changedInternalState is called if barrierColor or offstage updates
888
+ assert (barrierColor != barrierColor.withOpacity (0.0 ));
889
+ final Animation <Color ?> color = animation! .drive (
890
+ ColorTween (
891
+ begin: barrierColor.withOpacity (0.0 ),
892
+ end: barrierColor, // changedInternalState is called if barrierColor updates
893
+ ).chain (CurveTween (curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
894
+ );
895
+ return AnimatedModalBarrier (
896
+ color: color,
897
+ dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
898
+ semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
899
+ barrierSemanticsDismissible: semanticsDismissible,
900
+ clipDetailsNotifier: _clipDetailsNotifier,
901
+ semanticsOnTapHint: barrierOnTapHint,
902
+ );
903
+ } else {
904
+ return ModalBarrier (
905
+ dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
906
+ semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
907
+ barrierSemanticsDismissible: semanticsDismissible,
908
+ clipDetailsNotifier: _clipDetailsNotifier,
909
+ semanticsOnTapHint: barrierOnTapHint,
910
+ );
911
+ }
912
+ }
713
913
}
714
914
715
915
// TODO(guidezpl): Look into making this public. A copy of this class is in
@@ -844,11 +1044,13 @@ Future<T?> showModalBottomSheet<T>({
844
1044
assert (debugCheckHasMaterialLocalizations (context));
845
1045
846
1046
final NavigatorState navigator = Navigator .of (context, rootNavigator: useRootNavigator);
1047
+ final MaterialLocalizations localizations = MaterialLocalizations .of (context);
847
1048
return navigator.push (ModalBottomSheetRoute <T >(
848
1049
builder: builder,
849
1050
capturedThemes: InheritedTheme .capture (from: context, to: navigator.context),
850
1051
isScrollControlled: isScrollControlled,
851
- barrierLabel: MaterialLocalizations .of (context).modalBarrierDismissLabel,
1052
+ barrierLabel: localizations.scrimLabel,
1053
+ barrierOnTapHint: localizations.scrimOnTapHint (localizations.bottomSheetLabel),
852
1054
backgroundColor: backgroundColor,
853
1055
elevation: elevation,
854
1056
shape: shape,
0 commit comments