Skip to content

Commit 19e284f

Browse files
authored
Introduce AnimationStyle (#137945)
This PR introduces `AnimationStyle`, it is used to override default animation curves and durations in several widgets. fixes [Add the ability to customize MaterialApp theme animation duration](flutter/flutter#78372) fixes [Allow customization of showMenu transition animation curves and duration](flutter/flutter#135638) Here is an example where popup menu curve and transition duration is overriden: ```dart popUpAnimationStyle: AnimationStyle( curve: Easing.emphasizedAccelerate, duration: Durations.medium4, ), ``` Set `AnimationStyle.noAnimation` to disable animation. ```dart return MaterialApp( themeAnimationStyle: AnimationStyle.noAnimation, ```
1 parent c6c451e commit 19e284f

File tree

12 files changed

+652
-34
lines changed

12 files changed

+652
-34
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Flutter code sample for [MaterialApp].
8+
9+
void main() {
10+
runApp(const MaterialAppExample());
11+
}
12+
13+
enum AnimationStyles { defaultStyle, custom, none }
14+
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
15+
(AnimationStyles.defaultStyle, 'Default'),
16+
(AnimationStyles.custom, 'Custom'),
17+
(AnimationStyles.none, 'None'),
18+
];
19+
20+
class MaterialAppExample extends StatefulWidget {
21+
const MaterialAppExample({super.key});
22+
23+
@override
24+
State<MaterialAppExample> createState() => _MaterialAppExampleState();
25+
}
26+
27+
class _MaterialAppExampleState extends State<MaterialAppExample> {
28+
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
29+
AnimationStyle? _animationStyle;
30+
bool isDarkTheme = false;
31+
32+
@override
33+
Widget build(BuildContext context) {
34+
return MaterialApp(
35+
themeAnimationStyle: _animationStyle,
36+
themeMode: isDarkTheme ? ThemeMode.dark : ThemeMode.light,
37+
theme: ThemeData(colorSchemeSeed: Colors.green),
38+
darkTheme: ThemeData(
39+
colorSchemeSeed: Colors.green,
40+
brightness: Brightness.dark,
41+
),
42+
home: Scaffold(
43+
body: Center(
44+
child: Column(
45+
mainAxisAlignment: MainAxisAlignment.center,
46+
children: <Widget>[
47+
SegmentedButton<AnimationStyles>(
48+
selected: _animationStyleSelection,
49+
onSelectionChanged: (Set<AnimationStyles> styles) {
50+
setState(() {
51+
_animationStyleSelection = styles;
52+
switch (styles.first) {
53+
case AnimationStyles.defaultStyle:
54+
_animationStyle = null;
55+
case AnimationStyles.custom:
56+
_animationStyle = AnimationStyle(
57+
curve: Easing.emphasizedAccelerate,
58+
duration: const Duration(seconds: 1),
59+
);
60+
case AnimationStyles.none:
61+
_animationStyle = AnimationStyle.noAnimation;
62+
}
63+
});
64+
},
65+
segments: animationStyleSegments
66+
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
67+
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
68+
})
69+
.toList(),
70+
),
71+
const SizedBox(height: 10),
72+
OutlinedButton.icon(
73+
onPressed: () {
74+
setState(() {
75+
isDarkTheme = !isDarkTheme;
76+
});
77+
},
78+
icon: Icon(isDarkTheme ? Icons.wb_sunny : Icons.nightlight_round),
79+
label: const Text('Switch Theme Mode'),
80+
),
81+
],
82+
),
83+
),
84+
),
85+
);
86+
}
87+
}

examples/api/lib/material/popup_menu/popup_menu.0.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,18 @@ class PopupMenuExample extends StatefulWidget {
3030
}
3131

