Unverified Commit 746ce203 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Added the ability to constrain the size of bottom sheets. (#80527)

parent 21fd5cdd
......@@ -78,6 +78,7 @@ class BottomSheet extends StatefulWidget {
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
required this.onClosing,
required this.builder,
}) : assert(enableDrag != null),
......@@ -162,6 +163,23 @@ class BottomSheet extends StatefulWidget {
/// will be [Clip.none].
final Clip? clipBehavior;
/// Defines minimum and maximum sizes for a [BottomSheet].
///
/// Typically a bottom sheet will cover the entire width of its
/// parent. However for large screens you may want to limit the width
/// to something smaller and this property provides a way to specify
/// a maximum width.
///
/// If null, then the ambient [ThemeData.bottomSheetTheme]'s
/// [BottomSheetThemeData.constraints] will be used. If that
/// is null then the bottom sheet's size will be constrained
/// by its parent (usually a [Scaffold]).
///
/// If constraints are specified (either in this property or in the
/// theme), the bottom sheet will be aligned to the bottom-center of
/// the available space. Otherwise, no alignment is applied.
final BoxConstraints? constraints;
@override
_BottomSheetState createState() => _BottomSheetState();
......@@ -244,12 +262,13 @@ class _BottomSheetState extends State<BottomSheet> {
@override
Widget build(BuildContext context) {
final BottomSheetThemeData bottomSheetTheme = Theme.of(context).bottomSheetTheme;
final BoxConstraints? constraints = widget.constraints ?? bottomSheetTheme.constraints;
final Color? color = widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
final double elevation = widget.elevation ?? bottomSheetTheme.elevation ?? 0;
final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape;
final Clip clipBehavior = widget.clipBehavior ?? bottomSheetTheme.clipBehavior ?? Clip.none;
final Widget bottomSheet = Material(
Widget bottomSheet = Material(
key: _childKey,
color: color,
elevation: elevation,
......@@ -260,6 +279,17 @@ class _BottomSheetState extends State<BottomSheet> {
child: widget.builder(context),
),
);
if (constraints != null) {
bottomSheet = Align(
alignment: Alignment.bottomCenter,
child: ConstrainedBox(
constraints: constraints,
child: bottomSheet,
),
);
}
return !widget.enableDrag ? bottomSheet : GestureDetector(
onVerticalDragStart: _handleDragStart,
onVerticalDragUpdate: _handleDragUpdate,
......@@ -313,6 +343,7 @@ class _ModalBottomSheet<T> extends StatefulWidget {
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.isScrollControlled = false,
this.enableDrag = true,
}) : assert(isScrollControlled != null),
......@@ -325,6 +356,7 @@ class _ModalBottomSheet<T> extends StatefulWidget {
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
final bool enableDrag;
@override
......@@ -382,6 +414,7 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
elevation: widget.elevation,
shape: widget.shape,
clipBehavior: widget.clipBehavior,
constraints: widget.constraints,
enableDrag: widget.enableDrag,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
......@@ -418,6 +451,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.modalBarrierColor,
this.isDismissible = true,
this.enableDrag = true,
......@@ -436,6 +470,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
final Color? modalBarrierColor;
final bool isDismissible;
final bool enableDrag;
......@@ -481,6 +516,7 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
elevation: elevation ?? sheetTheme.modalElevation ?? sheetTheme.elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
isScrollControlled: isScrollControlled,
enableDrag: enableDrag,
);
......@@ -582,9 +618,11 @@ class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
/// The [enableDrag] parameter specifies whether the bottom sheet can be
/// dragged up and down and dismissed by swiping downwards.
///
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior] and [transitionAnimationController]
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
/// parameters can be passed in to customize the appearance and behavior of
/// modal bottom sheets.
/// modal bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
///
/// The [transitionAnimationController] controls the bottom sheet's entrance and
/// exit animations if provided.
......@@ -653,6 +691,7 @@ Future<T?> showModalBottomSheet<T>({
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
Color? barrierColor,
bool isScrollControlled = false,
bool useRootNavigator = false,
......@@ -680,6 +719,7 @@ Future<T?> showModalBottomSheet<T>({
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
isDismissible: isDismissible,
modalBarrierColor: barrierColor,
enableDrag: enableDrag,
......@@ -694,9 +734,11 @@ Future<T?> showModalBottomSheet<T>({
/// Returns a controller that can be used to close and otherwise manipulate the
/// bottom sheet.
///
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior] and [transitionAnimationController]
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
/// [constraints] and [transitionAnimationController]
/// parameters can be passed in to customize the appearance and behavior of
/// persistent bottom sheets.
/// persistent bottom sheets (see the documentation for these on [BottomSheet]
/// for more details).
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the controller returned by
......@@ -734,6 +776,7 @@ PersistentBottomSheetController<T> showBottomSheet<T>({
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
AnimationController? transitionAnimationController,
}) {
assert(context != null);
......@@ -746,6 +789,7 @@ PersistentBottomSheetController<T> showBottomSheet<T>({
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
transitionAnimationController: transitionAnimationController,
);
}
......@@ -34,6 +34,7 @@ class BottomSheetThemeData with Diagnosticable {
this.modalElevation,
this.shape,
this.clipBehavior,
this.constraints,
});
/// Default value for [BottomSheet.backgroundColor].
......@@ -67,6 +68,11 @@ class BottomSheetThemeData with Diagnosticable {
/// If null, [BottomSheet] uses [Clip.none].
final Clip? clipBehavior;
/// Constrains the size of the [BottomSheet].
///
/// If null, the bottom sheet's size will be unconstrained.
final BoxConstraints? constraints;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
BottomSheetThemeData copyWith({
......@@ -76,6 +82,7 @@ class BottomSheetThemeData with Diagnosticable {
double? modalElevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
}) {
return BottomSheetThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
......@@ -84,6 +91,7 @@ class BottomSheetThemeData with Diagnosticable {
modalElevation: modalElevation ?? this.modalElevation,
shape: shape ?? this.shape,
clipBehavior: clipBehavior ?? this.clipBehavior,
constraints: constraints ?? this.constraints,
);
}
......@@ -103,6 +111,7 @@ class BottomSheetThemeData with Diagnosticable {
modalElevation: lerpDouble(a?.modalElevation, b?.modalElevation, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
clipBehavior: t < 0.5 ? a?.clipBehavior : b?.clipBehavior,
constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t),
);
}
......@@ -115,6 +124,7 @@ class BottomSheetThemeData with Diagnosticable {
modalElevation,
shape,
clipBehavior,
constraints,
);
}
......@@ -130,7 +140,8 @@ class BottomSheetThemeData with Diagnosticable {
&& other.modalBackgroundColor == modalBackgroundColor
&& other.modalElevation == modalElevation
&& other.shape == shape
&& other.clipBehavior == clipBehavior;
&& other.clipBehavior == clipBehavior
&& other.constraints == constraints;
}
@override
......@@ -142,5 +153,6 @@ class BottomSheetThemeData with Diagnosticable {
properties.add(DoubleProperty('modalElevation', modalElevation, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null));
}
}
......@@ -2481,6 +2481,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
}) {
assert(() {
if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) {
......@@ -2554,6 +2555,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
);
if (!isPersistent)
......@@ -2653,6 +2655,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
AnimationController? transitionAnimationController,
}) {
assert(() {
......@@ -2678,6 +2681,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
);
});
return _currentBottomSheet! as PersistentBottomSheetController<T>;
......@@ -3356,6 +3360,7 @@ class _StandardBottomSheet extends StatefulWidget {
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
}) : super(key: key);
final AnimationController animationController; // we control it, but it must be disposed by whoever created it.
......@@ -3368,6 +3373,7 @@ class _StandardBottomSheet extends StatefulWidget {
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
@override
_StandardBottomSheetState createState() => _StandardBottomSheetState();
......@@ -3472,6 +3478,7 @@ class _StandardBottomSheetState extends State<_StandardBottomSheet> {
elevation: widget.elevation,
shape: widget.shape,
clipBehavior: widget.clipBehavior,
constraints: widget.constraints,
),
),
);
......
......@@ -833,6 +833,219 @@ void main() {
// The bottom sheet should not be showing any longer.
expect(find.text('BottomSheet'), findsNothing);
});
group('constraints', () {
testWidgets('No constraints by default for bottomSheet property', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Center(child: Text('body')),
bottomSheet: Text('BottomSheet'),
),
));
expect(find.text('BottomSheet'), findsOneWidget);
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(0, 586, 154, 600));
});
testWidgets('No constraints by default for showBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
Scaffold.of(context).showBottomSheet<void>(
(BuildContext context) => const Text('BottomSheet')
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(0, 586, 154, 600));
});
testWidgets('No constraints by default for showModalBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const Text('BottomSheet'),
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(0, 586, 800, 600));
});
testWidgets('Theme constraints used for bottomSheet property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
bottomSheetTheme: const BottomSheetThemeData(
constraints: BoxConstraints(maxWidth: 80),
)
),
home: const Scaffold(
body: Center(child: Text('body')),
bottomSheet: Text('BottomSheet'),
),
));
expect(find.text('BottomSheet'), findsOneWidget);
// Should be centered and only 80dp wide
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(360, 558, 440, 600));
});
testWidgets('Theme constraints used for showBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
bottomSheetTheme: const BottomSheetThemeData(
constraints: BoxConstraints(maxWidth: 80),
)
),
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
Scaffold.of(context).showBottomSheet<void>(
(BuildContext context) => const Text('BottomSheet')
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
// Should be centered and only 80dp wide
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(360, 558, 440, 600));
});
testWidgets('Theme constraints used for showModalBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
bottomSheetTheme: const BottomSheetThemeData(
constraints: BoxConstraints(maxWidth: 80),
)
),
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const Text('BottomSheet'),
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
// Should be centered and only 80dp wide
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(360, 558, 440, 600));
});
testWidgets('constraints param overrides theme for showBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
bottomSheetTheme: const BottomSheetThemeData(
constraints: BoxConstraints(maxWidth: 80),
)
),
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
Scaffold.of(context).showBottomSheet<void>(
(BuildContext context) => const Text('BottomSheet'),
constraints: const BoxConstraints(maxWidth: 100),
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
// Should be centered and only 100dp wide instead of 80dp wide
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(350, 572, 450, 600));
});
testWidgets('constraints param overrides theme for showModalBottomSheet', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
bottomSheetTheme: const BottomSheetThemeData(
constraints: BoxConstraints(maxWidth: 80),
)
),
home: Scaffold(
body: Builder(builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('Press me'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const Text('BottomSheet'),
constraints: const BoxConstraints(maxWidth: 100),
);
},
),
);
}),
),
));
expect(find.text('BottomSheet'), findsNothing);
await tester.tap(find.text('Press me'));
await tester.pumpAndSettle();
expect(find.text('BottomSheet'), findsOneWidget);
// Should be centered and only 100dp instead of 80dp wide
expect(tester.getRect(find.text('BottomSheet')),
const Rect.fromLTRB(350, 572, 450, 600));
});
});
}
class _TestPage extends StatelessWidget {
......
......@@ -18,6 +18,7 @@ void main() {
expect(bottomSheetTheme.elevation, null);
expect(bottomSheetTheme.shape, null);
expect(bottomSheetTheme.clipBehavior, null);
expect(bottomSheetTheme.constraints, null);
});
testWidgets('Default BottomSheetThemeData debugFillProperties', (WidgetTester tester) async {
......@@ -39,6 +40,7 @@ void main() {
elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2.0)),
clipBehavior: Clip.antiAlias,
constraints: const BoxConstraints(minWidth: 200, maxWidth: 640),
).debugFillProperties(builder);
final List<String> description = builder.properties
......@@ -51,6 +53,7 @@ void main() {
'elevation: 2.0',
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))',
'clipBehavior: Clip.antiAlias',
'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