Skip to content

Commit 50cde35

Browse files
authored
Expose the duration and curve for theme animation in MaterialApp. (#107383)
1 parent ed65b7e commit 50cde35

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

packages/flutter/lib/src/material/app.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ class MaterialApp extends StatefulWidget {
220220
this.highContrastTheme,
221221
this.highContrastDarkTheme,
222222
this.themeMode = ThemeMode.system,
223+
this.themeAnimationDuration = kThemeAnimationDuration,
224+
this.themeAnimationCurve = Curves.linear,
223225
this.locale,
224226
this.localizationsDelegates,
225227
this.localeListResolutionCallback,
@@ -271,6 +273,8 @@ class MaterialApp extends StatefulWidget {
271273
this.highContrastTheme,
272274
this.highContrastDarkTheme,
273275
this.themeMode = ThemeMode.system,
276+
this.themeAnimationDuration = kThemeAnimationDuration,
277+
this.themeAnimationCurve = Curves.linear,
274278
this.locale,
275279
this.localizationsDelegates,
276280
this.localeListResolutionCallback,
@@ -472,6 +476,30 @@ class MaterialApp extends StatefulWidget {
472476
/// system what kind of theme is being used.
473477
final ThemeMode? themeMode;
474478

479+
/// The duration of animated theme changes.
480+
///
481+
/// When the theme changes (either by the [theme], [darkTheme] or [themeMode]
482+
/// parameters changing) it is animated to the new theme over time.
483+
/// The [themeAnimationDuration] determines how long this animation takes.
484+
///
485+
/// To have the theme change immediately, you can set this to [Duration.zero].
486+
///
487+
/// The default is [kThemeAnimationDuration].
488+
///
489+
/// See also:
490+
/// [themeAnimationCurve], which defines the curve used for the animation.
491+
final Duration themeAnimationDuration;
492+
493+
/// The curve to apply when animating theme changes.
494+
///
495+
/// The default is [Curves.linear].
496+
///
497+
/// This is ignored if [themeAnimationDuration] is [Duration.zero].
498+
///
499+
/// See also:
500+
/// [themeAnimationDuration], which defines how long the animation is.
501+
final Curve themeAnimationCurve;
502+
475503
/// {@macro flutter.widgets.widgetsApp.color}
476504
final Color? color;
477505

@@ -896,6 +924,8 @@ class _MaterialAppState extends State<MaterialApp> {
896924
cursorColor: effectiveCursorColor,
897925
child: AnimatedTheme(
898926
data: theme,
927+
duration: widget.themeAnimationDuration,
928+
curve: widget.themeAnimationCurve,
899929
child: widget.builder != null
900930
? Builder(
901931
builder: (BuildContext context) {

packages/flutter/test/material/app_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,88 @@ void main() {
853853
tester.binding.platformDispatcher.clearPlatformBrightnessTestValue();
854854
});
855855

856+
testWidgets('MaterialApp animates theme changes', (WidgetTester tester) async {
857+
final ThemeData lightTheme = ThemeData.light();
858+
final ThemeData darkTheme = ThemeData.dark();
859+
await tester.pumpWidget(
860+
MaterialApp(
861+
theme: lightTheme,
862+
darkTheme: darkTheme,
863+
themeMode: ThemeMode.light,
864+
home: Builder(
865+
builder: (BuildContext context) {
866+
return const Scaffold();
867+
},
868+
),
869+
),
870+
);
871+
expect(tester.widget<Material>(find.byType(Material)).color, lightTheme.scaffoldBackgroundColor);
872+
873+
// Change to dark theme
874+
await tester.pumpWidget(
875+
MaterialApp(
876+
theme: ThemeData.light(),
877+
darkTheme: ThemeData.dark(),
878+
themeMode: ThemeMode.dark,
879+
home: Builder(
880+
builder: (BuildContext context) {
881+
return const Scaffold();
882+
},
883+
),
884+
),
885+
);
886+
887+
// Wait half kThemeAnimationDuration = 200ms.
888+
await tester.pump(const Duration(milliseconds: 100));
889+
890+
// Default curve is linear so background should be half way between
891+
// the two colors.
892+
final Color halfBGColor = Color.lerp(lightTheme.scaffoldBackgroundColor, darkTheme.scaffoldBackgroundColor, 0.5)!;
893+
expect(tester.widget<Material>(find.byType(Material)).color, halfBGColor);
894+
});
895+
896+
testWidgets('MaterialApp theme animation can be turned off', (WidgetTester tester) async {
897+
final ThemeData lightTheme = ThemeData.light();
898+
final ThemeData darkTheme = ThemeData.dark();
899+
int scaffoldRebuilds = 0;
900+
901+
final Widget scaffold = Builder(
902+
builder: (BuildContext context) {
903+
scaffoldRebuilds++;
904+
// Use Theme.of() to ensure we are building when the theme changes.
905+
return Scaffold(backgroundColor: Theme.of(context).scaffoldBackgroundColor);
906+
},
907+
);
908+
909+
await tester.pumpWidget(
910+
MaterialApp(
911+
theme: lightTheme,
912+
darkTheme: darkTheme,
913+
themeMode: ThemeMode.light,
914+
themeAnimationDuration: Duration.zero,
915+
home: scaffold,
916+
),
917+
);
918+
expect(tester.widget<Material>(find.byType(Material)).color, lightTheme.scaffoldBackgroundColor);
919+
expect(scaffoldRebuilds, 1);
920+
921+
// Change to dark theme
922+
await tester.pumpWidget(
923+
MaterialApp(
924+
theme: ThemeData.light(),
925+
darkTheme: ThemeData.dark(),
926+
themeMode: ThemeMode.dark,
927+
themeAnimationDuration: Duration.zero,
928+
home: scaffold,
929+
),
930+
);
931+
932+
// Wait for any animation to finish.
933+
await tester.pumpAndSettle();
934+
expect(tester.widget<Material>(find.byType(Material)).color, darkTheme.scaffoldBackgroundColor);
935+
expect(scaffoldRebuilds, 2);
936+
});
937+
856938
testWidgets('MaterialApp switches themes when the Window platformBrightness changes.', (WidgetTester tester) async {
857939
// Mock the Window to explicitly report a light platformBrightness.
858940
final TestWidgetsFlutterBinding binding = tester.binding;

0 commit comments

Comments
 (0)