Add AnimationStyle to showBottomSheet and showModalBottomSheet (#145536)

fixes [Introduce animation customizable with `AnimationStyle` to `BottomSheet`](https://github.com/flutter/flutter/issues/145532)

### Default bottom sheet animation
![00-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/a295b002-b310-4dea-8bc4-23b1d299748c)

### Custom bottom sheet animation
![01-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/8c5c3d5f-e67d-4ed5-880d-f17d262087e1)

### No bottom sheet animation
![02-ezgif com-video-to-gif-converter](https://github.com/flutter/flutter/assets/48603081/872409d8-8a8d-4db9-b95b-7f96a62cdffc)
This commit is contained in:
Taha Tesser 2024-03-25 10:39:05 +02:00 committed by GitHub
parent 62adaff870
commit 23687c5260
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1007 additions and 6 deletions

View file

@ -0,0 +1,101 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [showBottomSheet].
void main() => runApp(const BottomSheetExampleApp());
class BottomSheetExampleApp extends StatelessWidget {
const BottomSheetExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Bottom Sheet Sample')),
body: const BottomSheetExample(),
),
);
}
}
enum AnimationStyles { defaultStyle, custom, none }
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
(AnimationStyles.defaultStyle, 'Default'),
(AnimationStyles.custom, 'Custom'),
(AnimationStyles.none, 'None'),
];
class BottomSheetExample extends StatefulWidget {
const BottomSheetExample({super.key});
@override
State<BottomSheetExample> createState() => _BottomSheetExampleState();
}
class _BottomSheetExampleState extends State<BottomSheetExample> {
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
AnimationStyle? _animationStyle;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SegmentedButton<AnimationStyles>(
selected: _animationStyleSelection,
onSelectionChanged: (Set<AnimationStyles> styles) {
setState(() {
_animationStyle = switch (styles.first) {
AnimationStyles.defaultStyle => null,
AnimationStyles.custom => AnimationStyle(
duration: const Duration(seconds: 3),
reverseDuration: const Duration(seconds: 1),
),
AnimationStyles.none => AnimationStyle.noAnimation,
};
_animationStyleSelection = styles;
});
},
segments: animationStyleSegments
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
})
.toList(),
),
const SizedBox(height: 10),
ElevatedButton(
child: const Text('showBottomSheet'),
onPressed: () {
showBottomSheet(
context: context,
sheetAnimationStyle: _animationStyle,
builder: (BuildContext context) {
return SizedBox.expand(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Bottom sheet'),
ElevatedButton(
child: const Text('Close'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
},
);
},
),
],
),
);
}
}

View file

@ -0,0 +1,101 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [showModalBottomSheet].
void main() => runApp(const ModalBottomSheetApp());
class ModalBottomSheetApp extends StatelessWidget {
const ModalBottomSheetApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Modal Bottom Sheet Sample')),
body: const ModalBottomSheetExample(),
),
);
}
}
enum AnimationStyles { defaultStyle, custom, none }
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
(AnimationStyles.defaultStyle, 'Default'),
(AnimationStyles.custom, 'Custom'),
(AnimationStyles.none, 'None'),
];
class ModalBottomSheetExample extends StatefulWidget {
const ModalBottomSheetExample({super.key});
@override
State<ModalBottomSheetExample> createState() => _ModalBottomSheetExampleState();
}
class _ModalBottomSheetExampleState extends State<ModalBottomSheetExample> {
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
AnimationStyle? _animationStyle;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SegmentedButton<AnimationStyles>(
selected: _animationStyleSelection,
onSelectionChanged: (Set<AnimationStyles> styles) {
setState(() {
_animationStyle = switch (styles.first) {
AnimationStyles.defaultStyle => null,
AnimationStyles.custom => AnimationStyle(
duration: const Duration(seconds: 3),
reverseDuration: const Duration(seconds: 1),
),
AnimationStyles.none => AnimationStyle.noAnimation,
};
_animationStyleSelection = styles;
});
},
segments: animationStyleSegments
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
})
.toList(),
),
const SizedBox(height: 10),
ElevatedButton(
child: const Text('showModalBottomSheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
sheetAnimationStyle: _animationStyle,
builder: (BuildContext context) {
return SizedBox.expand(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Modal bottom sheet'),
ElevatedButton(
child: const Text('Close'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
},
);
},
),
],
),
);
}
}

