diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 73d677ff4cd..4ab01b2d591 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -80,6 +80,8 @@ class MaterialPageRoute extends PageRoute with MaterialRouteTransitionMixi /// * [CupertinoPageTransitionsBuilder], which is the default page transition /// for iOS and macOS. mixin MaterialRouteTransitionMixin on PageRoute { + TargetPlatform? _effectiveTargetPlatform; + /// Builds the primary contents of the route. @protected Widget buildContent(BuildContext context); @@ -116,8 +118,20 @@ mixin MaterialRouteTransitionMixin on PageRoute { @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme; - return theme.buildTransitions(this, context, animation, secondaryAnimation, child); + return ValueListenableBuilder( + valueListenable: navigator!.userGestureInProgressNotifier, + builder: (BuildContext context, bool useGestureInProgress, Widget? _) { + final ThemeData themeData = Theme.of(context); + + if (useGestureInProgress) { + // The platform should be kept unchanged during an user gesture. + _effectiveTargetPlatform ??= themeData.platform; + } else { + _effectiveTargetPlatform = themeData.platform; + } + return themeData.pageTransitionsTheme.buildTransitions(this, context, animation, secondaryAnimation, child, _effectiveTargetPlatform!); + }, + ); } } diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index 7c960a8362f..6cf76570dfb 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -741,7 +741,7 @@ class PageTransitionsTheme with Diagnosticable { Map get builders => _builders; final Map _builders; - /// Delegates to the builder for the current [ThemeData.platform]. + /// Delegates to the builder for the current [platform]. /// If a builder for the current platform is not found, then the /// [ZoomPageTransitionsBuilder] is used. /// @@ -752,13 +752,8 @@ class PageTransitionsTheme with Diagnosticable { Animation animation, Animation secondaryAnimation, Widget child, + TargetPlatform platform, ) { - TargetPlatform platform = Theme.of(context).platform; - - if (CupertinoRouteTransitionMixin.isPopGestureInProgress(route)) { - platform = TargetPlatform.iOS; - } - final PageTransitionsBuilder matchingBuilder = builders[platform] ?? const ZoomPageTransitionsBuilder(); return matchingBuilder.buildTransitions(route, context, animation, secondaryAnimation, child); diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index 323d4ed0c03..e1f933da482 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -350,4 +350,140 @@ void main() { await tester.pumpAndSettle(); expect(builtCount, 1); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + + testWidgets('android can use CupertinoPageTransitionsBuilder', (WidgetTester tester) async { + int builtCount = 0; + + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { Navigator.of(context).pushNamed('/b'); }, + ), + ), + '/b': (BuildContext context) => StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + builtCount++; + return TextButton( + child: const Text('pop'), + onPressed: () { Navigator.pop(context); }, + ); + }, + ), + }; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + // iOS uses different PageTransitionsBuilder + TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(), + }, + ), + ), + routes: routes, + ), + ); + + // No matter push or pop was called, the child widget should built only once. + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + expect(builtCount, 1); + + final Size size = tester.getSize(find.byType(MaterialApp)); + await tester.flingFrom(Offset(0, size.height / 2), Offset(size.width * 2 / 3, 0), 500); + + await tester.pumpAndSettle(); + expect(find.text('push'), findsOneWidget); + expect(builtCount, 1); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + + testWidgets('back gesture while TargetPlatform changes', (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('HELLO'), + }; + const PageTransitionsTheme pageTransitionsTheme = PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + // iOS uses different PageTransitionsBuilder + TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(), + }, + ); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + platform: TargetPlatform.android, + pageTransitionsTheme: pageTransitionsTheme, + ), + routes: routes, + ), + ); + await tester.tap(find.text('PUSH')); + expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); + expect(find.text('PUSH'), findsNothing); + expect(find.text('HELLO'), findsOneWidget); + + final Offset helloPosition1 = tester.getCenter(find.text('HELLO')); + final TestGesture gesture = await tester.startGesture(const Offset(2.5, 300.0)); + await tester.pump(const Duration(milliseconds: 20)); + await gesture.moveBy(const Offset(100.0, 0.0)); + expect(find.text('PUSH'), findsNothing); + expect(find.text('HELLO'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 20)); + expect(find.text('PUSH'), findsOneWidget); + expect(find.text('HELLO'), findsOneWidget); + final Offset helloPosition2 = tester.getCenter(find.text('HELLO')); + expect(helloPosition1.dx, lessThan(helloPosition2.dx)); + expect(helloPosition1.dy, helloPosition2.dy); + expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.android); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + platform: TargetPlatform.iOS, + pageTransitionsTheme: pageTransitionsTheme, + ), + routes: routes, + ), + ); + // Now, let the theme animation run through. + // This takes three frames (including the first one above): + // 1. Start the Theme animation. It's at t=0 so everything else is identical. + // 2. Start any animations that are informed by the Theme, for example, the + // DefaultTextStyle, on the first frame that the theme is not at t=0. In + // this case, it's at t=1.0 of the theme animation, so this is also the + // frame in which the theme animation ends. + // 3. End all the other animations. + expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); + expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.iOS); + final Offset helloPosition3 = tester.getCenter(find.text('HELLO')); + expect(helloPosition3, helloPosition2); + expect(find.text('PUSH'), findsOneWidget); + expect(find.text('HELLO'), findsOneWidget); + await gesture.moveBy(const Offset(100.0, 0.0)); + await tester.pump(const Duration(milliseconds: 20)); + expect(find.text('PUSH'), findsOneWidget); + expect(find.text('HELLO'), findsOneWidget); + final Offset helloPosition4 = tester.getCenter(find.text('HELLO')); + expect(helloPosition3.dx, lessThan(helloPosition4.dx)); + expect(helloPosition3.dy, helloPosition4.dy); + await gesture.moveBy(const Offset(500.0, 0.0)); + await gesture.up(); + expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3); + expect(find.text('PUSH'), findsOneWidget); + expect(find.text('HELLO'), findsNothing); + + await tester.tap(find.text('PUSH')); + expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); + expect(find.text('PUSH'), findsNothing); + expect(find.text('HELLO'), findsOneWidget); + }); }