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(