Unverified Commit d01874d7 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] re-rasterize page transition when layout size changes (#115371)

* [framework] autoresize on snapshot widget

* ++

* ++

* ++

* use layout to resize
parent b5791092
...@@ -291,7 +291,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -291,7 +291,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
bool get useSnapshot => !kIsWeb && widget.allowSnapshotting; bool get useSnapshot => !kIsWeb && widget.allowSnapshotting;
late _ZoomEnterTransitionPainter delegate; late _ZoomEnterTransitionPainter delegate;
MediaQueryData? mediaQueryData;
static final Animatable<double> _fadeInTransition = Tween<double>( static final Animatable<double> _fadeInTransition = Tween<double>(
begin: 0.0, begin: 0.0,
...@@ -356,18 +355,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -356,18 +355,6 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
super.didUpdateWidget(oldWidget); 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 @override
void dispose() { void dispose() {
widget.animation.removeListener(onAnimationValueChange); widget.animation.removeListener(onAnimationValueChange);
...@@ -382,6 +369,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -382,6 +369,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
painter: delegate, painter: delegate,
controller: controller, controller: controller,
mode: SnapshotMode.permissive, mode: SnapshotMode.permissive,
autoresize: true,
child: widget.child, child: widget.child,
); );
} }
...@@ -407,7 +395,6 @@ class _ZoomExitTransition extends StatefulWidget { ...@@ -407,7 +395,6 @@ class _ZoomExitTransition extends StatefulWidget {
class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase { class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase {
late _ZoomExitTransitionPainter delegate; late _ZoomExitTransitionPainter delegate;
MediaQueryData? mediaQueryData;
// See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't // 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 // 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 ...@@ -472,18 +459,6 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran
super.didUpdateWidget(oldWidget); 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 @override
void dispose() { void dispose() {
widget.animation.removeListener(onAnimationValueChange); widget.animation.removeListener(onAnimationValueChange);
...@@ -498,6 +473,7 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran ...@@ -498,6 +473,7 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran
painter: delegate, painter: delegate,
controller: controller, controller: controller,
mode: SnapshotMode.permissive, mode: SnapshotMode.permissive,
autoresize: true,
child: widget.child, child: widget.child,
); );
} }
...@@ -830,13 +806,6 @@ mixin _ZoomTransitionBase { ...@@ -830,13 +806,6 @@ mixin _ZoomTransitionBase {
break; 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 { class _ZoomEnterTransitionPainter extends SnapshotPainter {
......
...@@ -110,6 +110,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { ...@@ -110,6 +110,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget {
super.key, super.key,
this.mode = SnapshotMode.normal, this.mode = SnapshotMode.normal,
this.painter = const _DefaultSnapshotPainter(), this.painter = const _DefaultSnapshotPainter(),
this.autoresize = false,
required this.controller, required this.controller,
required super.child required super.child
}); });
...@@ -125,6 +126,12 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { ...@@ -125,6 +126,12 @@ class SnapshotWidget extends SingleChildRenderObjectWidget {
/// See [SnapshotMode] for more information. /// See [SnapshotMode] for more information.
final SnapshotMode mode; 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. /// The painter used to paint the child snapshot or child widgets.
final SnapshotPainter painter; final SnapshotPainter painter;
...@@ -136,6 +143,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { ...@@ -136,6 +143,7 @@ class SnapshotWidget extends SingleChildRenderObjectWidget {
mode: mode, mode: mode,
devicePixelRatio: MediaQuery.of(context).devicePixelRatio, devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
painter: painter, painter: painter,
autoresize: autoresize,
); );
} }
...@@ -146,7 +154,8 @@ class SnapshotWidget extends SingleChildRenderObjectWidget { ...@@ -146,7 +154,8 @@ class SnapshotWidget extends SingleChildRenderObjectWidget {
..controller = controller ..controller = controller
..mode = mode ..mode = mode
..devicePixelRatio = MediaQuery.of(context).devicePixelRatio ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
..painter = painter; ..painter = painter
..autoresize = autoresize;
} }
} }
...@@ -159,10 +168,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { ...@@ -159,10 +168,12 @@ class _RenderSnapshotWidget extends RenderProxyBox {
required SnapshotController controller, required SnapshotController controller,
required SnapshotMode mode, required SnapshotMode mode,
required SnapshotPainter painter, required SnapshotPainter painter,
required bool autoresize,
}) : _devicePixelRatio = devicePixelRatio, }) : _devicePixelRatio = devicePixelRatio,
_controller = controller, _controller = controller,
_mode = mode, _mode = mode,
_painter = painter; _painter = painter,
_autoresize = autoresize;
/// The device pixel ratio used to create the child image. /// The device pixel ratio used to create the child image.
double get devicePixelRatio => _devicePixelRatio; double get devicePixelRatio => _devicePixelRatio;
...@@ -230,6 +241,17 @@ class _RenderSnapshotWidget extends RenderProxyBox { ...@@ -230,6 +241,17 @@ class _RenderSnapshotWidget extends RenderProxyBox {
markNeedsPaint(); 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; ui.Image? _childRaster;
Size? _childRasterSize; Size? _childRasterSize;
// Set to true if the snapshot mode was not forced and a platform view // Set to true if the snapshot mode was not forced and a platform view
...@@ -292,9 +314,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { ...@@ -292,9 +314,12 @@ class _RenderSnapshotWidget extends RenderProxyBox {
} }
final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio); final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio);
offsetLayer.dispose(); offsetLayer.dispose();
_lastCachedSize = size;
return image; return image;
} }
Size? _lastCachedSize;
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (size.isEmpty) { if (size.isEmpty) {
...@@ -310,6 +335,12 @@ class _RenderSnapshotWidget extends RenderProxyBox { ...@@ -310,6 +335,12 @@ class _RenderSnapshotWidget extends RenderProxyBox {
painter.paint(context, offset, size, super.paint); painter.paint(context, offset, size, super.paint);
return; return;
} }
if (autoresize && size != _lastCachedSize && _lastCachedSize != null) {
_childRaster?.dispose();
_childRaster = null;
}
if (_childRaster == null) { if (_childRaster == null) {
_childRaster = _paintAndDetachToImage(); _childRaster = _paintAndDetachToImage();
_childRasterSize = size * devicePixelRatio; _childRasterSize = size * devicePixelRatio;
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
@Tags(<String>['reduced-test-set']) @Tags(<String>['reduced-test-set'])
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart' show CupertinoPageRoute; import 'package:flutter/cupertino.dart' show CupertinoPageRoute;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -236,13 +238,17 @@ void main() { ...@@ -236,13 +238,17 @@ void main() {
expect(find.text('Page 2'), findsNothing); expect(find.text('Page 2'), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.android)); }, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('test page transition (_ZoomPageTransition) with rasterization re-rasterizes when window size changes', (WidgetTester tester) async { testWidgets('test page transition (_ZoomPageTransition) with rasterization re-rasterizes when window insets', (WidgetTester tester) async {
// Shrink the window size.
late Size oldSize; late Size oldSize;
late ui.WindowPadding oldInsets;
try { try {
oldSize = tester.binding.window.physicalSize; oldSize = tester.binding.window.physicalSize;
oldInsets = tester.binding.window.viewInsets;
tester.binding.window.physicalSizeTestValue = const Size(1000, 1000); 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(); final Key key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
RepaintBoundary( RepaintBoundary(
...@@ -251,7 +257,9 @@ void main() { ...@@ -251,7 +257,9 @@ void main() {
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (BuildContext context) { 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() { ...@@ -265,8 +273,8 @@ void main() {
await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.small.png')); await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.small.png'));
// Increase the window size. // Change the view insets
tester.binding.window.physicalSizeTestValue = const Size(1000, 2000); tester.binding.window.viewInsetsTestValue = const TestWindowPadding(left: 0, top: 0, right: 0, bottom: 500);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
...@@ -274,6 +282,7 @@ void main() { ...@@ -274,6 +282,7 @@ void main() {
await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.big.png')); await expectLater(find.byKey(key), matchesGoldenFile('zoom_page_transition.big.png'));
} finally { } finally {
tester.binding.window.physicalSizeTestValue = oldSize; 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. }, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended] rasterization is not used on the web.
...@@ -1236,3 +1245,21 @@ class TestDependencies extends StatelessWidget { ...@@ -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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment