diff --git a/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart b/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart new file mode 100644 index 000000000000..e63885485c69 --- /dev/null +++ b/examples/api/lib/material/bottom_sheet/show_bottom_sheet.0.dart @@ -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 createState() => _BottomSheetExampleState(); +} + +class _BottomSheetExampleState extends State { + Set _animationStyleSelection = {AnimationStyles.defaultStyle}; + AnimationStyle? _animationStyle; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SegmentedButton( + selected: _animationStyleSelection, + onSelectionChanged: (Set 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>(((AnimationStyles, String) shirt) { + return ButtonSegment(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: [ + const Text('Bottom sheet'), + ElevatedButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart b/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart new file mode 100644 index 000000000000..e602c50be0f5 --- /dev/null +++ b/examples/api/lib/material/bottom_sheet/show_modal_bottom_sheet.2.dart @@ -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 createState() => _ModalBottomSheetExampleState(); +} + +class _ModalBottomSheetExampleState extends State { + Set _animationStyleSelection = {AnimationStyles.defaultStyle}; + AnimationStyle? _animationStyle; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SegmentedButton( + selected: _animationStyleSelection, + onSelectionChanged: (Set 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>(((AnimationStyles, String) shirt) { + return ButtonSegment(value: shirt.$1, label: Text(shirt.$2)); + }) + .toList(), + ), + const SizedBox(height: 10), + ElevatedButton( + child: const Text('showModalBottomSheet'), + onPressed: () { + showModalBottomSheet( + context: context, + sheetAnimationStyle: _animationStyle, + builder: (BuildContext context) { + return SizedBox.expand( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Modal bottom sheet'), + ElevatedButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart b/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart index 894d727b55b0..10bfd0d4e652 100644 --- a/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart +++ b/examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart @@ -34,7 +34,7 @@ class SnackBarExample extends StatefulWidget { } class _SnackBarExampleState extends State { - final Set _animationStyleSelection = {AnimationStyles.defaultStyle}; + Set _animationStyleSelection = {AnimationStyles.defaultStyle}; AnimationStyle? _animationStyle; @override @@ -57,6 +57,7 @@ class _SnackBarExampleState extends State { ), AnimationStyles.none => AnimationStyle.noAnimation, }; + _animationStyleSelection = styles; }); }, segments: animationStyleSegments diff --git a/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart b/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart new file mode 100644 index 000000000000..c00a98f8d429 --- /dev/null +++ b/examples/api/lib/material/scaffold/scaffold_state.show_bottom_sheet.1.dart @@ -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 createState() => _ShowBottomSheetExampleState(); +} + +class _ShowBottomSheetExampleState extends State { + Set _animationStyleSelection = {AnimationStyles.defaultStyle}; + AnimationStyle? _animationStyle; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SegmentedButton( + selected: _animationStyleSelection, + onSelectionChanged: (Set 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>(((AnimationStyles, String) shirt) { + return ButtonSegment(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: [ + const Text('BottomSheet'), + ElevatedButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart b/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart new file mode 100644 index 000000000000..9347cfe55140 --- /dev/null +++ b/examples/api/test/material/bottom_sheet/show_bottom_sheet.0_test.dart @@ -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)); + }); +} diff --git a/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart b/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart new file mode 100644 index 000000000000..6a582a344997 --- /dev/null +++ b/examples/api/test/material/bottom_sheet/show_modal_bottom_sheet.2_test.dart @@ -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)); + }); +} diff --git a/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart b/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart new file mode 100644 index 000000000000..8ff0a1fc8f2e --- /dev/null +++ b/examples/api/test/material/scaffold/scaffold_state.show_bottom_sheet.1_test.dart @@ -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)); + }); +} diff --git a/packages/flutter/lib/src/animation/animation_style.dart b/packages/flutter/lib/src/animation/animation_style.dart index 18f95aa24642..07894da70027 100644 --- a/packages/flutter/lib/src/animation/animation_style.dart +++ b/packages/flutter/lib/src/animation/animation_style.dart @@ -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. diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index f63e2c039897..e9a986aeba9d 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -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 extends PopupRoute { this.transitionAnimationController, this.anchorPoint, this.useSafeArea = false, + this.sheetAnimationStyle, }); /// A builder for the contents of the sheet. @@ -992,6 +996,20 @@ class ModalBottomSheetRoute extends PopupRoute { /// 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 extends PopupRoute { _animationController = transitionAnimationController; willDisposeAnimationController = false; } else { - _animationController = BottomSheet.createAnimationController(navigator!); + _animationController = BottomSheet.createAnimationController( + navigator!, + sheetAnimationStyle: sheetAnimationStyle, + ); } return _animationController!; } @@ -1159,6 +1180,26 @@ class ModalBottomSheetRoute extends PopupRoute { /// ** 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 extends PopupRoute { /// [DisplayFeature]s can split the screen into sub-screens. /// * The Material 2 spec at . /// * The Material 3 spec at . +/// * [AnimationStyle], which is used to override the modal bottom sheet +/// animation duration and reverse animation duration. Future showModalBottomSheet({ required BuildContext context, required WidgetBuilder builder, @@ -1191,6 +1234,7 @@ Future showModalBottomSheet({ RouteSettings? routeSettings, AnimationController? transitionAnimationController, Offset? anchorPoint, + AnimationStyle? sheetAnimationStyle, }) { assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMaterialLocalizations(context)); @@ -1217,6 +1261,7 @@ Future showModalBottomSheet({ transitionAnimationController: transitionAnimationController, anchorPoint: anchorPoint, useSafeArea: useSafeArea, + sheetAnimationStyle: sheetAnimationStyle, )); } @@ -1235,6 +1280,26 @@ Future showModalBottomSheet({ /// 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 showModalBottomSheet({ /// * [Scaffold.of], for information about how to obtain the [BuildContext]. /// * The Material 2 spec at . /// * The Material 3 spec at . +/// * [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, ); } diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 0eb3b8f40b3b..59e97126aef3 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -2506,6 +2506,26 @@ class ScaffoldState extends State 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 with TickerProviderStateMixin, Resto /// * [Scaffold.of], for information about how to obtain the [ScaffoldState]. /// * The Material 2 spec at . /// * The Material 3 spec at . + /// * [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 with TickerProviderStateMixin, Resto bool? enableDrag, bool? showDragHandle, AnimationController? transitionAnimationController, + AnimationStyle? sheetAnimationStyle, }) { assert(() { if (widget.bottomSheet != null) { @@ -2540,7 +2563,9 @@ class ScaffoldState extends State 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, diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 5ac634e914b8..101191a3bd65 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -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( + 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( + 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 { diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 8a5f56fd3a6b..114fa002c9a6 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -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 {