View file

@ -34,7 +34,7 @@ class SnackBarExample extends StatefulWidget {
}
class _SnackBarExampleState extends State<SnackBarExample> {
final Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
AnimationStyle? _animationStyle;
@override
@ -57,6 +57,7 @@ class _SnackBarExampleState extends State<SnackBarExample> {
),
AnimationStyles.none => AnimationStyle.noAnimation,
};
_animationStyleSelection = styles;
});
},
segments: animationStyleSegments

View file

@ -0,0 +1,103 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [ScaffoldState.showBottomSheet].
void main() => runApp(const ShowBottomSheetExampleApp());
class ShowBottomSheetExampleApp extends StatelessWidget {
const ShowBottomSheetExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ScaffoldState BottomSheet Sample')),
body: const ShowBottomSheetExample(),
),
);
}
}
enum AnimationStyles { defaultStyle, custom, none }
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
(AnimationStyles.defaultStyle, 'Default'),
(AnimationStyles.custom, 'Custom'),
(AnimationStyles.none, 'None'),
];
class ShowBottomSheetExample extends StatefulWidget {
const ShowBottomSheetExample({super.key});
@override
State<ShowBottomSheetExample> createState() => _ShowBottomSheetExampleState();
}
class _ShowBottomSheetExampleState extends State<ShowBottomSheetExample> {
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
AnimationStyle? _animationStyle;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SegmentedButton<AnimationStyles>(
selected: _animationStyleSelection,
onSelectionChanged: (Set<AnimationStyles> styles) {
setState(() {
_animationStyle = switch (styles.first) {
AnimationStyles.defaultStyle => null,
AnimationStyles.custom => AnimationStyle(
duration: const Duration(seconds: 3),
reverseDuration: const Duration(seconds: 1),
),
AnimationStyles.none => AnimationStyle.noAnimation,
};
_animationStyleSelection = styles;
});
},
segments: animationStyleSegments
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
})
.toList(),
),
const SizedBox(height: 10),
ElevatedButton(
child: const Text('showBottomSheet'),
onPressed: () {
Scaffold.of(context).showBottomSheet(
sheetAnimationStyle: _animationStyle,
(BuildContext context) {
return SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('BottomSheet'),
ElevatedButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
);
},
);
},
),
],
),
);
}
}

View file

@ -0,0 +1,66 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/bottom_sheet/show_bottom_sheet.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Bottom sheet animation can be customized using AnimationStyle', (WidgetTester tester) async {
await tester.pumpWidget(
const example.BottomSheetExampleApp(),
);
// Show the bottom sheet with default animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(178.0, 0.1));
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(56.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select custom animation style.
await tester.tap(find.text('Custom'));
await tester.pumpAndSettle();
// Show the bottom sheet with custom animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(178.0, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(56.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select no animation style.
await tester.tap(find.text('None'));
await tester.pumpAndSettle();
// Show the bottom sheet with no animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(56.0));
});
}

View file

@ -0,0 +1,66 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/bottom_sheet/show_modal_bottom_sheet.2.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Modal bottom sheet animation can be customized using AnimationStyle', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ModalBottomSheetApp(),
);
// Show the bottom sheet with default animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showModalBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(262.5));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select custom animation style.
await tester.tap(find.text('Custom'));
await tester.pumpAndSettle();
// Show the bottom sheet with custom animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showModalBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(262.5));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select no animation style.
await tester.tap(find.text('None'));
await tester.pumpAndSettle();
// Show the bottom sheet with no animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showModalBottomSheet'));
await tester.pump();
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(262.5));
});
}

View file

@ -0,0 +1,66 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/scaffold/scaffold_state.show_bottom_sheet.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Scaffold showBottomSheet animation can be customized using AnimationStyle', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ShowBottomSheetExampleApp(),
);
// Show the bottom sheet with default animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(444.8, 0.1));
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(400.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select custom animation style.
await tester.tap(find.text('Custom'));
await tester.pumpAndSettle();
// Show the bottom sheet with custom animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, closeTo(444.8, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 1500));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(400.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(ElevatedButton, 'Close'));
await tester.pumpAndSettle();
// Select no animation style.
await tester.tap(find.text('None'));
await tester.pumpAndSettle();
// Show the bottom sheet with no animation style.
await tester.tap(find.widgetWithText(ElevatedButton, 'showBottomSheet'));
await tester.pump();
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, equals(400.0));
});
}

