Skip to content

Commit 92107b1

Browse files
authored
Create new page transition for M3 (#158881)
This PR is to add a new page transition for Material 3. The new builder matches the latest Android page transition behavior; while the new page slides in from right to left, it fades in at the same time and the old page slides out from right to left, fading out at the same time. When both pages are fading in/out, the black background might show up, so I added a `ColoredBox` for the slides-out page whose color defaults to `ColorScheme.surface`. The `backgroundColor` property can be used to customize the default transition color. This demo shows the forward and backward behaviors. https://github.com/user-attachments/assets/a806f25d-8564-4cad-8dfc-eb4585294181 Fixes: flutter/flutter#142352 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
1 parent 8f1d877 commit 92107b1

File tree

8 files changed

+658
-7
lines changed

8 files changed

+658
-7
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class PageTransitionsThemeApp extends StatelessWidget {
1515
Widget build(BuildContext context) {
1616
return MaterialApp(
1717
theme: ThemeData(
18-
useMaterial3: true,
1918
// Defines the page transition animations used by MaterialPageRoute
2019
// for different target platforms.
2120
// Non-specified target platforms will default to
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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 the default Android U page transition theme
8+
/// [FadeForwardsPageTransitionsBuilder]. Tapping each list tile navigates to
9+
/// a second page, which slides in from right to left while fading in.
10+
/// Simultaneously, the first page slides out in the same direction while
11+
/// fading out.
12+
13+
void main() => runApp(const PageTransitionsThemeApp());
14+
15+
class PageTransitionsThemeApp extends StatelessWidget {
16+
const PageTransitionsThemeApp({super.key});
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return MaterialApp(
21+
debugShowCheckedModeBanner: false,
22+
theme: ThemeData(
23+
pageTransitionsTheme: PageTransitionsTheme(
24+
builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
25+
TargetPlatform.values, value: (_) => const FadeForwardsPageTransitionsBuilder()
26+
),
27+
),
28+
),
29+
home: const HomePage(),
30+
);
31+
}
32+
}
33+
34+
class HomePage extends StatelessWidget {
35+
const HomePage({super.key});
36+
37+
@override
38+
Widget build(BuildContext context) {
39+
return Scaffold(
40+
appBar: AppBar(
41+
leading: IconButton(icon: const Icon(Icons.dehaze), onPressed: () {}),
42+
actions: <Widget>[
43+
IconButton(icon: const Icon(Icons.search), onPressed: () {}),
44+
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
45+
],
46+
),
47+
body: Column(
48+
children: <Widget>[
49+
Text('Messages', style: Theme.of(context).textTheme.headlineLarge),
50+
Expanded(
51+
child: Padding(
52+
padding: const EdgeInsets.all(20.0),
53+
child: Card(
54+
clipBehavior: Clip.antiAlias,
55+
elevation: 0,
56+
color: Theme.of(context).colorScheme.surfaceContainerLowest,
57+
child: ListView(
58+
children: List<Widget>.generate(Colors.primaries.length, (int index) {
59+
final Text kittenName = Text('Kitten $index');
60+
final CircleAvatar avatar = CircleAvatar(backgroundColor: Colors.primaries[index]);
61+
final String message = index.isEven
62+
? 'Hello hooman! My name is Kitten $index'
63+
: "What's up hooman! My name is Kitten $index";
64+
return ListTile(
65+
leading: avatar,
66+
title: kittenName,
67+
subtitle: Text(message),
68+
trailing: Text('$index seconds ago'),
69+
onTap: () {
70+
Navigator.of(context).push(
71+
MaterialPageRoute<SecondPage>(
72+
builder: (BuildContext context)
73+
=> SecondPage(
74+
kittenName: kittenName,
75+
avatar: avatar,
76+
message: message,
77+
),
78+
),
79+
);
80+
},
81+
);
82+
}),
83+
),
84+
),
85+
),
86+
),
87+
],
88+
)
89+
);
90+
}
91+
}
92+
93+
class SecondPage extends StatelessWidget {
94+
const SecondPage({
95+
super.key,
96+
required this.kittenName,
97+
required this.avatar,
98+
required this.message,
99+
});
100+
final Text kittenName;
101+
final CircleAvatar avatar;
102+
final String message;
103+
104+
@override
105+
Widget build(BuildContext context) {
106+
return Scaffold(
107+
appBar: AppBar(
108+
leading: const BackButton(),
109+
title: kittenName,
110+
centerTitle: false,
111+
actions: <Widget>[
112+
IconButton(icon: const Icon(Icons.search), onPressed: () {}),
113+
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
114+
],
115+
),
116+
body: Padding(
117+
padding: const EdgeInsets.all(20.0),
118+
child: IntrinsicHeight(
119+
child: Row(
120+
children: <Widget>[
121+
avatar,
122+
ConstrainedBox(
123+
constraints: const BoxConstraints(minHeight: 50),
124+
child: Card(
125+
elevation: 0.0,
126+
shape: const RoundedRectangleBorder(
127+
borderRadius: BorderRadius.only(
128+
topLeft: Radius.circular(20),
129+
topRight: Radius.circular(20),
130+
bottomLeft: Radius.circular(5),
131+
bottomRight: Radius.circular(20),
132+
)
133+
),
134+
color: Theme.of(context).colorScheme.surfaceContainerLowest,
135+
child: Center(
136+
child: Padding(
137+
padding: const EdgeInsets.symmetric(horizontal: 15.0),
138+
child: Text(message)
139+
),
140+
),
141+
),
142+
)
143+
],
144+
),
145+
),
146+
),
147+
);
148+
}
149+
}

examples/api/lib/widgets/heroes/hero.1.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ class HeroApp extends StatelessWidget {
1818

1919
@override
2020
Widget build(BuildContext context) {
21-
return MaterialApp(
22-
theme: ThemeData(useMaterial3: true),
23-
home: const HeroExample(),
21+
return const MaterialApp(
22+
home: HeroExample(),
2423
);
2524
}
2625
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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/page_transitions_theme/page_transitions_theme.3.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('Page transition', (WidgetTester tester) async {
11+
await tester.pumpWidget(
12+
const example.PageTransitionsThemeApp(),
13+
);
14+
15+
final Finder homePage = find.byType(example.HomePage);
16+
expect(homePage, findsOneWidget);
17+
18+
final Finder kitten0 = find.widgetWithText(ListTile, 'Kitten 0');
19+
expect(kitten0, findsOneWidget);
20+
21+
await tester.tap(kitten0);
22+
await tester.pumpAndSettle();
23+
expect(find.widgetWithText(AppBar, 'Kitten 0'), findsOneWidget);
24+
25+
await tester.tap(find.byType(BackButton));
26+
await tester.pumpAndSettle();
27+
28+
expect(find.widgetWithText(ListTile, 'Kitten 0'), findsOneWidget);
29+
});
30+
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,10 +1036,15 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
10361036
}
10371037

10381038
@override
1039-
Duration get transitionDuration => _bottomSheetEnterDuration;
1039+
Duration get transitionDuration => transitionAnimationController?.duration
1040+
?? sheetAnimationStyle?.duration
1041+
?? _bottomSheetEnterDuration;
10401042

10411043
@override
1042-
Duration get reverseTransitionDuration => _bottomSheetExitDuration;
1044+
Duration get reverseTransitionDuration => transitionAnimationController?.reverseDuration
1045+
?? transitionAnimationController?.duration
1046+
?? sheetAnimationStyle?.reverseDuration
1047+
?? _bottomSheetExitDuration;
10431048

10441049
@override
10451050
bool get barrierDismissible => isDismissible;

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,40 @@ mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
8787
Widget buildContent(BuildContext context);
8888

8989
@override
90-
Duration get transitionDuration => const Duration(milliseconds: 300);
90+
Duration get transitionDuration => _getPageTransitionBuilder(navigator!.context)?.transitionDuration
91+
?? const Duration(microseconds: 300);
92+
93+
@override
94+
Duration get reverseTransitionDuration => _getPageTransitionBuilder(navigator!.context)?.reverseTransitionDuration
95+
?? const Duration(microseconds: 300);
96+
97+
PageTransitionsBuilder? _getPageTransitionBuilder(BuildContext context) {
98+
final TargetPlatform platform = Theme.of(context).platform;
99+
final PageTransitionsTheme pageTransitionsTheme = Theme.of(context).pageTransitionsTheme;
100+
return pageTransitionsTheme.builders[platform];
101+
}
102+
103+
// The transitionDuration is used to create the AnimationController which is only
104+
// built once, so when page transition builder is updated and transitionDuration
105+
// has a new value, the AnimationController cannot be updated automatically. So we
106+
// manually update its duration here.
107+
// TODO(quncCccccc): Clean up this override method when controller can be updated as the transitionDuration is changed.
108+
@override
109+
TickerFuture didPush() {
110+
controller?.duration = transitionDuration;
111+
return super.didPush();
112+
}
113+
114+
// The reverseTransitionDuration is used to create the AnimationController
115+
// which is only built once, so when page transition builder is updated and
116+
// reverseTransitionDuration has a new value, the AnimationController cannot
117+
// be updated automatically. So we manually update its reverseDuration here.
118+
// TODO(quncCccccc): Clean up this override method when controller can beupdated as the reverseTransitionDuration is changed.
119+
@override
120+
bool didPop(T? result) {
121+
controller?.reverseDuration = reverseTransitionDuration;
122+
return super.didPop(result);
123+
}
91124

92125
@override
93126
Color? get barrierColor => null;

0 commit comments

Comments
 (0)