Unverified Commit 9fc1fd15 authored by hangyu's avatar hangyu Committed by GitHub

Update Material 3 bottom sheet (#122445)

* M3 bottomsheet

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update bottom_sheet.dart

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update packages/flutter/lib/src/material/bottom_sheet.dart
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>

* Update bottom_sheet.dart

Update bottom_sheet_test.dart

Update bottom_sheet.dart

* showDragHandle defaults to false

* fix test

---------
Co-authored-by: 's avatarPierre-Louis <6655696+guidezpl@users.noreply.github.com>
parent d58be8c7
...@@ -17,6 +17,7 @@ class _${blockName}DefaultsM3 extends BottomSheetThemeData { ...@@ -17,6 +17,7 @@ class _${blockName}DefaultsM3 extends BottomSheetThemeData {
elevation: ${elevation("md.comp.sheet.bottom.docked.standard.container")}, elevation: ${elevation("md.comp.sheet.bottom.docked.standard.container")},
modalElevation: ${elevation("md.comp.sheet.bottom.docked.modal.container")}, modalElevation: ${elevation("md.comp.sheet.bottom.docked.modal.container")},
shape: ${shape("md.comp.sheet.bottom.docked.container")}, shape: ${shape("md.comp.sheet.bottom.docked.container")},
constraints: const BoxConstraints(maxWidth: 640),
); );
final BuildContext context; final BuildContext context;
...@@ -30,6 +31,12 @@ class _${blockName}DefaultsM3 extends BottomSheetThemeData { ...@@ -30,6 +31,12 @@ class _${blockName}DefaultsM3 extends BottomSheetThemeData {
@override @override
Color? get shadowColor => Colors.transparent; Color? get shadowColor => Colors.transparent;
@override
Color? get dragHandleColor => ${componentColor("md.comp.sheet.bottom.docked.drag-handle")};
@override
Size? get dragHandleSize => ${size("md.comp.sheet.bottom.docked.drag-handle")};
} }
'''; ''';
} }
...@@ -5,16 +5,19 @@ ...@@ -5,16 +5,19 @@
import 'dart:ui' show lerpDouble; import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'bottom_sheet_theme.dart'; import 'bottom_sheet_theme.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart';
import 'curves.dart'; import 'curves.dart';
import 'debug.dart'; import 'debug.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart';
import 'scaffold.dart'; import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -65,7 +68,8 @@ typedef BottomSheetDragEndHandler = void Function( ...@@ -65,7 +68,8 @@ typedef BottomSheetDragEndHandler = void Function(
/// sheet. /// sheet.
/// * [BottomSheetThemeData], which can be used to customize the default /// * [BottomSheetThemeData], which can be used to customize the default
/// bottom sheet property values. /// bottom sheet property values.
/// * <https://material.io/design/components/sheets-bottom.html> /// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
class BottomSheet extends StatefulWidget { class BottomSheet extends StatefulWidget {
/// Creates a bottom sheet. /// Creates a bottom sheet.
/// ///
...@@ -76,6 +80,9 @@ class BottomSheet extends StatefulWidget { ...@@ -76,6 +80,9 @@ class BottomSheet extends StatefulWidget {
super.key, super.key,
this.animationController, this.animationController,
this.enableDrag = true, this.enableDrag = true,
this.showDragHandle,
this.dragHandleColor,
this.dragHandleSize,
this.onDragStart, this.onDragStart,
this.onDragEnd, this.onDragEnd,
this.backgroundColor, this.backgroundColor,
...@@ -111,9 +118,34 @@ class BottomSheet extends StatefulWidget { ...@@ -111,9 +118,34 @@ class BottomSheet extends StatefulWidget {
/// If true, the bottom sheet can be dragged up and down and dismissed by /// If true, the bottom sheet can be dragged up and down and dismissed by
/// swiping downwards. /// swiping downwards.
/// ///
/// If [showDragHandle] is true, this only applies to the content below the drag handle,
/// because the drag handle is always draggable.
///
/// Default is true. /// Default is true.
final bool enableDrag; final bool enableDrag;
/// Specifies whether a drag handle is shown.
///
/// The drag handle appears at the top of the bottom sheet. The default color is
/// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
/// using [dragHandleColor]. The default size is `Size(32,4)` and can be customized
/// with [dragHandleSize].
///
/// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
/// that is also null, defaults to false.
final bool? showDragHandle;
/// The bottom sheet drag handle's color.
///
/// Defaults to [BottomSheetThemeData.dragHandleColor].
/// If that is also null, defaults to [ColorScheme.onSurfaceVariant]
/// with an opacity of 0.4.
final Color? dragHandleColor;
/// Defaults to [BottomSheetThemeData.dragHandleSize].
/// If that is also null, defaults to Size(32, 4).
final Size? dragHandleSize;
/// Called when the user begins dragging the bottom sheet vertically, if /// Called when the user begins dragging the bottom sheet vertically, if
/// [enableDrag] is true. /// [enableDrag] is true.
/// ///
...@@ -222,14 +254,19 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -222,14 +254,19 @@ class _BottomSheetState extends State<BottomSheet> {
bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse; bool get _dismissUnderway => widget.animationController!.status == AnimationStatus.reverse;
Set<MaterialState> dragHandleMaterialState = <MaterialState>{};
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
setState(() {
dragHandleMaterialState.add(MaterialState.dragged);
});
widget.onDragStart?.call(details); widget.onDragStart?.call(details);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
assert( assert(
widget.enableDrag && widget.animationController != null, (widget.enableDrag || (widget.showDragHandle?? false)) && widget.animationController != null,
"'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. " "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
"Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
); );
if (_dismissUnderway) { if (_dismissUnderway) {
...@@ -240,13 +277,16 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -240,13 +277,16 @@ class _BottomSheetState extends State<BottomSheet> {
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
assert( assert(
widget.enableDrag && widget.animationController != null, (widget.enableDrag || (widget.showDragHandle?? false)) && widget.animationController != null,
"'BottomSheet.animationController' can not be null when 'BottomSheet.enableDrag' is true. " "'BottomSheet.animationController' cannot be null when 'BottomSheet.enableDrag' or 'BottomSheet.showDragHandle' is true. "
"Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.", "Use 'BottomSheet.createAnimationController' to create one, or provide another AnimationController.",
); );
if (_dismissUnderway) { if (_dismissUnderway) {
return; return;
} }
setState(() {
dragHandleMaterialState.remove(MaterialState.dragged);
});
bool isClosing = false; bool isClosing = false;
if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) { if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight; final double flingVelocity = -details.velocity.pixelsPerSecond.dy / _childHeight;
...@@ -282,17 +322,55 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -282,17 +322,55 @@ class _BottomSheetState extends State<BottomSheet> {
return false; return false;
} }
void _handleDragHandleHover(bool hovering) {
if (hovering != dragHandleMaterialState.contains(MaterialState.hovered)) {
setState(() {
if(hovering){
dragHandleMaterialState.add(MaterialState.hovered);
}
else{
dragHandleMaterialState.remove(MaterialState.hovered);
}
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme; final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
final BottomSheetThemeData defaults = Theme.of(context).useMaterial3 ? _BottomSheetDefaultsM3(context) : const BottomSheetThemeData(); final bool useMaterial3 = Theme.of(context).useMaterial3;
final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints; final BottomSheetThemeData defaults = useMaterial3 ? _BottomSheetDefaultsM3(context) : const BottomSheetThemeData();
final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints ?? defaults.constraints;
final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor; final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor ?? defaults.backgroundColor;
final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor; final Color? surfaceTintColor = bottomSheetTheme.surfaceTintColor ?? defaults.surfaceTintColor;
final Color? shadowColor = widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor; final Color? shadowColor = widget.shadowColor ?? bottomSheetTheme.shadowColor ?? defaults.shadowColor;
final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0; final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? defaults.elevation ?? 0;
final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape; final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape ?? defaults.shape;
final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none; final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
final bool showDragHandle = widget.showDragHandle ?? (widget.enableDrag && (bottomSheetTheme.showDragHandle ?? false));
Widget? dragHandle;
if (showDragHandle){
dragHandle = _DragHandle(
onSemanticsTap: widget.onClosing,
handleHover: _handleDragHandleHover,
materialState: dragHandleMaterialState,
dragHandleColor: widget.dragHandleColor,
dragHandleSize: widget.dragHandleSize,
);
// Only add [GestureDetector] to the drag handle when the rest of the
// bottom sheet is not draggable. If the whole bottom sheet is draggable,
// no need to add it.
if(!widget.enableDrag) {
dragHandle = GestureDetector(
onVerticalDragStart: _handleDragStart,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
excludeFromSemantics: true,
child: dragHandle,
);
}
}
Widget bottomSheet = Material( Widget bottomSheet = Material(
key: _childKey, key: _childKey,
...@@ -304,7 +382,18 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -304,7 +382,18 @@ class _BottomSheetState extends State<BottomSheet> {
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
child: NotificationListener<DraggableScrollableNotification>( child: NotificationListener<DraggableScrollableNotification>(
onNotification: extentChanged, onNotification: extentChanged,
child: widget.builder(context), child: !showDragHandle
? widget.builder(context)
: Stack(
alignment: Alignment.topCenter,
children: <Widget>[
dragHandle!,
Padding(
padding: const EdgeInsets.only(top: kMinInteractiveDimension),
child: widget.builder(context),
),
],
),
), ),
); );
...@@ -335,6 +424,55 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -335,6 +424,55 @@ class _BottomSheetState extends State<BottomSheet> {
typedef _SizeChangeCallback<Size> = void Function(Size); typedef _SizeChangeCallback<Size> = void Function(Size);
class _DragHandle extends StatelessWidget {
const _DragHandle({
required this.onSemanticsTap,
required this.handleHover,
required this.materialState,
this.dragHandleColor,
this.dragHandleSize,
});
final VoidCallback? onSemanticsTap;
final Function(bool) handleHover;
final Set<MaterialState> materialState;
final Color? dragHandleColor;
final Size? dragHandleSize;
@override
Widget build(BuildContext context) {
final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
final BottomSheetThemeData m3Defaults = _BottomSheetDefaultsM3(context);
final Size handleSize = dragHandleSize ?? bottomSheetTheme.dragHandleSize ?? m3Defaults.dragHandleSize!;
return MouseRegion(
onEnter: (PointerEnterEvent event) => handleHover(true),
onExit: (PointerExitEvent event) => handleHover(false),
child: Semantics(
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
container: true,
onTap: onSemanticsTap,
child: SizedBox(
height: kMinInteractiveDimension,
width: kMinInteractiveDimension,
child: Center(
child: Container(
height: handleSize.height,
width: handleSize.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(handleSize.height/2),
color: MaterialStateProperty.resolveAs<Color?>(dragHandleColor, materialState)
?? MaterialStateProperty.resolveAs<Color?>(bottomSheetTheme.dragHandleColor, materialState)
?? m3Defaults.dragHandleColor,
),
),
),
),
),
);
}
}
class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget { class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
const _BottomSheetLayoutWithSizeListener({ const _BottomSheetLayoutWithSizeListener({
...@@ -500,6 +638,7 @@ class _ModalBottomSheet<T> extends StatefulWidget { ...@@ -500,6 +638,7 @@ class _ModalBottomSheet<T> extends StatefulWidget {
this.constraints, this.constraints,
this.isScrollControlled = false, this.isScrollControlled = false,
this.enableDrag = true, this.enableDrag = true,
this.showDragHandle = false,
}); });
final ModalBottomSheetRoute<T> route; final ModalBottomSheetRoute<T> route;
...@@ -510,6 +649,7 @@ class _ModalBottomSheet<T> extends StatefulWidget { ...@@ -510,6 +649,7 @@ class _ModalBottomSheet<T> extends StatefulWidget {
final Clip? clipBehavior; final Clip? clipBehavior;
final BoxConstraints? constraints; final BoxConstraints? constraints;
final bool enableDrag; final bool enableDrag;
final bool showDragHandle;
@override @override
_ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>(); _ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
...@@ -571,6 +711,7 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> { ...@@ -571,6 +711,7 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
constraints: widget.constraints, constraints: widget.constraints,
enableDrag: widget.enableDrag, enableDrag: widget.enableDrag,
showDragHandle: widget.showDragHandle,
onDragStart: handleDragStart, onDragStart: handleDragStart,
onDragEnd: handleDragEnd, onDragEnd: handleDragEnd,
), ),
...@@ -658,7 +799,8 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> { ...@@ -658,7 +799,8 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
/// and then becomes scrollable once it reaches its maximum size. /// and then becomes scrollable once it reaches its maximum size.
/// * [DisplayFeatureSubScreen], which documents the specifics of how /// * [DisplayFeatureSubScreen], which documents the specifics of how
/// [DisplayFeature]s can split the screen into sub-screens. /// [DisplayFeature]s can split the screen into sub-screens.
/// * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet> /// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
class ModalBottomSheetRoute<T> extends PopupRoute<T> { class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// A modal bottom sheet route. /// A modal bottom sheet route.
ModalBottomSheetRoute({ ModalBottomSheetRoute({
...@@ -674,6 +816,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> { ...@@ -674,6 +816,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
this.modalBarrierColor, this.modalBarrierColor,
this.isDismissible = true, this.isDismissible = true,
this.enableDrag = true, this.enableDrag = true,
this.showDragHandle,
required this.isScrollControlled, required this.isScrollControlled,
super.settings, super.settings,
this.transitionAnimationController, this.transitionAnimationController,
...@@ -773,9 +916,22 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> { ...@@ -773,9 +916,22 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// If true, the bottom sheet can be dragged up and down and dismissed by /// If true, the bottom sheet can be dragged up and down and dismissed by
/// swiping downwards. /// swiping downwards.
/// ///
/// This applies to the content below the drag handle, if showDragHandle is true.
///
/// Defaults is true. /// Defaults is true.
final bool enableDrag; final bool enableDrag;
/// Specifies whether a drag handle is shown.
///
/// The drag handle appears at the top of the bottom sheet. The default color is
/// [ColorScheme.onSurfaceVariant] with an opacity of 0.4 and can be customized
/// using dragHandleColor. The default size is `Size(32,4)` and can be customized
/// with dragHandleSize.
///
/// If null, then the value of [BottomSheetThemeData.showDragHandle] is used. If
/// that is also null, defaults to false.
final bool? showDragHandle;
/// The animation controller that controls the bottom sheet's entrance and /// The animation controller that controls the bottom sheet's entrance and
/// exit animations. /// exit animations.
/// ///
...@@ -875,6 +1031,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> { ...@@ -875,6 +1031,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
constraints: constraints, constraints: constraints,
isScrollControlled: isScrollControlled, isScrollControlled: isScrollControlled,
enableDrag: enableDrag, enableDrag: enableDrag,
showDragHandle: showDragHandle ?? (enableDrag && (sheetTheme.showDragHandle ?? false)),
); );
}, },
), ),
...@@ -1023,7 +1180,8 @@ class _BottomSheetSuspendedCurve extends ParametricCurve<double> { ...@@ -1023,7 +1180,8 @@ class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
/// and then becomes scrollable once it reaches its maximum size. /// and then becomes scrollable once it reaches its maximum size.
/// * [DisplayFeatureSubScreen], which documents the specifics of how /// * [DisplayFeatureSubScreen], which documents the specifics of how
/// [DisplayFeature]s can split the screen into sub-screens. /// [DisplayFeature]s can split the screen into sub-screens.
/// * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet> /// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
Future<T?> showModalBottomSheet<T>({ Future<T?> showModalBottomSheet<T>({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
...@@ -1037,6 +1195,7 @@ Future<T?> showModalBottomSheet<T>({ ...@@ -1037,6 +1195,7 @@ Future<T?> showModalBottomSheet<T>({
bool useRootNavigator = false, bool useRootNavigator = false,
bool isDismissible = true, bool isDismissible = true,
bool enableDrag = true, bool enableDrag = true,
bool? showDragHandle,
bool useSafeArea = false, bool useSafeArea = false,
RouteSettings? routeSettings, RouteSettings? routeSettings,
AnimationController? transitionAnimationController, AnimationController? transitionAnimationController,
...@@ -1061,6 +1220,7 @@ Future<T?> showModalBottomSheet<T>({ ...@@ -1061,6 +1220,7 @@ Future<T?> showModalBottomSheet<T>({
isDismissible: isDismissible, isDismissible: isDismissible,
modalBarrierColor: barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor, modalBarrierColor: barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
enableDrag: enableDrag, enableDrag: enableDrag,
showDragHandle: showDragHandle,
settings: routeSettings, settings: routeSettings,
transitionAnimationController: transitionAnimationController, transitionAnimationController: transitionAnimationController,
anchorPoint: anchorPoint, anchorPoint: anchorPoint,
...@@ -1111,7 +1271,8 @@ Future<T?> showModalBottomSheet<T>({ ...@@ -1111,7 +1271,8 @@ Future<T?> showModalBottomSheet<T>({
/// * [showModalBottomSheet], which can be used to display a modal bottom /// * [showModalBottomSheet], which can be used to display a modal bottom
/// sheet. /// sheet.
/// * [Scaffold.of], for information about how to obtain the [BuildContext]. /// * [Scaffold.of], for information about how to obtain the [BuildContext].
/// * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet> /// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
PersistentBottomSheetController<T> showBottomSheet<T>({ PersistentBottomSheetController<T> showBottomSheet<T>({
required BuildContext context, required BuildContext context,
required WidgetBuilder builder, required WidgetBuilder builder,
...@@ -1154,19 +1315,26 @@ class _BottomSheetDefaultsM3 extends BottomSheetThemeData { ...@@ -1154,19 +1315,26 @@ class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
elevation: 1.0, elevation: 1.0,
modalElevation: 1.0, modalElevation: 1.0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28.0))),
constraints: const BoxConstraints(maxWidth: 640),
); );
final BuildContext context; final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme; late final ColorScheme _colors = Theme.of(context).colorScheme;
@override @override
Color? get backgroundColor => _colors.surface; Color get backgroundColor => _colors.surface;
@override
Color get surfaceTintColor => _colors.surfaceTint;
@override
Color get shadowColor => Colors.transparent;
@override @override
Color? get surfaceTintColor => _colors.surfaceTint; Color get dragHandleColor => Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4);
@override @override
Color? get shadowColor => Colors.transparent; Size get dragHandleSize => const Size(32, 4);
} }
// END GENERATED TOKEN PROPERTIES - BottomSheet // END GENERATED TOKEN PROPERTIES - BottomSheet
...@@ -36,6 +36,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -36,6 +36,9 @@ class BottomSheetThemeData with Diagnosticable {
this.shadowColor, this.shadowColor,
this.modalElevation, this.modalElevation,
this.shape, this.shape,
this.showDragHandle,
this.dragHandleColor,
this.dragHandleSize,
this.clipBehavior, this.clipBehavior,
this.constraints, this.constraints,
}); });
...@@ -80,6 +83,15 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -80,6 +83,15 @@ class BottomSheetThemeData with Diagnosticable {
/// [BottomSheet] is rectangular. /// [BottomSheet] is rectangular.
final ShapeBorder? shape; final ShapeBorder? shape;
/// Overrides the default value for [BottomSheet.showDragHandle].
final bool? showDragHandle;
/// Overrides the default value for [BottomSheet.dragHandleColor].
final Color? dragHandleColor;
/// Overrides the default value for [BottomSheet.dragHandleSize].
final Size? dragHandleSize;
/// Overrides the default value for [BottomSheet.clipBehavior]. /// Overrides the default value for [BottomSheet.clipBehavior].
/// ///
/// If null, [BottomSheet] uses [Clip.none]. /// If null, [BottomSheet] uses [Clip.none].
...@@ -101,6 +113,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -101,6 +113,9 @@ class BottomSheetThemeData with Diagnosticable {
Color? shadowColor, Color? shadowColor,
double? modalElevation, double? modalElevation,
ShapeBorder? shape, ShapeBorder? shape,
bool? showDragHandle,
Color? dragHandleColor,
Size? dragHandleSize,
Clip? clipBehavior, Clip? clipBehavior,
BoxConstraints? constraints, BoxConstraints? constraints,
}) { }) {
...@@ -113,6 +128,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -113,6 +128,9 @@ class BottomSheetThemeData with Diagnosticable {
shadowColor: shadowColor ?? this.shadowColor, shadowColor: shadowColor ?? this.shadowColor,
modalElevation: modalElevation ?? this.modalElevation, modalElevation: modalElevation ?? this.modalElevation,
shape: shape ?? this.shape, shape: shape ?? this.shape,
showDragHandle: showDragHandle ?? this.showDragHandle,
dragHandleColor: dragHandleColor ?? this.dragHandleColor,
dragHandleSize: dragHandleSize ?? this.dragHandleSize,
clipBehavior: clipBehavior ?? this.clipBehavior, clipBehavior: clipBehavior ?? this.clipBehavior,
constraints: constraints ?? this.constraints, constraints: constraints ?? this.constraints,
); );
...@@ -136,6 +154,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -136,6 +154,9 @@ class BottomSheetThemeData with Diagnosticable {
shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t),
modalElevation: lerpDouble(a?.modalElevation, b?.modalElevation, t), modalElevation: lerpDouble(a?.modalElevation, b?.modalElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
showDragHandle: t < 0.5 ? a?.showDragHandle : b?.showDragHandle,
dragHandleColor: Color.lerp(a?.dragHandleColor, b?.dragHandleColor, t),
dragHandleSize: Size.lerp(a?.dragHandleSize, b?.dragHandleSize, t),
clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior, clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t),
); );
...@@ -151,6 +172,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -151,6 +172,9 @@ class BottomSheetThemeData with Diagnosticable {
shadowColor, shadowColor,
modalElevation, modalElevation,
shape, shape,
showDragHandle,
dragHandleColor,
dragHandleSize,
clipBehavior, clipBehavior,
constraints, constraints,
); );
...@@ -172,6 +196,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -172,6 +196,9 @@ class BottomSheetThemeData with Diagnosticable {
&& other.modalBarrierColor == modalBarrierColor && other.modalBarrierColor == modalBarrierColor
&& other.modalElevation == modalElevation && other.modalElevation == modalElevation
&& other.shape == shape && other.shape == shape
&& other.showDragHandle == showDragHandle
&& other.dragHandleColor == dragHandleColor
&& other.dragHandleSize == dragHandleSize
&& other.clipBehavior == clipBehavior && other.clipBehavior == clipBehavior
&& other.constraints == constraints; && other.constraints == constraints;
} }
...@@ -187,6 +214,9 @@ class BottomSheetThemeData with Diagnosticable { ...@@ -187,6 +214,9 @@ class BottomSheetThemeData with Diagnosticable {
properties.add(ColorProperty('modalBarrierColor', modalBarrierColor, defaultValue: null)); properties.add(ColorProperty('modalBarrierColor', modalBarrierColor, defaultValue: null));
properties.add(DoubleProperty('modalElevation', modalElevation, defaultValue: null)); properties.add(DoubleProperty('modalElevation', modalElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('showDragHandle', showDragHandle, defaultValue: null));
properties.add(ColorProperty('dragHandleColor', dragHandleColor, defaultValue: null));
properties.add(DiagnosticsProperty<Size>('dragHandleSize', dragHandleSize, defaultValue: null));
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: null)); properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null)); properties.add(DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null));
} }
......
...@@ -2425,7 +2425,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto ...@@ -2425,7 +2425,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
/// * [showModalBottomSheet], which can be used to display a modal bottom /// * [showModalBottomSheet], which can be used to display a modal bottom
/// sheet. /// sheet.
/// * [Scaffold.of], for information about how to obtain the [ScaffoldState]. /// * [Scaffold.of], for information about how to obtain the [ScaffoldState].
/// * <https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet> /// * The Material 2 spec at <https://m2.material.io/components/sheets-bottom>.
/// * The Material 3 spec at <https://m3.material.io/components/bottom-sheets/overview>.
PersistentBottomSheetController<T> showBottomSheet<T>( PersistentBottomSheetController<T> showBottomSheet<T>(
WidgetBuilder builder, { WidgetBuilder builder, {
Color? backgroundColor, Color? backgroundColor,
......
...@@ -1008,6 +1008,138 @@ void main() { ...@@ -1008,6 +1008,138 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('modal BottomSheet with drag handle has semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(MaterialApp(
theme: ThemeData.light(useMaterial3: true),
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
showModalBottomSheet<void>(
context: scaffoldKey.currentContext!,
showDragHandle: true,
builder: (BuildContext context) {
return const Text('BottomSheet');
},
);
await tester.pump(); // bottom sheet show animation starts
await tester.pump(const Duration(seconds: 1)); // animation done
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Dialog',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
children: <TestSemantics>[
TestSemantics(
label: 'BottomSheet',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Dismiss',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
label: 'Scrim',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Drag handle color can take MaterialStateProperty', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
const Color defaultColor=Colors.blue;
const Color hoveringColor=Colors.green;
await tester.pumpWidget(MaterialApp(
theme: ThemeData.light(useMaterial3: true).copyWith(
bottomSheetTheme: BottomSheetThemeData(
dragHandleColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveringColor;
}
return defaultColor;
}),
),
),
home: Scaffold(
key: scaffoldKey,
body: const Center(child: Text('body')),
),
));
showModalBottomSheet<void>(
context: scaffoldKey.currentContext!,
showDragHandle: true,
builder: (BuildContext context) {
return const Text('BottomSheet');
},
);
await tester.pump(); // bottom sheet show animation starts
await tester.pump(const Duration(seconds: 1)); // animation done
final Finder dragHandle = find.bySemanticsLabel('Dismiss');
expect(
tester.getSize(dragHandle),
const Size(48, 48),
);
final Offset center = tester.getCenter(dragHandle);
final Offset edge = tester.getTopLeft(dragHandle) - const Offset(1, 1);
// Shows default drag handle color
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: edge);
await tester.pump();
BoxDecoration boxDecoration=tester.widget<Container>(find.descendant(
of: dragHandle,
matching: find.byWidgetPredicate((Widget widget) => widget is Container && widget.decoration != null),
)).decoration! as BoxDecoration;
expect(boxDecoration.color, defaultColor);
// Shows hovering drag handle color
await gesture.moveTo(center);
await tester.pump();
boxDecoration = tester.widget<Container>(find.descendant(
of: dragHandle,
matching: find.byWidgetPredicate((Widget widget) => widget is Container && widget.decoration != null),
)).decoration! as BoxDecoration;
expect(boxDecoration.color, hoveringColor);
});
testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async { testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: Scaffold( home: Scaffold(
...@@ -1617,6 +1749,21 @@ void main() { ...@@ -1617,6 +1749,21 @@ void main() {
}); });
group('constraints', () { group('constraints', () {
testWidgets('default constraints are max width 640 in material 3', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.light(useMaterial3: true),
home: const MediaQuery(
data: MediaQueryData(size: Size(1000, 1000)),
child: Scaffold(
body: Center(child: Text('body')),
bottomSheet: Placeholder(fallbackWidth: 800),
),
),
),
);
expect(tester.getSize(find.byType(Placeholder)).width, 640);
});
testWidgets('No constraints by default for bottomSheet property', (WidgetTester tester) async { testWidgets('No constraints by default for bottomSheet property', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp( await tester.pumpWidget(const MaterialApp(
......
...@@ -32,6 +32,8 @@ void main() { ...@@ -32,6 +32,8 @@ void main() {
expect(bottomSheetTheme.shape, null); expect(bottomSheetTheme.shape, null);
expect(bottomSheetTheme.clipBehavior, null); expect(bottomSheetTheme.clipBehavior, null);
expect(bottomSheetTheme.constraints, null); expect(bottomSheetTheme.constraints, null);
expect(bottomSheetTheme.dragHandleColor, null);
expect(bottomSheetTheme.dragHandleSize, null);
}); });
testWidgets('Default BottomSheetThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default BottomSheetThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -55,6 +57,8 @@ void main() { ...@@ -55,6 +57,8 @@ void main() {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
constraints: BoxConstraints(minWidth: 200, maxWidth: 640), constraints: BoxConstraints(minWidth: 200, maxWidth: 640),
dragHandleColor: Color(0xFFFFFFFF),
dragHandleSize: Size(20, 20)
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -67,6 +71,8 @@ void main() { ...@@ -67,6 +71,8 @@ void main() {
'elevation: 2.0', 'elevation: 2.0',
'shadowColor: Color(0xff00ffff)', 'shadowColor: Color(0xff00ffff)',
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
'dragHandleColor: Color(0xffffffff)',
'dragHandleSize: Size(20.0, 20.0)',
'clipBehavior: Clip.antiAlias', 'clipBehavior: Clip.antiAlias',
'constraints: BoxConstraints(200.0<=w<=640.0, 0.0<=h<=Infinity)', 'constraints: BoxConstraints(200.0<=w<=640.0, 0.0<=h<=Infinity)',
]); ]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment