From 92107b15fdce27e9cc8b1ddca54bb61f0fafec0c Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:56:57 -0800 Subject: [PATCH] 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: https://github.com/flutter/flutter/issues/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. --- .../page_transitions_theme.0.dart | 1 - .../page_transitions_theme.3.dart | 149 ++++++++++++ examples/api/lib/widgets/heroes/hero.1.dart | 5 +- .../page_transitions_theme.3_test.dart | 30 +++ .../lib/src/material/bottom_sheet.dart | 9 +- packages/flutter/lib/src/material/page.dart | 35 ++- .../src/material/page_transitions_theme.dart | 225 ++++++++++++++++++ .../material/page_transitions_theme_test.dart | 211 ++++++++++++++++ 8 files changed, 658 insertions(+), 7 deletions(-) create mode 100644 examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart create mode 100644 examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart diff --git a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart b/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart index e541cd898d..dc2dab2eed 100644 --- a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart +++ b/examples/api/lib/material/page_transitions_theme/page_transitions_theme.0.dart @@ -15,7 +15,6 @@ class PageTransitionsThemeApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( - useMaterial3: true, // Defines the page transition animations used by MaterialPageRoute // for different target platforms. // Non-specified target platforms will default to diff --git a/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart b/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart new file mode 100644 index 0000000000..42cb937653 --- /dev/null +++ b/examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart @@ -0,0 +1,149 @@ +// 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 the default Android U page transition theme +/// [FadeForwardsPageTransitionsBuilder]. Tapping each list tile navigates to +/// a second page, which slides in from right to left while fading in. +/// Simultaneously, the first page slides out in the same direction while +/// fading out. + +void main() => runApp(const PageTransitionsThemeApp()); + +class PageTransitionsThemeApp extends StatelessWidget { + const PageTransitionsThemeApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + pageTransitionsTheme: PageTransitionsTheme( + builders: Map.fromIterable( + TargetPlatform.values, value: (_) => const FadeForwardsPageTransitionsBuilder() + ), + ), + ), + home: const HomePage(), + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton(icon: const Icon(Icons.dehaze), onPressed: () {}), + actions: [ + IconButton(icon: const Icon(Icons.search), onPressed: () {}), + IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), + ], + ), + body: Column( + children: [ + Text('Messages', style: Theme.of(context).textTheme.headlineLarge), + Expanded( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Card( + clipBehavior: Clip.antiAlias, + elevation: 0, + color: Theme.of(context).colorScheme.surfaceContainerLowest, + child: ListView( + children: List.generate(Colors.primaries.length, (int index) { + final Text kittenName = Text('Kitten $index'); + final CircleAvatar avatar = CircleAvatar(backgroundColor: Colors.primaries[index]); + final String message = index.isEven + ? 'Hello hooman! My name is Kitten $index' + : "What's up hooman! My name is Kitten $index"; + return ListTile( + leading: avatar, + title: kittenName, + subtitle: Text(message), + trailing: Text('$index seconds ago'), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) + => SecondPage( + kittenName: kittenName, + avatar: avatar, + message: message, + ), + ), + ); + }, + ); + }), + ), + ), + ), + ), + ], + ) + ); + } +} + +class SecondPage extends StatelessWidget { + const SecondPage({ + super.key, + required this.kittenName, + required this.avatar, + required this.message, + }); + final Text kittenName; + final CircleAvatar avatar; + final String message; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: const BackButton(), + title: kittenName, + centerTitle: false, + actions: [ + IconButton(icon: const Icon(Icons.search), onPressed: () {}), + IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}), + ], + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: IntrinsicHeight( + child: Row( + children: [ + avatar, + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 50), + child: Card( + elevation: 0.0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + bottomLeft: Radius.circular(5), + bottomRight: Radius.circular(20), + ) + ), + color: Theme.of(context).colorScheme.surfaceContainerLowest, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + child: Text(message) + ), + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/heroes/hero.1.dart b/examples/api/lib/widgets/heroes/hero.1.dart index 63d6931c38..f71686208c 100644 --- a/examples/api/lib/widgets/heroes/hero.1.dart +++ b/examples/api/lib/widgets/heroes/hero.1.dart @@ -18,9 +18,8 @@ class HeroApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData(useMaterial3: true), - home: const HeroExample(), + return const MaterialApp( + home: HeroExample(), ); } } diff --git a/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart b/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart new file mode 100644 index 0000000000..418dade945 --- /dev/null +++ b/examples/api/test/material/page_transitions_theme/page_transitions_theme.3_test.dart @@ -0,0 +1,30 @@ +// 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/page_transitions_theme/page_transitions_theme.3.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Page transition', (WidgetTester tester) async { + await tester.pumpWidget( + const example.PageTransitionsThemeApp(), + ); + + final Finder homePage = find.byType(example.HomePage); + expect(homePage, findsOneWidget); + + final Finder kitten0 = find.widgetWithText(ListTile, 'Kitten 0'); + expect(kitten0, findsOneWidget); + + await tester.tap(kitten0); + await tester.pumpAndSettle(); + expect(find.widgetWithText(AppBar, 'Kitten 0'), findsOneWidget); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.widgetWithText(ListTile, 'Kitten 0'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index 735551f992..d25883e58b 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -1036,10 +1036,15 @@ class ModalBottomSheetRoute extends PopupRoute { } @override - Duration get transitionDuration => _bottomSheetEnterDuration; + Duration get transitionDuration => transitionAnimationController?.duration + ?? sheetAnimationStyle?.duration + ?? _bottomSheetEnterDuration; @override - Duration get reverseTransitionDuration => _bottomSheetExitDuration; + Duration get reverseTransitionDuration => transitionAnimationController?.reverseDuration + ?? transitionAnimationController?.duration + ?? sheetAnimationStyle?.reverseDuration + ?? _bottomSheetExitDuration; @override bool get barrierDismissible => isDismissible; diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 5a5fe8122b..649c388c87 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -87,7 +87,40 @@ mixin MaterialRouteTransitionMixin on PageRoute { Widget buildContent(BuildContext context); @override - Duration get transitionDuration => const Duration(milliseconds: 300); + Duration get transitionDuration => _getPageTransitionBuilder(navigator!.context)?.transitionDuration + ?? const Duration(microseconds: 300); + + @override + Duration get reverseTransitionDuration => _getPageTransitionBuilder(navigator!.context)?.reverseTransitionDuration + ?? const Duration(microseconds: 300); + + PageTransitionsBuilder? _getPageTransitionBuilder(BuildContext context) { + final TargetPlatform platform = Theme.of(context).platform; + final PageTransitionsTheme pageTransitionsTheme = Theme.of(context).pageTransitionsTheme; + return pageTransitionsTheme.builders[platform]; + } + + // The transitionDuration is used to create the AnimationController which is only + // built once, so when page transition builder is updated and transitionDuration + // has a new value, the AnimationController cannot be updated automatically. So we + // manually update its duration here. + // TODO(quncCccccc): Clean up this override method when controller can be updated as the transitionDuration is changed. + @override + TickerFuture didPush() { + controller?.duration = transitionDuration; + return super.didPush(); + } + + // The reverseTransitionDuration is used to create the AnimationController + // which is only built once, so when page transition builder is updated and + // reverseTransitionDuration has a new value, the AnimationController cannot + // be updated automatically. So we manually update its reverseDuration here. + // TODO(quncCccccc): Clean up this override method when controller can beupdated as the reverseTransitionDuration is changed. + @override + bool didPop(T? result) { + controller?.reverseDuration = reverseTransitionDuration; + return super.didPop(result); + } @override Color? get barrierColor => null; diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index 85f93649ed..c2dc23300a 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -534,6 +534,76 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran } } +// This transition slides a new page in from right to left while fading it in, +// and simultaneously slides the previous page out to the left while fading it out. +// This transition is designed to match the Android U activity transition. +class _FadeForwardsPageTransition extends StatelessWidget { + const _FadeForwardsPageTransition({ + required this.animation, + required this.secondaryAnimation, + this.backgroundColor, + this.child, + }); + + final Animation animation; + + final Animation secondaryAnimation; + + final Color? backgroundColor; + + final Widget? child; + + // The new page slides in from right to left. + static final Animatable _forwardTranslationTween = Tween( + begin: const Offset(0.25, 0.0), + end: Offset.zero, + ).chain(CurveTween(curve: FadeForwardsPageTransitionsBuilder._transitionCurve)); + + // The old page slides back from left to right. + static final Animatable _backwardTranslationTween = Tween( + begin: Offset.zero, + end: const Offset(0.25, 0.0), + ).chain(CurveTween(curve: FadeForwardsPageTransitionsBuilder._transitionCurve)); + + @override + Widget build(BuildContext context) { + return DualTransitionBuilder( + animation: animation, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget? child + ) { + return FadeTransition( + opacity: FadeForwardsPageTransitionsBuilder._fadeInTransition.animate(animation), + child: SlideTransition( + position: _forwardTranslationTween.animate(animation), + child: child, + ), + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget? child + ) { + return FadeTransition( + opacity: FadeForwardsPageTransitionsBuilder._fadeOutTransition.animate(animation), + child: SlideTransition( + position: _backwardTranslationTween.animate(animation), + child: child, + ), + ); + }, + child: FadeForwardsPageTransitionsBuilder._delegatedTransition( + context, + secondaryAnimation, + backgroundColor, + child, + )); + } +} + /// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page /// transition animation. /// @@ -551,6 +621,8 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran /// that's similar to the one provided in Android Q. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. +/// * [FadeForwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android U. abstract class PageTransitionsBuilder { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -561,6 +633,16 @@ abstract class PageTransitionsBuilder { /// {@macro flutter.widgets.delegatedTransition} DelegatedTransitionBuilder? get delegatedTransition => null; + /// {@macro flutter.widgets.TransitionRoute.transitionDuration} + /// + /// Defaults to 300 milliseconds. + Duration get transitionDuration => const Duration(milliseconds: 300); + + /// {@macro flutter.widgets.TransitionRoute.reverseTransitionDuration} + /// + /// Defaults to 300 milliseconds. + Duration get reverseTransitionDuration => transitionDuration; + /// Wraps the child with one or more transition widgets which define how [route] /// arrives on and leaves the screen. /// @@ -625,6 +707,8 @@ class FadeUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { /// transition that matches native iOS page transitions. /// * [PredictiveBackPageTransitionsBuilder], which defines a page /// transition that allows peeking behind the current route on Android. +/// * [FadeForwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android U. class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { /// Constructs a page transition animation that matches the transition used on /// Android P. @@ -646,6 +730,143 @@ class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { } } +/// Used by [PageTransitionsTheme] to define a horizontal [MaterialPageRoute] page +/// transition animation that looks like the default page transition +/// used on Android U. +/// +/// {@tool dartpad} +/// This example shows the default page transition on Android. +/// +/// ** See code in examples/api/lib/material/page_transitions_theme/page_transitions_theme.3.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [FadeUpwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android O. +/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Andoird P. +/// * [ZoomPageTransitionsBuilder], which defines the default page transition +/// that's similar to the one provided in Android Q. +/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page +/// transition that matches native iOS page transitions. +/// * [PredictiveBackPageTransitionsBuilder], which defines a page +/// transition that allows peeking behind the current route on Android. +/// * [FadeForwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android U. +class FadeForwardsPageTransitionsBuilder extends PageTransitionsBuilder { + /// Constructs a page transition animation that matches the transition used on + /// Android U. + const FadeForwardsPageTransitionsBuilder({ + this.backgroundColor, + }); + + /// The background color during transition between two routes. + /// + /// When a new page fades in and the old page fades out, this background color + /// helps avoid a black background between two page. + /// + /// Defaults to [ColorScheme.surface] + final Color? backgroundColor; + + @override + Duration get transitionDuration => const Duration(milliseconds: 800); + + @override + DelegatedTransitionBuilder? get delegatedTransition => (BuildContext context, Animation animation, Animation secondaryAnimation, bool allowSnapshotting, Widget? child) + => _delegatedTransition(context, animation, backgroundColor, child); + + // Used by all of the sliding transition animations. + static const Curve _transitionCurve = Curves.easeInOutCubicEmphasized; + + // The previous page slides from right to left as the current page appears. + static final Animatable _secondaryBackwardTranslationTween = Tween( + begin: Offset.zero, + end: const Offset(-0.25, 0.0), + ).chain(CurveTween(curve: _transitionCurve)); + + // The previous page slides from left to right as the current page disappears. + static final Animatable _secondaryForwardTranslationTween = Tween( + begin: const Offset(-0.25, 0.0), + end: Offset.zero, + ).chain(CurveTween(curve: _transitionCurve)); + + // The fade in transition when the new page appears. + static final Animatable _fadeInTransition = Tween( + begin: 0.0, + end: 1.0, + ).chain(CurveTween(curve: const Interval(0.0, 0.75))); + + // The fade out transition of the old page when the new page appears. + static final Animatable _fadeOutTransition = Tween( + begin: 1.0, + end: 0.0, + ).chain(CurveTween(curve: const Interval(0.0, 0.25))); + + static Widget _delegatedTransition( + BuildContext context, + Animation secondaryAnimation, + Color? backgroundColor, + Widget? child, + ) => DualTransitionBuilder( + animation: ReverseAnimation(secondaryAnimation), + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget? child + ) { + return ColoredBox( + color: animation.isAnimating + ? backgroundColor ?? Theme.of(context).colorScheme.surface + : Colors.transparent, + child: FadeTransition( + opacity: _fadeInTransition.animate(animation), + child: SlideTransition( + position: _secondaryForwardTranslationTween.animate(animation), + child: child, + ), + ), + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget? child + ) { + return ColoredBox( + color: animation.isAnimating + ? backgroundColor ?? Theme.of(context).colorScheme.surface + : Colors.transparent, + child: FadeTransition( + opacity: _fadeOutTransition.animate(animation), + child: SlideTransition( + position: _secondaryBackwardTranslationTween.animate(animation), + child: child, + ), + ), + ); + }, + child: child, + ); + + + @override + Widget buildTransitions( + PageRoute? route, + BuildContext? context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return _FadeForwardsPageTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + backgroundColor: backgroundColor, + child: child, + ); + } +} + /// Used by [PageTransitionsTheme] to define a zooming [MaterialPageRoute] page /// transition animation that looks like the default page transition used on /// Android Q. @@ -660,6 +881,8 @@ class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { /// transition that matches native iOS page transitions. /// * [PredictiveBackPageTransitionsBuilder], which defines a page /// transition that allows peeking behind the current route on Android. +/// * [FadeForwardsPageTransitionsBuilder], which defines a page transition +/// that's similar to the one provided by Android U. class ZoomPageTransitionsBuilder extends PageTransitionsBuilder { /// Constructs a page transition animation that matches the transition used on /// Android Q. @@ -836,6 +1059,8 @@ class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder { /// that's similar to the one provided by Android P. /// * [ZoomPageTransitionsBuilder], which defines the default page transition /// that's similar to the one provided by Android Q. +/// * [FadeForwardsPageTransitionsBuilder], which defines the default page transition +/// that's similar to the one provided by Android U. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. @immutable diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index 2361a327de..3c96def5f7 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -97,6 +97,217 @@ void main() { expect(findZoomPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + testWidgets('Default background color when FadeForwardsPageTransitionBuilder is used', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { Navigator.of(context).pushNamed('/b'); }, + ), + ), + '/b': (BuildContext context) => const Text('page b'), + }; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder() + } + ), + colorScheme: ThemeData().colorScheme.copyWith(surface: Colors.pink) + ), + routes: routes, + ), + ); + + Finder findFadeForwardsPageTransition() { + return find.descendant( + of: find.byType(MaterialApp), + matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition'), + ); + } + expect(findFadeForwardsPageTransition(), findsOneWidget); + + await tester.tap(find.text('push')); + await tester.pump(const Duration(milliseconds: 400)); + + final Finder coloredBoxFinder = find.byType(ColoredBox).last; + expect(coloredBoxFinder, findsOneWidget); + final ColoredBox coloredBox = tester.widget(coloredBoxFinder); + expect(coloredBox.color, Colors.pink); + + await tester.pumpAndSettle(); + expect(find.text('page b'), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + + testWidgets('Override background color in FadeForwardsPageTransitionBuilder', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { Navigator.of(context).pushNamed('/b'); }, + ), + ), + '/b': (BuildContext context) => const Text('page b'), + }; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder( + backgroundColor: Colors.lightGreen, + ) + } + ), + colorScheme: ThemeData().colorScheme.copyWith(surface: Colors.pink) + ), + routes: routes, + ), + ); + + Finder findFadeForwardsPageTransition() { + return find.descendant( + of: find.byType(MaterialApp), + matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition'), + ); + } + expect(findFadeForwardsPageTransition(), findsOneWidget); + + await tester.tap(find.text('push')); + await tester.pump(const Duration(milliseconds: 400)); + + final Finder coloredBoxFinder = find.byType(ColoredBox).last; + expect(coloredBoxFinder, findsOneWidget); + final ColoredBox coloredBox = tester.widget(coloredBoxFinder); + expect(coloredBox.color, Colors.lightGreen); + + await tester.pumpAndSettle(); + expect(find.text('page b'), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + + testWidgets('FadeForwardsPageTransitionBuilder default duration is 800ms', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { Navigator.of(context).pushNamed('/b'); }, + ), + ), + '/b': (BuildContext context) => const Text('page b'), + }; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder() + } + ), + ), + routes: routes, + ), + ); + + Finder findFadeForwardsPageTransition() { + return find.descendant( + of: find.byType(MaterialApp), + matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition'), + ); + } + expect(findFadeForwardsPageTransition(), findsOneWidget); + + await tester.tap(find.text('push')); + await tester.pump(const Duration(milliseconds: 799)); + expect(find.text('page b'), findsNothing); + ColoredBox coloredBox = tester.widget(find.byType(ColoredBox).last); + expect(coloredBox.color, isNot(Colors.transparent)); // Color is not transparent during animation. + + await tester.pump(const Duration(milliseconds: 801)); + expect(find.text('page b'), findsOneWidget); + coloredBox = tester.widget(find.byType(ColoredBox).last); + expect(coloredBox.color, Colors.transparent); // Color is transparent during animation. + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + + testWidgets('Animation duration changes accordingly when page transition builder changes', (WidgetTester tester) async { + Widget buildApp(PageTransitionsBuilder pageTransitionBuilder) { + return MaterialApp( + theme: ThemeData( + pageTransitionsTheme: PageTransitionsTheme( + builders: { + TargetPlatform.android: pageTransitionBuilder, + } + ) + ), + routes: { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { Navigator.of(context).pushNamed('/b'); }, + ), + ), + '/b': (BuildContext context) => Material( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + child: const Text('pop'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const Text('page b'), + ], + ) + ), + }, + ); + } + + await tester.pumpWidget(buildApp(const FadeForwardsPageTransitionsBuilder())); + + Finder findFadeForwardsPageTransition() { + return find.descendant( + of: find.byType(MaterialApp), + matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition'), + ); + } + expect(findFadeForwardsPageTransition(), findsOneWidget); + + await tester.tap(find.text('push')); + await tester.pump(const Duration(milliseconds: 799)); + expect(find.text('page b'), findsNothing); + ColoredBox coloredBox = tester.widget(find.byType(ColoredBox).last); + expect(coloredBox.color, isNot(Colors.transparent)); // The color is not transparent during animation. + + await tester.pump(const Duration(milliseconds: 801)); + expect(find.text('page b'), findsOneWidget); + coloredBox = tester.widget(find.byType(ColoredBox).last); + expect(coloredBox.color, Colors.transparent); // The color is transparent during animation. + + await tester.pumpWidget(buildApp(const FadeUpwardsPageTransitionsBuilder())); + await tester.pumpAndSettle(); + expect(find.descendant( + of: find.byType(MaterialApp), + matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeUpwardsPageTransition'), + ), findsOneWidget); + await tester.tap(find.text('pop')); + await tester.pump(const Duration(milliseconds: 299)); + expect(find.text('page b'), findsOneWidget); + expect(find.byType(ColoredBox), findsNothing); // ColoredBox doesn't exist in FadeUpwardsPageTransition. + + await tester.pump(const Duration(milliseconds: 301)); + expect(find.text('page b'), findsNothing); + expect(find.text('push'), findsOneWidget); // The first page + expect(find.byType(ColoredBox), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + testWidgets('PageTransitionsTheme override builds a _OpenUpwardsPageTransition', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => Material(