mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
Add AnimationStyle
to showSnackBar
(#142825)
fixes [`showSnackBar` is always replacing `animation` parameter of `SnackBar`](https://github.com/flutter/flutter/issues/141646) ### Code sample preview ![Screenshot 2024-02-02 at 21 10 57](https://github.com/flutter/flutter/assets/48603081/66d808f0-d638-4561-b9a4-96d1b93938f4)
This commit is contained in:
parent
e6ba809b8a
commit
0cc381da19
|
@ -0,0 +1,90 @@
|
|||
// 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 [SnackBar].
|
||||
|
||||
void main() => runApp(const SnackBarApp());
|
||||
|
||||
class SnackBarApp extends StatelessWidget {
|
||||
const SnackBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: SnackBarExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AnimationStyles { defaultStyle, custom, none }
|
||||
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
|
||||
(AnimationStyles.defaultStyle, 'Default'),
|
||||
(AnimationStyles.custom, 'Custom'),
|
||||
(AnimationStyles.none, 'None'),
|
||||
];
|
||||
|
||||
class SnackBarExample extends StatefulWidget {
|
||||
const SnackBarExample({super.key});
|
||||
|
||||
@override
|
||||
State<SnackBarExample> createState() => _SnackBarExampleState();
|
||||
}
|
||||
|
||||
class _SnackBarExampleState extends State<SnackBarExample> {
|
||||
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
|
||||
AnimationStyle? _animationStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('SnackBar Sample')),
|
||||
body: 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,
|
||||
};
|
||||
});
|
||||
},
|
||||
segments: animationStyleSegments
|
||||
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
|
||||
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
snackBarAnimationStyle: _animationStyle,
|
||||
);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// 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_messenger_state.show_snack_bar.2.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('ScaffoldMessenger showSnackBar animation can be customized using AnimationStyle',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SnackBarApp(),
|
||||
);
|
||||
|
||||
// Tap the button to show the SnackBar with default animation style.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 250)); // Advance the animation by 250ms.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
|
||||
// Select custom animation style.
|
||||
await tester.tap(find.text('Custom'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the button to show the SnackBar with custom animation style.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 1500)); // Advance the animation by 125ms.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 1500)); // Advance the animation by 125ms.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1)); // Advance the animation by 1sec.
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
|
||||
// Select no animation style.
|
||||
await tester.tap(find.text('None'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the button to show the SnackBar with no animation style.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('I am a snack bar.'), findsNothing);
|
||||
});
|
||||
}
|
|
@ -290,13 +290,38 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
|||
/// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(SnackBar snackBar) {
|
||||
/// If [AnimationStyle.duration] is provided in the [snackBarAnimationStyle]
|
||||
/// parameter, it will be used to override the snackbar show animation duration.
|
||||
/// Otherwise, defaults to 250ms.
|
||||
///
|
||||
/// If [AnimationStyle.reverseDuration] is provided in the [snackBarAnimationStyle]
|
||||
/// parameter, it will be used to override the snackbar hide animation duration.
|
||||
/// Otherwise, defaults to 250ms.
|
||||
///
|
||||
/// To disable the snackbar animation, use [AnimationStyle.noAnimation].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample showcases how to override [SnackBar] show and hide animation
|
||||
/// duration using [AnimationStyle] in [ScaffoldMessengerState.showSnackBar].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/scaffold/scaffold_messenger_state.show_snack_bar.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
|
||||
SnackBar snackBar,
|
||||
{ AnimationStyle? snackBarAnimationStyle }
|
||||
) {
|
||||
assert(
|
||||
_scaffolds.isNotEmpty,
|
||||
'ScaffoldMessenger.showSnackBar was called, but there are currently no '
|
||||
'descendant Scaffolds to present to.',
|
||||
);
|
||||
_snackBarController ??= SnackBar.createAnimationController(vsync: this)
|
||||
_didUpdateAnimationStyle(snackBarAnimationStyle);
|
||||
_snackBarController ??= SnackBar.createAnimationController(
|
||||
duration: snackBarAnimationStyle?.duration,
|
||||
reverseDuration: snackBarAnimationStyle?.reverseDuration,
|
||||
vsync: this,
|
||||
)
|
||||
..addStatusListener(_handleSnackBarStatusChanged);
|
||||
if (_snackBars.isEmpty) {
|
||||
assert(_snackBarController!.isDismissed);
|
||||
|
@ -355,6 +380,16 @@ class ScaffoldMessengerState extends State<ScaffoldMessenger> with TickerProvide
|
|||
return controller;
|
||||
}
|
||||
|
||||
void _didUpdateAnimationStyle(AnimationStyle? snackBarAnimationStyle) {
|
||||
if (snackBarAnimationStyle != null) {
|
||||
if (_snackBarController?.duration != snackBarAnimationStyle.duration ||
|
||||
_snackBarController?.reverseDuration != snackBarAnimationStyle.reverseDuration) {
|
||||
_snackBarController?.dispose();
|
||||
_snackBarController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSnackBarStatusChanged(AnimationStatus status) {
|
||||
switch (status) {
|
||||
case AnimationStatus.dismissed:
|
||||
|
|
|
@ -476,9 +476,14 @@ class SnackBar extends StatefulWidget {
|
|||
// API for ScaffoldMessengerState.showSnackBar():
|
||||
|
||||
/// Creates an animation controller useful for driving a snack bar's entrance and exit animation.
|
||||
static AnimationController createAnimationController({ required TickerProvider vsync }) {
|
||||
static AnimationController createAnimationController({
|
||||
required TickerProvider vsync,
|
||||
Duration? duration,
|
||||
Duration? reverseDuration,
|
||||
}) {
|
||||
return AnimationController(
|
||||
duration: _snackBarTransitionDuration,
|
||||
duration: duration ?? _snackBarTransitionDuration,
|
||||
reverseDuration: reverseDuration,
|
||||
debugLabel: 'SnackBar',
|
||||
vsync: vsync,
|
||||
);
|
||||
|
|
|
@ -2868,6 +2868,241 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('ScaffoldMessenger showSnackBar default animatiom', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Tap the button to show the SnackBar.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
// The SnackBar is fully visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
// The SnackBar is dismissed.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
});
|
||||
|
||||
testWidgets('ScaffoldMessenger showSnackBar animation can be customized', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
snackBarAnimationStyle: AnimationStyle(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
reverseDuration: const Duration(milliseconds: 600),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Tap the button to show the SnackBar.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300)); // Advance the animation by 300ms.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(602.6, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 300)); // Advance the animation by 300ms.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 600)); // Advance the animation by 600ms.
|
||||
|
||||
// The SnackBar is fully visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300)); // Advance the animation by 300ns.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 300)); // Advance the animation by 300ms.
|
||||
|
||||
// The SnackBar is dismissed.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
});
|
||||
|
||||
testWidgets('Updated snackBarAnimationStyle updates snack bar animation', (WidgetTester tester) async {
|
||||
Widget buildSnackBar(AnimationStyle snackBarAnimationStyle) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
snackBarAnimationStyle: snackBarAnimationStyle,
|
||||
);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Test custom animation style.
|
||||
await tester.pumpWidget(buildSnackBar(AnimationStyle(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
reverseDuration: const Duration(milliseconds: 400),
|
||||
)));
|
||||
|
||||
// Tap the button to show the SnackBar.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400)); // Advance the animation by 400ms.
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 400)); // Advance the animation by 400ms.
|
||||
|
||||
// The SnackBar is fully visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400)); // Advance the animation by 400ms.
|
||||
|
||||
// The SnackBar is dismissed.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
|
||||
// Test no animation style.
|
||||
await tester.pumpWidget(buildSnackBar(AnimationStyle.noAnimation));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the button to show the SnackBar.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
|
||||
// The SnackBar is fully visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
|
||||
// The SnackBar is dismissed.
|
||||
expect(find.text('I am a snack bar.'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('snackBarAnimationStyle with only reverseDuration uses default forward duration',
|
||||
(WidgetTester tester) async {
|
||||
Widget buildSnackBar(AnimationStyle snackBarAnimationStyle) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('I am a snack bar.'),
|
||||
showCloseIcon: true,
|
||||
),
|
||||
snackBarAnimationStyle: snackBarAnimationStyle,
|
||||
);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Test custom animation style with only reverseDuration.
|
||||
await tester.pumpWidget(buildSnackBar(AnimationStyle(
|
||||
reverseDuration: const Duration(milliseconds: 400),
|
||||
)));
|
||||
|
||||
// Tap the button to show the SnackBar.
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
// Advance the animation by 1/2 of the default forward duration.
|
||||
await tester.pump(const Duration(milliseconds: 125));
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
// Advance the animation by 1/2 of the default forward duration.
|
||||
await tester.pump(const Duration(milliseconds: 125)); // Advance the animation by 125ms.
|
||||
|
||||
// The SnackBar is fully visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(566, 0.1));
|
||||
|
||||
// Tap the close button to dismiss the SnackBar.
|
||||
await tester.tap(find.byType(IconButton));
|
||||
await tester.pump();
|
||||
// Advance the animation by 1/2 of the reverse duration.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// The SnackBar is partially visible.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(576.7, 0.1));
|
||||
|
||||
// Advance the animation by 1/2 of the reverse duration.
|
||||
await tester.pump(const Duration(milliseconds: 200)); // Advance the animation by 200ms.
|
||||
|
||||
// The SnackBar is dismissed.
|
||||
expect(tester.getTopLeft(find.text('I am a snack bar.')).dy, closeTo(614, 0.1));
|
||||
});
|
||||
}
|
||||
|
||||
class _GeometryListener extends StatefulWidget {
|
||||
|
|
Loading…
Reference in a new issue