3232
class _PopupMenuExampleState extends State<PopupMenuExample> {
33-
SampleItem? selectedMenu;
33+
SampleItem? selectedItem;
3434

3535
@override
3636
Widget build(BuildContext context) {
3737
return Scaffold(
3838
appBar: AppBar(title: const Text('PopupMenuButton')),
3939
body: Center(
4040
child: PopupMenuButton<SampleItem>(
41-
initialValue: selectedMenu,
42-
// Callback that sets the selected popup menu item.
41+
initialValue: selectedItem,
4342
onSelected: (SampleItem item) {
4443
setState(() {
45-
selectedMenu = item;
44+
selectedItem = item;
4645
});
4746
},
4847
itemBuilder: (BuildContext context) => <PopupMenuEntry<SampleItem>>[

examples/api/lib/material/popup_menu/popup_menu.1.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,18 @@ class PopupMenuExample extends StatefulWidget {
3131
}
3232

3333
class _PopupMenuExampleState extends State<PopupMenuExample> {
34-
SampleItem? selectedMenu;
34+
SampleItem? selectedItem;
3535

3636
@override
3737
Widget build(BuildContext context) {
3838
return Scaffold(
3939
appBar: AppBar(title: const Text('PopupMenuButton')),
4040
body: Center(
4141
child: PopupMenuButton<SampleItem>(
42-
initialValue: selectedMenu,
43-
// Callback that sets the selected popup menu item.
42+
initialValue: selectedItem,
4443
onSelected: (SampleItem item) {
4544
setState(() {
46-
selectedMenu = item;
45+
selectedItem = item;
4746
});
4847
},
4948
itemBuilder: (BuildContext context) => <PopupMenuEntry<SampleItem>>[
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
/// Flutter code sample for [PopupMenuButton].
8+
9+
void main() => runApp(const PopupMenuApp());
10+
11+
class PopupMenuApp extends StatelessWidget {
12+
const PopupMenuApp({super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return const MaterialApp(
17+
home: PopupMenuExample(),
18+
);
19+
}
20+
}
21+
22+
enum AnimationStyles { defaultStyle, custom, none }
23+
const List<(AnimationStyles, String)> animationStyleSegments = <(AnimationStyles, String)>[
24+
(AnimationStyles.defaultStyle, 'Default'),
25+
(AnimationStyles.custom, 'Custom'),
26+
(AnimationStyles.none, 'None'),
27+
];
28+
29+
enum Menu { preview, share, getLink, remove, download }
30+
31+
class PopupMenuExample extends StatefulWidget {
32+
const PopupMenuExample({super.key});
33+
34+
@override
35+
State<PopupMenuExample> createState() => _PopupMenuExampleState();
36+
}
37+
38+
class _PopupMenuExampleState extends State<PopupMenuExample> {
39+
Set<AnimationStyles> _animationStyleSelection = <AnimationStyles>{AnimationStyles.defaultStyle};
40+
AnimationStyle? _animationStyle;
41+
42+
@override
43+
Widget build(BuildContext context) {
44+
return Scaffold(
45+
body: SafeArea(
46+
child: Padding(
47+
padding: const EdgeInsets.only(top: 50),
48+
child: Align(
49+
alignment: Alignment.topCenter,
50+
child: Column(
51+
mainAxisSize: MainAxisSize.min,
52+
mainAxisAlignment: MainAxisAlignment.center,
53+
children: <Widget>[
54+
SegmentedButton<AnimationStyles>(
55+
selected: _animationStyleSelection,
56+
onSelectionChanged: (Set<AnimationStyles> styles) {
57+
setState(() {
58+
_animationStyleSelection = styles;
59+
switch (styles.first) {
60+
case AnimationStyles.defaultStyle:
61+
_animationStyle = null;
62+
case AnimationStyles.custom:
63+
_animationStyle = AnimationStyle(
64+
curve: Easing.emphasizedDecelerate,
65+
duration: const Duration(seconds: 3),
66+
);
67+
case AnimationStyles.none:
68+
_animationStyle = AnimationStyle.noAnimation;
69+
}
70+
});
71+
},
72+
segments: animationStyleSegments
73+
.map<ButtonSegment<AnimationStyles>>(((AnimationStyles, String) shirt) {
74+
return ButtonSegment<AnimationStyles>(value: shirt.$1, label: Text(shirt.$2));
75+
})
76+
.toList(),
77+
),
78+
const SizedBox(height: 10),
79+
PopupMenuButton<Menu>(
80+
popUpAnimationStyle: _animationStyle,
81+
icon: const Icon(Icons.more_vert),
82+
onSelected: (Menu item) { },
83+
itemBuilder: (BuildContext context) => <PopupMenuEntry<Menu>>[
84+
const PopupMenuItem<Menu>(
85+
value: Menu.preview,
86+
child: ListTile(
87+
leading: Icon(Icons.visibility_outlined),
88+
title: Text('Preview'),
89+
),
90+
),
91+
const PopupMenuItem<Menu>(
92+
value: Menu.share,
93+
child: ListTile(
94+
leading: Icon(Icons.share_outlined),
95+
title: Text('Share'),
96+
),
97+
),
98+
const PopupMenuItem<Menu>(
99+
value: Menu.getLink,
100+
child: ListTile(
101+
leading: Icon(Icons.link_outlined),
102+
title: Text('Get link'),
103+
),
104+
),
105+
const PopupMenuDivider(),
106+
const PopupMenuItem<Menu>(
107+
value: Menu.remove,
108+
child: ListTile(
109+
leading: Icon(Icons.delete_outline),
110+
title: Text('Remove'),
111+
),
112+
),
113+
const PopupMenuItem<Menu>(
114+
value: Menu.download,
115+
child: ListTile(
116+
leading: Icon(Icons.download_outlined),
117+
title: Text('Download'),
118+
),
119+
),
120+
],
121+
),
122+
],
123+
),
124+
),
125+
),
126+
),
127+
);
128+
}
129+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/app/app.0.dart'
7+
as example;
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
void main() {
11+
testWidgets('Theme Animation can be customized using AnimationStyle', (WidgetTester tester) async {
12+
await tester.pumpWidget(
13+
const example.MaterialAppExample(),
14+
);
15+
16+
Material getScaffoldMaterial() {
17+
return tester.widget<Material>(find.descendant(
18+
of: find.byType(Scaffold),
19+
matching: find.byType(Material).first,
20+
));
21+
}
22+
23+
final ThemeData lightTheme = ThemeData(colorSchemeSeed: Colors.green);
24+
final ThemeData darkTheme = ThemeData(
25+
colorSchemeSeed: Colors.green,
26+
brightness: Brightness.dark,
27+
);
28+
29+
// Test the default animation.
30+
expect(getScaffoldMaterial().color, lightTheme.colorScheme.background);
31+
32+
await tester.tap(find.text( 'Switch Theme Mode'));
33+
await tester.pump();
34+
// Advance the animation by half of the default duration.
35+
await tester.pump(const Duration(milliseconds: 100));
36+
37+
// The Scaffold background color is updated.
38+
expect(
39+
getScaffoldMaterial().color,
40+
Color.lerp(lightTheme.colorScheme.background, darkTheme.colorScheme.background, 0.5),
41+
);
42+
43+
await tester.pumpAndSettle();
44+
45+
// The Scaffold background color is now fully dark.
46+
expect(getScaffoldMaterial().color, darkTheme.colorScheme.background);
47+
48+
// Test the custom animation curve and duration.
49+
await tester.tap(find.text('Custom'));
50+
await tester.pumpAndSettle();
51+
52+
await tester.tap(find.text('Switch Theme Mode'));
53+
await tester.pump();
54+
// Advance the animation by half of the custom duration.
55+
await tester.pump(const Duration(milliseconds: 500));
56+
57+
// The Scaffold background color is updated.
58+
expect(getScaffoldMaterial().color, const Color(0xff3c3e3b));
59+
60+
await tester.pumpAndSettle();
61+
62+
// The Scaffold background color is now fully light.
63+
expect(getScaffoldMaterial().color, lightTheme.colorScheme.background);
64+
65+
// Test the no animation style.
66+
await tester.tap(find.text('None'));
67+
await tester.pumpAndSettle();
68+
69+
await tester.tap(find.text('Switch Theme Mode'));
70+
// Advance the animation by only one frame.
71+
await tester.pump();
72+
73+
// The Scaffold background color is updated immediately.
74+
expect(getScaffoldMaterial().color, darkTheme.colorScheme.background);
75+
});
76+
}

0 commit comments

Comments
 (0)