Unverified Commit 9ae37030 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Use toPictureSync for faster zoom page transition (#106621)

parent 9f7033a9
......@@ -342,6 +342,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi
super.settings,
this.maintainState = true,
super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null) {
......@@ -371,6 +372,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi
class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
_PageBasedCupertinoPageRoute({
required CupertinoPage<T> page,
super.preferRasterization = true,
}) : assert(page != null),
super(settings: page) {
assert(opaque);
......@@ -417,6 +419,7 @@ class CupertinoPage<T> extends Page<T> {
this.maintainState = true,
this.title,
this.fullscreenDialog = false,
this.preferRasterization = true,
super.key,
super.name,
super.arguments,
......@@ -437,9 +440,12 @@ class CupertinoPage<T> extends Page<T> {
/// {@macro flutter.widgets.PageRoute.fullscreenDialog}
final bool fullscreenDialog;
/// {@macro flutter.widgets.TransitionRoute.preferRasterization}
final bool preferRasterization;
@override
Route<T> createRoute(BuildContext context) {
return _PageBasedCupertinoPageRoute<T>(page: this);
return _PageBasedCupertinoPageRoute<T>(page: this, preferRasterization: preferRasterization);
}
}
......
......@@ -39,6 +39,7 @@ class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixi
super.settings,
this.maintainState = true,
super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null) {
......@@ -157,6 +158,7 @@ class MaterialPage<T> extends Page<T> {
required this.child,
this.maintainState = true,
this.fullscreenDialog = false,
this.preferRasterization = true,
super.key,
super.name,
super.arguments,
......@@ -174,9 +176,12 @@ class MaterialPage<T> extends Page<T> {
/// {@macro flutter.widgets.PageRoute.fullscreenDialog}
final bool fullscreenDialog;
/// {@macro flutter.widgets.TransitionRoute.preferRasterization}
final bool preferRasterization;
@override
Route<T> createRoute(BuildContext context) {
return _PageBasedMaterialPageRoute<T>(page: this);
return _PageBasedMaterialPageRoute<T>(page: this, preferRasterization: preferRasterization);
}
}
......@@ -187,6 +192,7 @@ class MaterialPage<T> extends Page<T> {
class _PageBasedMaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {
_PageBasedMaterialPageRoute({
required MaterialPage<T> page,
super.preferRasterization,
}) : assert(page != null),
super(settings: page) {
assert(opaque);
......
......@@ -168,6 +168,18 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
bool _debugMutationsLocked = false;
/// Whether or not this layer, or any child layers, can be rasterized with
/// [Scene.toImage] or [Scene.toImageSync].
///
/// If `false`, calling the above methods may yield an image which is
/// incomplete.
///
/// This value may change throughout the lifetime of the object, as the
/// child layers themselves are added or removed.
bool supportsRasterization() {
return true;
}
/// Describes the clip that would be applied to contents of this layer,
/// if any.
Rect? describeClipBounds() => null;
......@@ -875,6 +887,12 @@ class TextureLayer extends Layer {
/// The identity of the backend texture.
final int textureId;
// TODO(jonahwilliams): remove once https://github.com/flutter/flutter/issues/107576 is fixed.
@override
bool supportsRasterization() {
return false;
}
/// When true the texture will not be updated with new frames.
///
/// This is used for resizing embedded Android views: when resizing there
......@@ -925,6 +943,11 @@ class PlatformViewLayer extends Layer {
/// A UIView with this identifier must have been created by [PlatformViewsService.initUiKitView].
final int viewId;
@override
bool supportsRasterization() {
return false;
}
@override
void addToScene(ui.SceneBuilder builder) {
builder.addPlatformView(
......@@ -1043,6 +1066,16 @@ class ContainerLayer extends Layer {
/// Returns whether this layer has at least one child layer.
bool get hasChildren => _firstChild != null;
@override
bool supportsRasterization() {
for (Layer? child = lastChild; child != null; child = child.previousSibling) {
if (!child.supportsRasterization()) {
return false;
}
}
return true;
}
/// Consider this layer as the root and build a scene (a tree of layers)
/// in the engine.
// The reason this method is in the `ContainerLayer` class rather than
......@@ -1385,6 +1418,16 @@ class OffsetLayer extends ContainerLayer {
properties.add(DiagnosticsProperty<Offset>('offset', offset));
}
ui.Scene _createSceneForImage(Rect bounds, { double pixelRatio = 1.0 }) {
assert(bounds != null);
assert(pixelRatio != null);
final ui.SceneBuilder builder = ui.SceneBuilder();
final Matrix4 transform = Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1);
transform.translate(-(bounds.left + offset.dx), -(bounds.top + offset.dy));
builder.pushTransform(transform.storage);
return buildScene(builder);
}
/// Capture an image of the current state of this layer and its children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
......@@ -1397,22 +1440,15 @@ class OffsetLayer extends ContainerLayer {
/// (the default) will give you a 1:1 mapping between logical pixels and the
/// output pixels in the image.
///
/// This API functions like [toImageSync], except that it only returns after
/// rasterization is complete.
///
/// See also:
///
/// * [RenderRepaintBoundary.toImage] for a similar API at the render object level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
Future<ui.Image> toImage(Rect bounds, { double pixelRatio = 1.0 }) async {
assert(bounds != null);
assert(pixelRatio != null);
final ui.SceneBuilder builder = ui.SceneBuilder();
final Matrix4 transform = Matrix4.translationValues(
(-bounds.left - offset.dx) * pixelRatio,
(-bounds.top - offset.dy) * pixelRatio,
0.0,
);
transform.scale(pixelRatio, pixelRatio);
builder.pushTransform(transform.storage);
final ui.Scene scene = buildScene(builder);
final ui.Scene scene = _createSceneForImage(bounds, pixelRatio: pixelRatio);
try {
// Size is rounded up to the next pixel to make sure we don't clip off
......@@ -1425,6 +1461,40 @@ class OffsetLayer extends ContainerLayer {
scene.dispose();
}
}
/// Capture an image of the current state of this layer and its children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
/// by the top-left corner of [bounds], and have dimensions equal to the size
/// of [bounds] multiplied by [pixelRatio].
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [dart:ui.FlutterView.devicePixelRatio] for the device, so specifying 1.0
/// (the default) will give you a 1:1 mapping between logical pixels and the
/// output pixels in the image.
///
/// This API functions like [toImage], except that rasterization begins eagerly
/// on the raster thread and the image is returned before this is completed.
///
/// See also:
///
/// * [RenderRepaintBoundary.toImage] for a similar API at the render object level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
ui.Image toImageSync(Rect bounds, { double pixelRatio = 1.0 }) {
final ui.Scene scene = _createSceneForImage(bounds, pixelRatio: pixelRatio);
try {
// Size is rounded up to the next pixel to make sure we don't clip off
// anything.
return scene.toImageSync(
(pixelRatio * bounds.width).ceil(),
(pixelRatio * bounds.height).ceil(),
);
} finally {
scene.dispose();
}
}
}
/// A composite layer that clips its children using a rectangle.
......
......@@ -12,6 +12,7 @@ abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({
super.settings,
this.fullscreenDialog = false,
this.preferRasterization = true,
});
/// {@template flutter.widgets.PageRoute.fullscreenDialog}
......@@ -24,6 +25,9 @@ abstract class PageRoute<T> extends ModalRoute<T> {
/// {@endtemplate}
final bool fullscreenDialog;
@override
final bool preferRasterization;
@override
bool get opaque => true;
......@@ -62,6 +66,7 @@ class PageRouteBuilder<T> extends PageRoute<T> {
this.barrierLabel,
this.maintainState = true,
super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(pageBuilder != null),
assert(transitionsBuilder != null),
assert(opaque != null),
......
This diff is collapsed.
......@@ -126,6 +126,21 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// {@endtemplate}
bool get opaque;
/// {@template flutter.widgets.TransitionRoute.preferRasterization}
/// Whether the route transition will prefer to animate a rasterized
/// snapshot of the entering/exiting routes.
///
/// When this value is true, certain route transitions (such as the Android
/// zoom page transition) will rasterize the entering and exiting routes.
/// These textures are then animated in place of the underlying widgets to
/// improve performance of the transition.
///
/// Generally this means that animations that occur on the entering/exiting
/// route while the route animation plays may appear frozen - unless they
/// are a hero animation or something that is drawn in a separate overlay.
/// {@endtemplate}
bool get preferRasterization => true;
// This ensures that if we got to the dismissed state while still current,
// we will still be disposed when we are eventually popped.
//
......@@ -1719,6 +1734,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
@override
bool get maintainState => true;
@override
bool get preferRasterization => false;
}
/// A [Navigator] observer that notifies [RouteAware]s of changes to the
......
......@@ -89,6 +89,7 @@ export 'src/widgets/platform_menu_bar.dart';
export 'src/widgets/platform_view.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raster_widget.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/reorderable_list.dart';
export 'src/widgets/restoration.dart';
......
......@@ -155,32 +155,33 @@ void main() {
expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('test page transition (_ZoomPageTransition)', (WidgetTester tester) async {
Iterable<T> findWidgets<T extends Widget>(Finder of) {
return tester.widgetList<T>(
find.ancestor(of: of, matching: find.byType(T)),
testWidgets('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async {
Iterable<Layer> findLayers(Finder of) {
return tester.layerListOf(
find.ancestor(of: of, matching: find.byType(RasterWidget)).first,
);
}
FadeTransition findForwardFadeTransition(Finder of) {
return findWidgets<FadeTransition>(of).where(
(FadeTransition t) => t.opacity.status == AnimationStatus.forward,
).first;
OpacityLayer findForwardFadeTransition(Finder of) {
return findLayers(of).whereType<OpacityLayer>().first;
}
ScaleTransition findForwardScaleTransition(Finder of) {
return findWidgets<ScaleTransition>(of).where(
(ScaleTransition t) => t.scale.status == AnimationStatus.forward,
).first;
TransformLayer findForwardScaleTransition(Finder of) {
return findLayers(of).whereType<TransformLayer>().first;
}
await tester.pumpWidget(
MaterialApp(
home: const Material(child: Text('Page 1')),
routes: <String, WidgetBuilder>{
'/next': (BuildContext context) {
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
preferRasterization: false,
builder: (BuildContext context) {
if (settings.name == '/') {
return const Material(child: Text('Page 1'));
}
return const Material(child: Text('Page 2'));
},
);
},
),
);
......@@ -189,16 +190,20 @@ void main() {
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
ScaleTransition widget1Scale = findForwardScaleTransition(find.text('Page 1'));
ScaleTransition widget2Scale = findForwardScaleTransition(find.text('Page 2'));
FadeTransition widget2Opacity = findForwardFadeTransition(find.text('Page 2'));
TransformLayer widget1Scale = findForwardScaleTransition(find.text('Page 1'));
TransformLayer widget2Scale = findForwardScaleTransition(find.text('Page 2'));
OpacityLayer widget2Opacity = findForwardFadeTransition(find.text('Page 2'));
double getScale(TransformLayer layer) {
return layer.transform!.storage[0];
}
// Page 1 is enlarging, starts from 1.0.
expect(widget1Scale.scale.value, greaterThan(1.0));
expect(getScale(widget1Scale), greaterThan(1.0));
// Page 2 is enlarging from the value less than 1.0.
expect(widget2Scale.scale.value, lessThan(1.0));
expect(getScale(widget2Scale), lessThan(1.0));
// Page 2 is becoming none transparent.
expect(widget2Opacity.opacity.value, lessThan(1.0));
expect(widget2Opacity.alpha, lessThan(255));
await tester.pump(const Duration(milliseconds: 250));
await tester.pump(const Duration(milliseconds: 1));
......@@ -216,11 +221,11 @@ void main() {
widget2Opacity = findForwardFadeTransition(find.text('Page 2'));
// Page 1 is narrowing down, but still larger than 1.0.
expect(widget1Scale.scale.value, greaterThan(1.0));
expect(getScale(widget1Scale), greaterThan(1.0));
// Page 2 is smaller than 1.0.
expect(widget2Scale.scale.value, lessThan(1.0));
expect(getScale(widget2Scale), lessThan(1.0));
// Page 2 is becoming transparent.
expect(widget2Opacity.opacity.value, lessThan(1.0));
expect(widget2Opacity.alpha, lessThan(255));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 1));
......
......@@ -1679,7 +1679,7 @@ void main() {
// Wait for context menu to be built.
await tester.pumpAndSettle();
final RenderBox container = tester.renderObject(find.descendant(
of: find.byType(FadeTransition),
of: find.byType(RasterWidget),
matching: find.byType(SizedBox),
).first);
expect(container.size, Size.zero);
......
......@@ -553,6 +553,24 @@ void main() {
parent.buildScene(SceneBuilder());
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857
test('ContainerLayer.toImageSync can render interior layer', () {
final OffsetLayer parent = OffsetLayer();
final OffsetLayer child = OffsetLayer();
final OffsetLayer grandChild = OffsetLayer();
child.append(grandChild);
parent.append(child);
// This renders the layers and generates engine layers.
parent.buildScene(SceneBuilder());
// Causes grandChild to pass its engine layer as `oldLayer`
grandChild.toImageSync(const Rect.fromLTRB(0, 0, 10, 10));
// Ensure we can render the same scene again after rendering an interior
// layer.
parent.buildScene(SceneBuilder());
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857
test('PictureLayer does not let you call dispose unless refcount is 0', () {
PictureLayer layer = PictureLayer(Rect.zero);
expect(layer.debugHandleCount, 0);
......@@ -980,6 +998,35 @@ void main() {
root.dispose();
expect(() => callback(), returnsNormally);
});
test('Layer types that support rasterization', () {
// Supported.
final OffsetLayer offsetLayer = OffsetLayer();
final OpacityLayer opacityLayer = OpacityLayer();
final ClipRectLayer clipRectLayer = ClipRectLayer();
final ClipRRectLayer clipRRectLayer = ClipRRectLayer();
final ImageFilterLayer imageFilterLayer = ImageFilterLayer();
final BackdropFilterLayer backdropFilterLayer = BackdropFilterLayer();
final PhysicalModelLayer physicalModelLayer = PhysicalModelLayer();
final ColorFilterLayer colorFilterLayer = ColorFilterLayer();
final ShaderMaskLayer shaderMaskLayer = ShaderMaskLayer();
expect(offsetLayer.supportsRasterization(), true);
expect(opacityLayer.supportsRasterization(), true);
expect(clipRectLayer.supportsRasterization(), true);
expect(clipRRectLayer.supportsRasterization(), true);
expect(imageFilterLayer.supportsRasterization(), true);
expect(backdropFilterLayer.supportsRasterization(), true);
expect(physicalModelLayer.supportsRasterization(), true);
expect(colorFilterLayer.supportsRasterization(), true);
expect(shaderMaskLayer.supportsRasterization(), true);
// Unsupported.
final TextureLayer textureLayer = TextureLayer(rect: Rect.zero, textureId: 1);
final PlatformViewLayer platformViewLayer = PlatformViewLayer(rect: Rect.zero, viewId: 1);
expect(textureLayer.supportsRasterization(), false);
expect(platformViewLayer.supportsRasterization(), false);
});
}
class FakeEngineLayer extends Fake implements EngineLayer {
......
......@@ -4089,7 +4089,7 @@ class ZeroDurationPage extends Page<void> {
class ZeroDurationPageRoute extends PageRoute<void> {
ZeroDurationPageRoute({required ZeroDurationPage page})
: super(settings: page);
: super(settings: page, preferRasterization: false);
@override
Duration get transitionDuration => Duration.zero;
......
This diff is collapsed.
......@@ -92,6 +92,21 @@ abstract class WidgetController {
});
}
/// Find all layers that are children of the provided [finder].
///
/// The [finder] must match exactly one element.
Iterable<Layer> layerListOf(Finder finder) {
TestAsyncUtils.guardSync();
final Element element = finder.evaluate().single;
final RenderObject object = element.renderObject!;
RenderObject current = object;
while (current.debugLayer == null) {
current = current.parent! as RenderObject;
}
final ContainerLayer layer = current.debugLayer!;
return _walkLayers(layer);
}
/// All elements currently in the widget tree (lazy pre-order traversal).
///
/// The returned iterable is lazy. It does not walk the entire widget tree
......
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