From a6f17e697c61e02ee868d0fb4d6d13301e056956 Mon Sep 17 00:00:00 2001 From: Youchen Du Date: Wed, 11 Jan 2023 05:31:17 +0800 Subject: [PATCH] Add option for opting out of enter route snapshotting. (#118086) * Add option for opting out of enter route snapshotting. * Fix typo. * Merge find layers logic. * Add justification comment on why web is skipped in test. * Update documentation as suggested. * Update documentation as suggested. --- .../src/material/page_transitions_theme.dart | 28 ++++++- packages/flutter/test/material/page_test.dart | 83 +++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index ce9415079897..168003077cdc 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -157,6 +157,7 @@ class _ZoomPageTransition extends StatelessWidget { required this.animation, required this.secondaryAnimation, required this.allowSnapshotting, + required this.allowEnterRouteSnapshotting, this.child, }) : assert(animation != null), assert(secondaryAnimation != null); @@ -207,6 +208,15 @@ class _ZoomPageTransition extends StatelessWidget { /// [secondaryAnimation]. final Widget? child; + /// Whether to enable snapshotting on the entering route during the + /// transition animation. + /// + /// If not specified, defaults to true. + /// If false, the route snapshotting will not be applied to the route being + /// animating into, e.g. when transitioning from route A to route B, B will + /// not be snapshotted. + final bool allowEnterRouteSnapshotting; + @override Widget build(BuildContext context) { return DualTransitionBuilder( @@ -218,7 +228,7 @@ class _ZoomPageTransition extends StatelessWidget { ) { return _ZoomEnterTransition( animation: animation, - allowSnapshotting: allowSnapshotting, + allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting, child: child, ); }, @@ -243,7 +253,7 @@ class _ZoomPageTransition extends StatelessWidget { ) { return _ZoomEnterTransition( animation: animation, - allowSnapshotting: allowSnapshotting, + allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting , reverse: true, child: child, ); @@ -596,7 +606,18 @@ class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { class ZoomPageTransitionsBuilder extends PageTransitionsBuilder { /// Constructs a page transition animation that matches the transition used on /// Android Q. - const ZoomPageTransitionsBuilder(); + const ZoomPageTransitionsBuilder({ + this.allowEnterRouteSnapshotting = true, + }); + + /// Whether to enable snapshotting on the entering route during the + /// transition animation. + /// + /// If not specified, defaults to true. + /// If false, the route snapshotting will not be applied to the route being + /// animating into, e.g. when transitioning from route A to route B, B will + /// not be snapshotted. + final bool allowEnterRouteSnapshotting; @override Widget buildTransitions( @@ -610,6 +631,7 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder { animation: animation, secondaryAnimation: secondaryAnimation, allowSnapshotting: route?.allowSnapshotting ?? true, + allowEnterRouteSnapshotting: allowEnterRouteSnapshotting, child: child, ); } diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 0296c25bcddd..87d9d6211c62 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -287,6 +287,89 @@ void main() { } }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. + + testWidgets( + 'test page transition (_ZoomPageTransition) with rasterization disables snapshotting for enter route', + (WidgetTester tester) async { + Iterable findLayers(Finder of) { + return tester.layerListOf( + find.ancestor(of: of, matching: find.byType(SnapshotWidget)).first, + ); + } + + bool isTransitioningWithoutSnapshotting(Finder of) { + // When snapshotting is off, the OpacityLayer and TransformLayer will be + // applied directly. + final Iterable layers = findLayers(of); + return layers.whereType().length == 1 && + layers.whereType().length == 1; + } + + bool isSnapshotted(Finder of) { + final Iterable layers = findLayers(of); + // The scrim and the snapshot image are the only two layers. + return layers.length == 2 && + layers.whereType().length == 1 && + layers.whereType().length == 1; + } + + await tester.pumpWidget( + MaterialApp( + routes: { + '/1': (_) => const Material(child: Text('Page 1')), + '/2': (_) => const Material(child: Text('Page 2')), + }, + initialRoute: '/1', + builder: (BuildContext context, Widget? child) { + final ThemeData themeData = Theme.of(context); + return Theme( + data: themeData.copyWith( + pageTransitionsTheme: PageTransitionsTheme( + builders: { + ...themeData.pageTransitionsTheme.builders, + TargetPlatform.android: const ZoomPageTransitionsBuilder( + allowEnterRouteSnapshotting: false, + ), + }, + ), + ), + child: Builder(builder: (_) => child!), + ); + }, + ), + ); + + final Finder page1Finder = find.text('Page 1'); + final Finder page2Finder = find.text('Page 2'); + + // Page 1 on top. + expect(isSnapshotted(page1Finder), isFalse); + + // Transitioning from page 1 to page 2. + tester.state(find.byType(Navigator)).pushNamed('/2'); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + expect(isSnapshotted(page1Finder), isTrue); + expect(isTransitioningWithoutSnapshotting(page2Finder), isTrue); + + // Page 2 on top. + await tester.pumpAndSettle(); + expect(isSnapshotted(page2Finder), isFalse); + + // Transitioning back from page 2 to page 1. + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + expect(isTransitioningWithoutSnapshotting(page1Finder), isTrue); + expect(isSnapshotted(page2Finder), isTrue); + + // Page 1 on top. + await tester.pumpAndSettle(); + expect(isSnapshotted(page1Finder), isFalse); + }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. + testWidgets('test fullscreen dialog transition', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(