View file

@ -13,6 +13,9 @@ import 'tween.dart';
/// - [ExpansionTile]
/// - [MaterialApp]
/// - [PopupMenuButton]
/// - [ScaffoldMessengerState.showSnackBar]
/// - [showBottomSheet]
/// - [showModalBottomSheet]
///
/// If [duration] and [reverseDuration] are set to [Duration.zero], the
/// corresponding animation will be disabled.

View file

@ -237,10 +237,13 @@ class BottomSheet extends StatefulWidget {
/// This API available as a convenience for a Material compliant bottom sheet
/// animation. If alternative animation durations are required, a different
/// animation controller could be provided.
static AnimationController createAnimationController(TickerProvider vsync) {
static AnimationController createAnimationController(
TickerProvider vsync,
{ AnimationStyle? sheetAnimationStyle }
) {
return AnimationController(
duration: _bottomSheetEnterDuration,
reverseDuration: _bottomSheetExitDuration,
duration: sheetAnimationStyle?.duration ?? _bottomSheetEnterDuration,
reverseDuration: sheetAnimationStyle?.reverseDuration ?? _bottomSheetExitDuration,
debugLabel: 'BottomSheet',
vsync: vsync,
);
@ -846,6 +849,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
this.transitionAnimationController,
this.anchorPoint,
this.useSafeArea = false,
this.sheetAnimationStyle,
});
/// A builder for the contents of the sheet.
@ -992,6 +996,20 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// The default is false.
final bool useSafeArea;
/// Used to override the modal bottom sheet animation duration and reverse
/// animation duration.
///
/// If [AnimationStyle.duration] is provided, it will be used to override
/// the modal bottom sheet animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
/// override the modal bottom sheet reverse animation duration in the
/// underlying [BottomSheet.createAnimationController].
///
/// To disable the modal bottom sheet animation, use [AnimationStyle.noAnimation].
final AnimationStyle? sheetAnimationStyle;
/// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
/// The semantic hint text that informs users what will happen if they
/// tap on the widget. Announced in the format of 'Double tap to ...'.
@ -1051,7 +1069,10 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
_animationController = transitionAnimationController;
willDisposeAnimationController = false;
} else {
_animationController = BottomSheet.createAnimationController(navigator!);
_animationController = BottomSheet.createAnimationController(
navigator!,
sheetAnimationStyle: sheetAnimationStyle,
);
}
return _animationController!;
}
@ -1159,6 +1180,26 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.1.dart **
/// {@end-tool}
///
/// The [sheetAnimationStyle] parameter is used to override the modal bottom sheet
/// animation duration and reverse animation duration.
///
/// If [AnimationStyle.duration] is provided, it will be used to override
/// the modal bottom sheet animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
/// override the modal bottom sheet reverse animation duration in the
/// underlying [BottomSheet.createAnimationController].
///
/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
///
/// {@tool dartpad}
/// This sample showcases how to override the [showModalBottomSheet] animation
/// duration and reverse animation duration using [AnimationStyle].
///
/// ** See code in examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [BottomSheet], which becomes the parent of the widget returned by the
@ -1171,6 +1212,8 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
/// [DisplayFeature]s can split the screen into sub-screens.
/// * 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>.
/// * [AnimationStyle], which is used to override the modal bottom sheet
/// animation duration and reverse animation duration.
Future<T?> showModalBottomSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
@ -1191,6 +1234,7 @@ Future<T?> showModalBottomSheet<T>({
RouteSettings? routeSettings,
AnimationController? transitionAnimationController,
Offset? anchorPoint,
AnimationStyle? sheetAnimationStyle,
}) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
@ -1217,6 +1261,7 @@ Future<T?> showModalBottomSheet<T>({
transitionAnimationController: transitionAnimationController,
anchorPoint: anchorPoint,
useSafeArea: useSafeArea,
sheetAnimationStyle: sheetAnimationStyle,
));
}
@ -1235,6 +1280,26 @@ Future<T?> showModalBottomSheet<T>({
/// The [enableDrag] parameter specifies whether the bottom sheet can be
/// dragged up and down and dismissed by swiping downwards.
///
/// The [sheetAnimationStyle] parameter is used to override the bottom sheet
/// animation duration and reverse animation duration.
///
/// If [AnimationStyle.duration] is provided, it will be used to override
/// the bottom sheet animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
/// override the bottom sheet reverse animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
///
/// {@tool dartpad}
/// This sample showcases how to override the [showBottomSheet] animation
/// duration and reverse animation duration using [AnimationStyle].
///
/// ** See code in examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart **
/// {@end-tool}
///
/// To rebuild the bottom sheet (e.g. if it is stateful), call
/// [PersistentBottomSheetController.setState] on the controller returned by
/// this method.
@ -1265,6 +1330,8 @@ Future<T?> showModalBottomSheet<T>({
/// * [Scaffold.of], for information about how to obtain the [BuildContext].
/// * 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>.
/// * [AnimationStyle], which is used to override the bottom sheet animation
/// duration and reverse animation duration.
PersistentBottomSheetController showBottomSheet({
required BuildContext context,
required WidgetBuilder builder,
@ -1276,6 +1343,7 @@ PersistentBottomSheetController showBottomSheet({
bool? enableDrag,
bool? showDragHandle,
AnimationController? transitionAnimationController,
AnimationStyle? sheetAnimationStyle,
}) {
assert(debugCheckHasScaffold(context));
@ -1289,6 +1357,7 @@ PersistentBottomSheetController showBottomSheet({
enableDrag: enableDrag,
showDragHandle: showDragHandle,
transitionAnimationController: transitionAnimationController,
sheetAnimationStyle: sheetAnimationStyle,
);
}

View file

@ -2506,6 +2506,26 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
///
/// ** See code in examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.0.dart **
/// {@end-tool}
///
/// The [sheetAnimationStyle] parameter is used to override the bottom sheet
/// animation duration and reverse animation duration.
///
/// If [AnimationStyle.duration] is provided, it will be used to override
/// the bottom sheet animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// If [AnimationStyle.reverseDuration] is provided, it will be used to
/// override the bottom sheet reverse animation duration in the underlying
/// [BottomSheet.createAnimationController].
///
/// To disable the bottom sheet animation, use [AnimationStyle.noAnimation].
///
/// {@tool dartpad}
/// This sample showcases how to override the [showBottomSheet] animation
/// duration and reverse animation duration using [AnimationStyle].
///
/// ** See code in examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart **
/// {@end-tool}
/// See also:
///
/// * [BottomSheet], which becomes the parent of the widget returned by the
@ -2516,6 +2536,8 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
/// * [Scaffold.of], for information about how to obtain the [ScaffoldState].
/// * 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>.
/// * [AnimationStyle], which is used to override the modal bottom sheet
/// animation duration and reverse animation duration.
PersistentBottomSheetController showBottomSheet(
WidgetBuilder builder, {
Color? backgroundColor,
@ -2526,6 +2548,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
bool? enableDrag,
bool? showDragHandle,
AnimationController? transitionAnimationController,
AnimationStyle? sheetAnimationStyle,
}) {
assert(() {
if (widget.bottomSheet != null) {
@ -2540,7 +2563,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
assert(debugCheckHasMediaQuery(context));
_closeCurrentBottomSheet();
final AnimationController controller = (transitionAnimationController ?? BottomSheet.createAnimationController(this))..forward();
final AnimationController controller = (transitionAnimationController
?? BottomSheet.createAnimationController(this, sheetAnimationStyle: sheetAnimationStyle))
..forward();
setState(() {
_currentBottomSheet = _buildBottomSheet(
builder,

View file

@ -2290,6 +2290,252 @@ void main() {
expect(modalBarrier.semanticsLabel, MaterialLocalizations.of(scaffoldKey.currentContext!).scrimLabel);
});
});
testWidgets('Bottom sheet animation can be customized', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
Widget buildWidget({ AnimationStyle? sheetAnimationStyle }) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
showBottomSheet(
context: context,
sheetAnimationStyle: sheetAnimationStyle,
builder: (BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
);
}
// Test custom animation style.
await tester.pumpWidget(buildWidget(
sheetAnimationStyle: AnimationStyle(
duration: const Duration(milliseconds: 800),
reverseDuration: const Duration(milliseconds: 400),
),
));
await tester.tap(find.text('X'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is dismissed.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
// Test no animation style.
await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pump();
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// The bottom sheet is dismissed.
expect(find.byKey(sheetKey), findsNothing);
});
testWidgets('Modal bottom sheet default animation', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
// Test default modal bottom sheet animation.
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
));
// Tap the 'X' to show the bottom sheet.
await tester.tap(find.text('X'));
await tester.pump();
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The modal bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The modal bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// Advance the animation by 1/2 of the default reverse duration.
await tester.pump(const Duration(milliseconds: 100));
// The modal bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the default reverse duration.
await tester.pump(const Duration(milliseconds: 100));
// The modal bottom sheet is dismissed.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
});
testWidgets('Modal bottom sheet animation can be customized', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
Widget buildWidget({ AnimationStyle? sheetAnimationStyle }) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
showModalBottomSheet<void>(
context: context,
sheetAnimationStyle: sheetAnimationStyle,
builder: (BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
);
}
// Test custom animation style.
await tester.pumpWidget(buildWidget(
sheetAnimationStyle: AnimationStyle(
duration: const Duration(milliseconds: 800),
reverseDuration: const Duration(milliseconds: 400),
),
));
await tester.tap(find.text('X'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(316.7, 0.1));
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is dismissed.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
// Test no animation style.
await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pump();
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(262.5));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// The bottom sheet is dismissed.
expect(find.byKey(sheetKey), findsNothing);
});
}
class _TestPage extends StatelessWidget {

View file

@ -3102,6 +3102,160 @@ void main() {
// The SnackBar is dismissed.
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
});
testWidgets('Scaffold showBottomSheet default animation', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
// Test default bottom sheet animation.
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showBottomSheet(
(BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
));
// Tap the 'X' to show the bottom sheet.
await tester.tap(find.text('X'));
await tester.pump();
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the default forward duration.
await tester.pump(const Duration(milliseconds: 125));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// Advance the animation by 1/2 of the default reverse duration.
await tester.pump(const Duration(milliseconds: 100));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the default reverse duration.
await tester.pump(const Duration(milliseconds: 100));
// The bottom sheet is dismissed.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
});
testWidgets('Scaffold showBottomSheet animation can be customized', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
Widget buildWidget({ AnimationStyle? sheetAnimationStyle }) {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
Scaffold.of(context).showBottomSheet(
sheetAnimationStyle: sheetAnimationStyle,
(BuildContext context) {
return SizedBox.expand(
child: ColoredBox(
key: sheetKey,
color: Theme.of(context).colorScheme.primary,
child: FilledButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
),
);
},
);
},
child: const Text('X'),
);
},
),
),
);
}
// Test custom animation style.
await tester.pumpWidget(buildWidget(
sheetAnimationStyle: AnimationStyle(
duration: const Duration(milliseconds: 800),
reverseDuration: const Duration(milliseconds: 400),
),
));
await tester.tap(find.text('X'));
await tester.pump();
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the custom forward duration.
await tester.pump(const Duration(milliseconds: 400));
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is partially visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, closeTo(134.6, 0.1));
// Advance the animation by 1/2 of the custom reverse duration.
await tester.pump(const Duration(milliseconds: 200));
// The bottom sheet is dismissed.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(600.0));
// Test no animation style.
await tester.pumpWidget(buildWidget(sheetAnimationStyle: AnimationStyle.noAnimation));
await tester.pumpAndSettle();
await tester.tap(find.text('X'));
await tester.pump();
// The bottom sheet is fully visible.
expect(tester.getTopLeft(find.byKey(sheetKey)).dy, equals(0.0));
// Dismiss the bottom sheet.
await tester.tap(find.widgetWithText(FilledButton, 'Close'));
await tester.pump();
// The bottom sheet is dismissed.
expect(find.byKey(sheetKey), findsNothing);
});
}
class _GeometryListener extends StatefulWidget {