55import 'dart:ui' show lerpDouble;
66
77import 'package:flutter/foundation.dart' ;
8+ import 'package:flutter/rendering.dart' ;
89import 'package:flutter/widgets.dart' ;
910
1011import 'bottom_sheet_theme.dart' ;
@@ -319,16 +320,134 @@ class _BottomSheetState extends State<BottomSheet> {
319320
320321// See scaffold.dart
321322
323+ typedef _SizeChangeCallback <Size > = void Function (Size );
322324
323- // MODAL BOTTOM SHEETS
324- class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
325- _ModalBottomSheetLayout (this .progress, this .isScrollControlled);
325+ class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
326326
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;
328335 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+ }
329417
330418 @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) {
332451 return BoxConstraints (
333452 minWidth: constraints.maxWidth,
334453 maxWidth: constraints.maxWidth,
@@ -338,14 +457,26 @@ class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
338457 );
339458 }
340459
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);
344462 }
345463
346464 @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+ }
349480 }
350481}
351482
@@ -392,6 +523,10 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
392523 }
393524 }
394525
526+ EdgeInsets _getNewClipDetails (Size topLayerSize) {
527+ return EdgeInsets .fromLTRB (0 , 0 , 0 , topLayerSize.height);
528+ }
529+
395530 void handleDragStart (DragStartDetails details) {
396531 // Allow the bottom sheet to track the user's finger accurately.
397532 animationCurve = Curves .linear;
@@ -443,8 +578,14 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
443578 label: routeLabel,
444579 explicitChildNodes: true ,
445580 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,
448589 child: child,
449590 ),
450591 ),
@@ -516,6 +657,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
516657 required this .builder,
517658 this .capturedThemes,
518659 this .barrierLabel,
660+ this .barrierOnTapHint,
519661 this .backgroundColor,
520662 this .elevation,
521663 this .shape,
@@ -646,6 +788,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
646788 /// Default is false.
647789 final bool useSafeArea;
648790
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+
649820 @override
650821 Duration get transitionDuration => _bottomSheetEnterDuration;
651822
@@ -710,6 +881,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
710881
711882 return capturedThemes? .wrap (bottomSheet) ?? bottomSheet;
712883 }
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+ }
713913}
714914
715915// TODO(guidezpl): Look into making this public. A copy of this class is in
@@ -844,11 +1044,13 @@ Future<T?> showModalBottomSheet<T>({
8441044 assert (debugCheckHasMaterialLocalizations (context));
8451045
8461046 final NavigatorState navigator = Navigator .of (context, rootNavigator: useRootNavigator);
1047+ final MaterialLocalizations localizations = MaterialLocalizations .of (context);
8471048 return navigator.push (ModalBottomSheetRoute <T >(
8481049 builder: builder,
8491050 capturedThemes: InheritedTheme .capture (from: context, to: navigator.context),
8501051 isScrollControlled: isScrollControlled,
851- barrierLabel: MaterialLocalizations .of (context).modalBarrierDismissLabel,
1052+ barrierLabel: localizations.scrimLabel,
1053+ barrierOnTapHint: localizations.scrimOnTapHint (localizations.bottomSheetLabel),
8521054 backgroundColor: backgroundColor,
8531055 elevation: elevation,
8541056 shape: shape,
0 commit comments