From d01874d71e571b401f9909be1a0aa8aa597b11f3 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 16 Nov 2022 15:05:15 -0800 Subject: [PATCH] [framework] re-rasterize page transition when layout size changes (#115371) * [framework] autoresize on snapshot widget * ++ * ++ * ++ * use layout to resize --- .../src/material/page_transitions_theme.dart | 35 +----------------- .../lib/src/widgets/snapshot_widget.dart | 35 +++++++++++++++++- packages/flutter/test/material/page_test.dart | 37 ++++++++++++++++--- 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index b3ea77b21b..ce94150798 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -291,7 +291,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr bool get useSnapshot => !kIsWeb && widget.allowSnapshotting; late _ZoomEnterTransitionPainter delegate; - MediaQueryData? mediaQueryData; static final Animatable _fadeInTransition = Tween( begin: 0.0, @@ -356,18 +355,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr super.didUpdateWidget(oldWidget); } - @override - void didChangeDependencies() { - // If the screen size changes during the transition, perhaps due to - // a keyboard dismissal, then ensure that contents are re-rasterized once. - final MediaQueryData? data = MediaQuery.maybeOf(context); - if (mediaQueryDataChanged(mediaQueryData, data)) { - controller.clear(); - } - mediaQueryData = data; - super.didChangeDependencies(); - } - @override void dispose() { widget.animation.removeListener(onAnimationValueChange); @@ -382,6 +369,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr painter: delegate, controller: controller, mode: SnapshotMode.permissive, + autoresize: true, child: widget.child, ); } @@ -407,7 +395,6 @@ class _ZoomExitTransition extends StatefulWidget { class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase { late _ZoomExitTransitionPainter delegate; - MediaQueryData? mediaQueryData; // See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't // support this functionality and the canvaskit backend uses a single thread for UI and raster @@ -472,18 +459,6 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran super.didUpdateWidget(oldWidget); } - @override - void didChangeDependencies() { - // If the screen size changes during the transition, perhaps due to - // a keyboard dismissal, then ensure that contents are re-rasterized once. - final MediaQueryData? data = MediaQuery.maybeOf(context); - if (mediaQueryDataChanged(mediaQueryData, data)) { - controller.clear(); - } - mediaQueryData = data; - super.didChangeDependencies(); - } - @override void dispose() { widget.animation.removeListener(onAnimationValueChange); @@ -498,6 +473,7 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran painter: delegate, controller: controller, mode: SnapshotMode.permissive, + autoresize: true, child: widget.child, ); } @@ -830,13 +806,6 @@ mixin _ZoomTransitionBase { break; } } - - // Whether any of the properties that would impact the page transition - // changed. - bool mediaQueryDataChanged(MediaQueryData? oldData, MediaQueryData? newData) { - return oldData?.size != newData?.size || - oldData?.viewInsets != newData?.viewInsets; - } } class _ZoomEnterTransitionPainter extends SnapshotPainter { diff --git a/packages/flutter/lib/src/widgets/snapshot_widget.dart b/packages/flutter/lib/src/widgets/snapshot_widget.dart index d7d6a33b0c..a2d6422013 100644 --- a/packages/flutter/lib/src/widgets/snapshot_widget.dart +++ b/packages/flutter/lib/src/widgets/snapshot_widget.dart @@ -110,6 +110,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { super.key, this.mode = SnapshotMode.normal, this.painter = const _DefaultSnapshotPainter(), + this.autoresize = false, required this.controller, required super.child }); @@ -125,6 +126,12 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { /// See [SnapshotMode] for more information. final SnapshotMode mode; + /// Whether or not changes in render object size should automatically re-create + /// the snapshot. + /// + /// Defaults to false. + final bool autoresize; + /// The painter used to paint the child snapshot or child widgets. final SnapshotPainter painter; @@ -136,6 +143,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { mode: mode, devicePixelRatio: MediaQuery.of(context).devicePixelRatio, painter: painter, + autoresize: autoresize, ); } @@ -146,7 +154,8 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { ..controller = controller ..mode = mode ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio - ..painter = painter; + ..painter = painter + ..autoresize = autoresize; } } @@ -159,10 +168,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { required SnapshotController controller, required SnapshotMode mode, required SnapshotPainter painter, + required bool autoresize, }) : _devicePixelRatio = devicePixelRatio, _controller = controller, _mode = mode, - _painter = painter; + _painter = painter, + _autoresize = autoresize; /// The device pixel ratio used to create the child image. double get devicePixelRatio => _devicePixelRatio; @@ -230,6 +241,17 @@ class _RenderSnapshotWidget extends RenderProxyBox { markNeedsPaint(); } + /// Whether or not changes in render object size should automatically re-rasterize. + bool get autoresize => _autoresize; + bool _autoresize; + set autoresize(bool value) { + if (value == autoresize) { + return; + } + _autoresize = value; + markNeedsPaint(); + } + ui.Image? _childRaster; Size? _childRasterSize; // Set to true if the snapshot mode was not forced and a platform view @@ -292,9 +314,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { } final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio); offsetLayer.dispose(); + _lastCachedSize = size; return image; } + Size? _lastCachedSize; + @override void paint(PaintingContext context, Offset offset) { if (size.isEmpty) { @@ -310,6 +335,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { painter.paint(context, offset, size, super.paint); return; } + + if (autoresize && size != _lastCachedSize && _lastCachedSize != null) { + _childRaster?.dispose(); + _childRaster = null; + } + if (_childRaster == null) { _childRaster = _paintAndDetachToImage(); _childRasterSize = size * devicePixelRatio; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index b97bfd87c7..b0fe989ca4 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. @Tags(['reduced-test-set']) +import 'dart:ui' as ui; + import 'package:flutter/cupertino.dart' show CupertinoPageRoute; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -236,13 +238,17 @@ void main() { expect(find.text('Page 2'), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); - testWidgets('test page transition (_ZoomPageTransition) with rasterization re-rasterizes when window size changes', (WidgetTester tester) async { - // Shrink the window size. + testWidgets('test page transition (_ZoomPageTransition) with rasterization re-rasterizes when window insets', (WidgetTester tester) async { late Size oldSize; + late ui.WindowPadding oldInsets; try { oldSize = tester.binding.window.physicalSize; + oldInsets = tester.binding.window.viewInsets; tester.binding.window.physicalSizeTestValue = const Size(1000, 1000); + tester.binding.window.viewInsetsTestValue = ui.WindowPadding.zero; + // Intentionally use nested scaffolds to simulate the view insets being + // consumed. final Key key = GlobalKey(); await tester.pumpWidget( RepaintBoundary( @@ -251,7 +257,9 @@ void main() { onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { - return const Material(child: SizedBox.shrink()); + return const Scaffold(body: Scaffold( + body: Material(child: SizedBox.shrink()) + )); }, ); }, @@ -265,8 +273,8 @@ void main() { await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.small.png')); - // Increase the window size. - tester.binding.window.physicalSizeTestValue = const Size(1000, 2000); + // Change the view insets + tester.binding.window.viewInsetsTestValue = const TestWindowPadding(left: 0, top: 0, right: 0, bottom: 500); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); @@ -274,6 +282,7 @@ void main() { await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.big.png')); } finally { tester.binding.window.physicalSizeTestValue = oldSize; + tester.binding.window.viewInsetsTestValue = oldInsets; } }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web. @@ -1236,3 +1245,21 @@ class TestDependencies extends StatelessWidget { ); } } + +class TestWindowPadding implements ui.WindowPadding { + const TestWindowPadding({ + required this.left, + required this.top, + required this.right, + required this.bottom, + }); + + @override + final double left; + @override + final double top; + @override + final double right; + @override + final double bottom; +}