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);
......
......@@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'colors.dart';
import 'theme.dart';
......@@ -153,6 +156,7 @@ class _ZoomPageTransition extends StatelessWidget {
const _ZoomPageTransition({
required this.animation,
required this.secondaryAnimation,
required this.preferRasterization,
this.child,
}) : assert(animation != null),
assert(secondaryAnimation != null);
......@@ -190,6 +194,14 @@ class _ZoomPageTransition extends StatelessWidget {
/// property when the [_ZoomPageTransition] is used as a page transition.
final Animation<double> secondaryAnimation;
/// Whether the [RasterWidget] based-rasterized strategy for the zoom page transition
/// will be used.
///
/// Notably, this improves performance by disabling animations on both the outgoing and
/// incoming route. This also implies that ink-splashes or similar animations will
/// not animate during the transition.
final bool preferRasterization;
/// The widget below this widget in the tree.
///
/// This widget will transition in and out as driven by [animation] and
......@@ -207,6 +219,7 @@ class _ZoomPageTransition extends StatelessWidget {
) {
return _ZoomEnterTransition(
animation: animation,
preferRasterization: preferRasterization,
child: child,
);
},
......@@ -217,6 +230,7 @@ class _ZoomPageTransition extends StatelessWidget {
) {
return _ZoomExitTransition(
animation: animation,
preferRasterization: preferRasterization,
reverse: true,
child: child,
);
......@@ -230,6 +244,7 @@ class _ZoomPageTransition extends StatelessWidget {
) {
return _ZoomEnterTransition(
animation: animation,
preferRasterization: preferRasterization,
reverse: true,
child: child,
);
......@@ -241,6 +256,7 @@ class _ZoomPageTransition extends StatelessWidget {
) {
return _ZoomExitTransition(
animation: animation,
preferRasterization: preferRasterization,
child: child,
);
},
......@@ -250,18 +266,30 @@ class _ZoomPageTransition extends StatelessWidget {
}
}
class _ZoomEnterTransition extends StatelessWidget {
class _ZoomEnterTransition extends StatefulWidget {
const _ZoomEnterTransition({
required this.animation,
this.reverse = false,
required this.preferRasterization,
this.child,
}) : assert(animation != null),
assert(reverse != null);
final Animation<double> animation;
final Widget? child;
final bool preferRasterization;
final bool reverse;
@override
State<_ZoomEnterTransition> createState() => _ZoomEnterTransitionState();
}
class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTransitionBase {
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
bool get allowRasterization => !kIsWeb && widget.preferRasterization;
late _ZoomEnterTransitionDelegate delegate;
static final Animatable<double> _fadeInTransition = Tween<double>(
begin: 0.0,
end: 1.00,
......@@ -282,60 +310,93 @@ class _ZoomEnterTransition extends StatelessWidget {
end: 0.60,
).chain(CurveTween(curve: const Interval(0.2075, 0.4175)));
@override
Widget build(BuildContext context) {
double opacity = 0;
// The transition's scrim opacity only increases on the forward transition.
// In the reverse transition, the opacity should always be 0.0.
//
// Therefore, we need to only apply the scrim opacity animation when
// the transition is running forwards.
//
// The reason that we check that the animation's status is not `completed`
// instead of checking that it is `forward` is that this allows
// the interrupted reversal of the forward transition to smoothly fade
// the scrim away. This prevents a disjointed removal of the scrim.
if (!reverse && animation.status != AnimationStatus.completed) {
opacity = _scrimOpacityTween.evaluate(animation)!;
}
final Animation<double> fadeTransition = reverse
void _updateAnimations() {
fadeTransition = widget.reverse
? kAlwaysCompleteAnimation
: _fadeInTransition.animate(animation);
: _fadeInTransition.animate(widget.animation);
final Animation<double> scaleTransition = (reverse
scaleTransition = (widget.reverse
? _scaleDownTransition
: _scaleUpTransition
).animate(animation);
).animate(widget.animation);
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return ColoredBox(
color: Colors.black.withOpacity(opacity),
child: child,
widget.animation.addListener(onAnimationValueChange);
widget.animation.addStatusListener(onAnimationStatusChange);
}
@override
void initState() {
_updateAnimations();
delegate = _ZoomEnterTransitionDelegate(
reverse: widget.reverse,
fade: fadeTransition,
scale: scaleTransition,
animation: widget.animation,
);
},
child: FadeTransition(
opacity: fadeTransition,
child: ScaleTransition(scale: scaleTransition, child: child),
),
super.initState();
}
@override
void didUpdateWidget(covariant _ZoomEnterTransition oldWidget) {
if (oldWidget.reverse != widget.reverse || oldWidget.animation != widget.animation) {
oldWidget.animation.removeListener(onAnimationValueChange);
oldWidget.animation.removeStatusListener(onAnimationStatusChange);
_updateAnimations();
delegate.dispose();
delegate = _ZoomEnterTransitionDelegate(
reverse: widget.reverse,
fade: fadeTransition,
scale: scaleTransition,
animation: widget.animation,
);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.animation.removeListener(onAnimationValueChange);
widget.animation.removeStatusListener(onAnimationStatusChange);
delegate.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RasterWidget(
delegate: delegate,
controller: controller,
fallback: delegate,
mode: allowRasterization ? RasterizeMode.enabled : RasterizeMode.fallback,
child: widget.child,
);
}
}
class _ZoomExitTransition extends StatelessWidget {
class _ZoomExitTransition extends StatefulWidget {
const _ZoomExitTransition({
required this.animation,
this.reverse = false,
required this.preferRasterization,
this.child,
}) : assert(animation != null),
assert(reverse != null);
final Animation<double> animation;
final bool preferRasterization;
final bool reverse;
final Widget? child;
@override
State<_ZoomExitTransition> createState() => _ZoomExitTransitionState();
}
class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase {
late _ZoomExitTransitionDelegate delegate;
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
bool get allowRasterization => !kIsWeb && widget.preferRasterization;
static final Animatable<double> _fadeOutTransition = Tween<double>(
begin: 1.0,
end: 0.0,
......@@ -351,19 +412,62 @@ class _ZoomExitTransition extends StatelessWidget {
end: 0.90,
).chain(_ZoomPageTransition._scaleCurveSequence);
@override
Widget build(BuildContext context) {
final Animation<double> fadeTransition = reverse
? _fadeOutTransition.animate(animation)
void _updateAnimations() {
fadeTransition = widget.reverse
? _fadeOutTransition.animate(widget.animation)
: kAlwaysCompleteAnimation;
final Animation<double> scaleTransition = (reverse
scaleTransition = (widget.reverse
? _scaleDownTransition
: _scaleUpTransition
).animate(animation);
).animate(widget.animation);
widget.animation.addListener(onAnimationValueChange);
widget.animation.addStatusListener(onAnimationStatusChange);
}
@override
void initState() {
_updateAnimations();
delegate = _ZoomExitTransitionDelegate(
reverse: widget.reverse,
fade: fadeTransition,
scale: scaleTransition,
);
super.initState();
}
@override
void didUpdateWidget(covariant _ZoomExitTransition oldWidget) {
if (oldWidget.reverse != widget.reverse || oldWidget.animation != widget.animation) {
oldWidget.animation.removeListener(onAnimationValueChange);
oldWidget.animation.removeStatusListener(onAnimationStatusChange);
_updateAnimations();
delegate.dispose();
delegate = _ZoomExitTransitionDelegate(
reverse: widget.reverse,
fade: fadeTransition,
scale: scaleTransition,
);
}
super.didUpdateWidget(oldWidget);
}
return FadeTransition(
opacity: fadeTransition,
child: ScaleTransition(scale: scaleTransition, child: child),
@override
void dispose() {
widget.animation.removeListener(onAnimationValueChange);
widget.animation.removeStatusListener(onAnimationStatusChange);
delegate.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RasterWidget(
delegate: delegate,
controller: controller,
fallback: delegate,
mode: allowRasterization ? RasterizeMode.enabled : RasterizeMode.fallback,
child: widget.child,
);
}
}
......@@ -498,6 +602,7 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
return _ZoomPageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
preferRasterization: route?.preferRasterization ?? true,
child: child,
);
}
......@@ -629,3 +734,191 @@ class PageTransitionsTheme with Diagnosticable {
);
}
}
// Take an image and draw it centered and scaled. The image is already scaled by the [pixelRatio].
void _drawImageScaledAndCentered(PaintingContext context, ui.Image image, double scale, double opacity, double pixelRatio) {
if (scale <= 0.0 || opacity <= 0.0) {
return;
}
final Paint paint = Paint()
..filterQuality = ui.FilterQuality.low
..color = Color.fromRGBO(0, 0, 0, opacity);
final double logicalWidth = image.width / pixelRatio;
final double logicalHeight = image.height / pixelRatio;
final double scaledLogicalWidth = logicalWidth * scale;
final double scaledLogicalHeight = logicalHeight * scale;
final double left = (logicalWidth - scaledLogicalWidth) / 2;
final double top = (logicalHeight - scaledLogicalHeight) / 2;
final Rect dst = Rect.fromLTWH(left, top, scaledLogicalWidth, scaledLogicalHeight);
context.canvas.drawImageRect(image, Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), dst, paint);
}
void _updateScaledTransform(Matrix4 transform, double scale, Size size) {
transform.setIdentity();
if (scale == 1.0) {
return;
}
transform.scale(scale, scale);
final double dx = ((size.width * scale) - size.width) / 2;
final double dy = ((size.height * scale) - size.height) / 2;
transform.translate(-dx, -dy);
}
mixin _ZoomTransitionBase {
// Don't rasterize if:
// 1. Rasterization is disabled by the platform.
// 2. The animation is paused/stopped.
// 3. The values of the scale/fade transition do not
// benefit from rasterization.
final RasterWidgetController controller = RasterWidgetController();
late Animation<double> fadeTransition;
late Animation<double> scaleTransition;
void onAnimationValueChange() {
if ((scaleTransition.value == 1.0) &&
(fadeTransition.value == 0.0 ||
fadeTransition.value == 1.0)) {
controller.rasterize = false;
} else {
controller.rasterize = true;
}
}
void onAnimationStatusChange(AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
case AnimationStatus.completed:
controller.rasterize = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
controller.rasterize = true;
break;
}
}
}
class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements RasterWidgetFallbackDelegate {
_ZoomEnterTransitionDelegate({
required this.reverse,
required this.scale,
required this.fade,
required this.animation,
}) {
animation.addListener(notifyListeners);
scale.addListener(notifyListeners);
fade.addListener(notifyListeners);
}
final bool reverse;
final Animation<double> animation;
final Animation<double> scale;
final Animation<double> fade;
final Matrix4 _transform = Matrix4.zero();
final LayerHandle<OpacityLayer> _opacityHandle = LayerHandle<OpacityLayer>();
final LayerHandle<TransformLayer> _transformHandler = LayerHandle<TransformLayer>();
void _drawScrim(PaintingContext context, Offset offset, Size size) {
double scrimOpacity = 0.0;
// The transition's scrim opacity only increases on the forward transition.
// In the reverse transition, the opacity should always be 0.0.
//
// Therefore, we need to only apply the scrim opacity animation when
// the transition is running forwards.
//
// The reason that we check that the animation's status is not `completed`
// instead of checking that it is `forward` is that this allows
// the interrupted reversal of the forward transition to smoothly fade
// the scrim away. This prevents a disjointed removal of the scrim.
if (!reverse && animation.status != AnimationStatus.completed) {
scrimOpacity = _ZoomEnterTransitionState._scrimOpacityTween.evaluate(animation)!;
}
assert(!reverse || scrimOpacity == 0.0);
if (scrimOpacity > 0.0) {
context.canvas.drawRect(
offset & size,
Paint()..color = Colors.black.withOpacity(scrimOpacity),
);
}
}
@override
void paintFallback(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
_drawScrim(context, offset, size);
_updateScaledTransform(_transform, scale.value, size);
_transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) {
_opacityHandle.layer = context.pushOpacity(offset, (fade.value * 255).round(), painter, oldLayer: _opacityHandle.layer);
}, oldLayer: _transformHandler.layer);
}
@override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
_drawScrim(context, offset, size);
_drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio);
}
@override
void dispose() {
animation.removeListener(notifyListeners);
scale.removeListener(notifyListeners);
fade.removeListener(notifyListeners);
_opacityHandle.layer = null;
_transformHandler.layer = null;
super.dispose();
}
@override
bool shouldRepaint(covariant _ZoomEnterTransitionDelegate oldDelegate) {
return oldDelegate.reverse != reverse
|| oldDelegate.animation.value != animation.value
|| oldDelegate.scale.value != scale.value
|| oldDelegate.fade.value != fade.value;
}
}
class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements RasterWidgetFallbackDelegate {
_ZoomExitTransitionDelegate({
required this.reverse,
required this.scale,
required this.fade,
}) {
scale.addListener(notifyListeners);
fade.addListener(notifyListeners);
}
final bool reverse;
final Animation<double> scale;
final Animation<double> fade;
final Matrix4 _transform = Matrix4.zero();
final LayerHandle<OpacityLayer> _opacityHandle = LayerHandle<OpacityLayer>();
final LayerHandle<TransformLayer> _transformHandler = LayerHandle<TransformLayer>();
@override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
_drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio);
}
@override
void paintFallback(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
_updateScaledTransform(_transform, scale.value, size);
_transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) {
_opacityHandle.layer = context.pushOpacity(offset, (fade.value * 255).round(), painter, oldLayer: _opacityHandle.layer);
}, oldLayer: _transformHandler.layer);
}
@override
bool shouldRepaint(covariant _ZoomExitTransitionDelegate oldDelegate) {
return oldDelegate.reverse != reverse || oldDelegate.fade.value != fade.value || oldDelegate.scale.value != scale.value;
}
@override
void dispose() {
_opacityHandle.layer = null;
_transformHandler.layer = null;
scale.removeListener(notifyListeners);
fade.removeListener(notifyListeners);
super.dispose();
}
}
......@@ -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),
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
/// Controls how the [RasterWidget] paints its children via the [RasterWidgetController].
enum RasterizeMode {
/// the children are rasterized, but only if all descendants can be rasterized.
///
/// This setting is the default state of the [RasterWidgetController].
///
/// If there is a platform view in the children of a raster widget
/// and a [RasterWidgetFallbackDelegate]h as been provided to the raster_widget,
/// this fallback delegate will be used to render the children instead of the image.
/// If there is no fallback delegate, an excpetion will be thrown
enabled,
/// The children are rasterized and any child platform views are ignored.
///
/// In this state a [RasterWidgetFallbackDelegate] is never used. Generally this
/// can be useful if there is a platform view descendant that does not need to
/// be included in the raster.
forced,
/// the children are not rasterized and the [RasterWidgetFallbackDelegate],
/// if provided, is used to draw the children.
///
///
fallback,
}
/// A controller for the [RasterWidget] that controls when the child image is displayed
/// and when to regenerated the child image.
///
/// When the value of [rasterize] is true, the [RasterWidget] will paint the child
/// widgets based on the [RasterizeMode] of the raster widget.
///
/// To force [RasterWidget] to recreate the child image, call [clear].
class RasterWidgetController extends ChangeNotifier {
/// Create a new [RasterWidgetController].
///
/// By default, [rasterize] is `false` and cannot be `null`.
RasterWidgetController({
bool rasterize = false,
}) : _rasterize = rasterize;
/// Reset the raster held by any listening [RasterWidget].
///
/// This has no effect if [rasterize] is `false`.
void clear() {
notifyListeners();
}
/// Whether a rasterized version of this render objects child is drawn in
/// place of the child.
bool get rasterize => _rasterize;
bool _rasterize;
set rasterize(bool value) {
if (value == rasterize) {
return;
}
_rasterize = value;
notifyListeners();
}
}
/// A widget that replaces its child with a rasterized version of the child.
///
/// By default, the child is drawn as is. The default [delegate] simply scales
/// down the image by the current device pixel ratio and paints it into the
/// canvas. How this image is drawn can be customized by providing a new
/// subclass of [RasterWidgetDelegate] to the [delegate] argument.
///
/// Caveats:
///
/// The contents of platform views cannot be captured by a raster
/// widget. If a platform view is encountered, then the raster widget will
/// determine how to render its children based on the [RasterizeMode]. This
/// defaults to [RasterizeMode.enabled] which will throw an exception if a platform
/// view is encountered.
///
/// This widget is not supported on the HTML backend of Flutter for the web.
class RasterWidget extends SingleChildRenderObjectWidget {
/// Create a new [RasterWidget].
///
/// The [controller] and [child] arguments are required.
const RasterWidget({
super.key,
this.delegate = const _RasterDefaultDelegate(),
this.fallback,
this.mode = RasterizeMode.enabled,
required this.controller,
required super.child
});
/// A delegate that allows customization of how the image is painted.
///
/// If not provided, defaults to a delegate which paints the child as is.
final RasterWidgetDelegate delegate;
/// The controller that determines when to display the children as an image.
final RasterWidgetController controller;
/// A fallback delegate which is used if the child layers contains a platform view.
final RasterWidgetFallbackDelegate? fallback;
/// Configuration that controls how the raster widget decides to draw its children.
///
/// Defaults to [RasterizeMode.enabled], which throws an error when a platform view
/// or other un-rasterizable view is encountered.
///
/// See [RasterizeMode] for more information.
final RasterizeMode mode;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderRasterWidget(
delegate: delegate,
controller: controller,
fallback: fallback,
mode: mode,
devicePixelRatio: MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0,
);
}
@override
void updateRenderObject(BuildContext context, covariant RenderRasterWidget renderObject) {
renderObject
..delegate = delegate
..controller = controller
..fallback = fallback
..mode = mode
..devicePixelRatio = MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0;
}
}
/// A delegate which the [RasterWidget] can use to fallback to regular rendering
/// if a platform view is present in the layer tree.
///
/// Consumers of [RasterWidget] should almost never use this delegate. For the most part,
/// the raster widget only functions as a performance improvement. If a platform view is
/// present, the performance improving qualities aren't possible and using this API is
/// pointless.
///
/// Instead, this interface is useful if a generic/reusable widget is being created which
/// may include a platform view and it needs to handle this transparently. For example, the
/// framework uses this for the zoom page transition so that navigating to a page shows the same
/// animation whether or not there is a platform view.
abstract class RasterWidgetFallbackDelegate {
/// const constructor so that subclasses can be const.
const RasterWidgetFallbackDelegate();
/// Paint the child via [painter], applying any effects that would have been painted
/// with the [RasterWidgetDelegate].
///
/// The [offset] and [size] are the location and dimensions of the render object.
void paintFallback(PaintingContext context, Offset offset, Size size, PaintingContextCallback painter);
}
/// A delegate used to draw the image representing the rasterized child.
///
/// The delegate can call [notifyListeners] to have the raster widget
/// re-paint (re-using the same raster). This allows animations to be connected
/// to the raster and performed without re-rasterization of children. For
/// certain scale or perspective changing transforms, such as a rotation, this
/// can be significantly faster than performing the same animation at the
/// widget level.
///
/// By default, the [RasterWidget] includes a delegate that draws the child raster
/// exactly as the child widgets would have been drawn. Nevertheless, this can
/// also be used to efficiently transform the child raster and apply complex paint
/// effects.
///
/// {@tool snippet}
///
/// The following method shows how to efficiently rotate the child raster.
///
/// ```dart
/// void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
/// const double radians = 0.5; // Could be driven by an animation.
/// final Matrix4 transform = Matrix4.rotationZ(radians);
/// context.canvas.transform(transform.storage);
/// final Rect src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
/// final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
/// final Paint paint = Paint()
/// ..filterQuality = FilterQuality.low;
/// context.canvas.drawImageRect(image, src, dst, paint);
/// }
/// ```
/// {@end-tool}
abstract class RasterWidgetDelegate extends ChangeNotifier {
/// Called whenever the [image] that represents a [RasterWidget]s child should be painted.
///
/// The image is rasterized at the physical pixel resolution and should be scaled down by
/// [pixelRatio] to account for device independent pixels.
///
/// There is no offset given in this paint method, as the parent is an [OffsetLayer] all
/// offsets are [Offset.zero].
///
/// {@tool snippet}
///
/// The follow method shows how the default implementation of the delegate used by the
/// [RasterWidget] paints the child image. This must account for the fact that the image
/// width and height will be given in physical pixels, while the image must be painted with
/// device independent pixels. That is, the width and height of the image is the widget and
/// height of the provided `size`, multiplied by the `pixelRatio`:
///
/// ```dart
/// void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
/// final Rect src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
/// final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
/// final Paint paint = Paint()
/// ..filterQuality = FilterQuality.low;
/// context.canvas.drawImageRect(image, src, dst, paint);
/// }
/// ```
/// {@end-tool}
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio);
/// Called whenever a new instance of the raster widget delegate class is
/// provided to the [RenderRasterWidget] object, or any time that a new
/// [RasterWidgetDelegate] object is created with a new instance of the
/// delegate class (which amounts to the same thing, because the latter is
/// implemented in terms of the former).
///
/// If the new instance represents different information than the old
/// instance, then the method should return true, otherwise it should return
/// false.
///
/// If the method returns false, then the [paint] call might be optimized
/// away.
///
/// It's possible that the [paint] method will get called even if
/// [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to
/// be repainted). It's also possible that the [paint] method will get called
/// without [shouldRepaint] being called at all (e.g. if the box changes
/// size).
///
/// Changing the delegate will not cause the child image retained by the
/// [RenderRasterWidget] to be updated. Instead, [RasterWidgetController.clear] can
/// be used to force the generation of a new image.
///
/// The `oldDelegate` argument will never be null.
bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate);
}
/// A render object that draws its child as a [ui.Image].
class RenderRasterWidget extends RenderProxyBox {
/// Create a new [RenderRasterWidget].
RenderRasterWidget({
required RasterWidgetDelegate delegate,
required double devicePixelRatio,
required RasterWidgetController controller,
required RasterizeMode mode,
RasterWidgetFallbackDelegate? fallback,
}) : _delegate = delegate,
_devicePixelRatio = devicePixelRatio,
_controller = controller,
_fallback = fallback,
_mode = mode;
/// The device pixel ratio used to create the child image.
double get devicePixelRatio => _devicePixelRatio;
double _devicePixelRatio;
set devicePixelRatio(double value) {
if (value == devicePixelRatio) {
return;
}
_devicePixelRatio = value;
markNeedsPaint();
}
/// Whether a rasterized version of this render objects child is drawn in
/// place of the child.
RasterWidgetController get controller => _controller;
RasterWidgetController _controller;
set controller(RasterWidgetController value) {
if (value == controller) {
return;
}
controller.removeListener(_onRasterValueChanged);
final bool oldValue = controller.rasterize;
_controller = value;
if (attached) {
controller.addListener(_onRasterValueChanged);
if (oldValue != controller.rasterize) {
_onRasterValueChanged();
}
}
}
/// The delegate used to draw the image representing the child.
RasterWidgetDelegate get delegate => _delegate;
RasterWidgetDelegate _delegate;
set delegate(RasterWidgetDelegate value) {
if (value == delegate) {
return;
}
delegate.removeListener(markNeedsPaint);
final RasterWidgetDelegate oldDelegate = _delegate;
_delegate = value;
if (attached) {
delegate.addListener(markNeedsPaint);
if (delegate.shouldRepaint(oldDelegate)) {
markNeedsPaint();
}
}
}
/// A fallback delegate which is used if the child layers contains a platform view.
RasterWidgetFallbackDelegate? get fallback => _fallback;
RasterWidgetFallbackDelegate? _fallback;
set fallback(RasterWidgetFallbackDelegate? value) {
if (value == fallback) {
return;
}
_fallback = value;
markNeedsPaint();
}
/// How the raster widget will handle platform views in child layers.
RasterizeMode get mode => _mode;
RasterizeMode _mode;
set mode(RasterizeMode value) {
if (value == _mode) {
return;
}
_mode = value;
markNeedsPaint();
}
ui.Image? _childRaster;
@override
void attach(covariant PipelineOwner owner) {
delegate.addListener(markNeedsPaint);
controller.addListener(_onRasterValueChanged);
super.attach(owner);
}
@override
void detach() {
delegate.removeListener(markNeedsPaint);
controller.removeListener(_onRasterValueChanged);
_childRaster?.dispose();
_childRaster = null;
super.detach();
}
@override
void dispose() {
delegate.removeListener(markNeedsPaint);
controller.removeListener(_onRasterValueChanged);
_childRaster?.dispose();
_childRaster = null;
super.dispose();
}
void _onRasterValueChanged() {
_childRaster?.dispose();
_childRaster = null;
markNeedsPaint();
}
bool _hitPlatformView = false;
bool get _useFallback => _hitPlatformView || mode == RasterizeMode.fallback;
// Paint [child] with this painting context, then convert to a raster and detach all
// children from this layer.
ui.Image? _paintAndDetachToImage() {
final OffsetLayer offsetLayer = OffsetLayer();
final PaintingContext context = PaintingContext(offsetLayer, Offset.zero & size);
super.paint(context, Offset.zero);
// This ignore is here because this method is protected by the `PaintingContext`. Adding a new
// method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but
// that would conflict with our goals of minimizing painting context.
// ignore: invalid_use_of_protected_member
context.stopRecordingIfNeeded();
if (mode != RasterizeMode.forced && !offsetLayer.supportsRasterization()) {
_hitPlatformView = true;
if (fallback == null) {
assert(() {
throw FlutterError(
'RasterWidget used with a child that contains a PlatformView.'
);
}());
}
return null;
}
final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio);
offsetLayer.dispose();
return image;
}
@override
void paint(PaintingContext context, Offset offset) {
if (size.isEmpty) {
_childRaster?.dispose();
_childRaster = null;
return;
}
if (controller.rasterize) {
if (_useFallback) {
fallback?.paintFallback(context, offset, size, super.paint);
} else {
_childRaster ??= _paintAndDetachToImage();
if (_childRaster == null && _useFallback) {
fallback?.paintFallback(context, offset, size, super.paint);
} else {
delegate.paint(context, offset, size, _childRaster!, devicePixelRatio);
}
}
return;
}
_childRaster?.dispose();
_childRaster = null;
super.paint(context, offset);
}
}
// A delegate that paints the child widget as is.
class _RasterDefaultDelegate implements RasterWidgetDelegate {
const _RasterDefaultDelegate();
@override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
final Rect src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
final Paint paint = Paint()
..filterQuality = FilterQuality.low;
context.canvas.drawImageRect(image, src, dst, paint);
}
@override
bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate) => false;
@override
void addListener(ui.VoidCallback listener) { }
@override
void dispose() { }
@override
bool get hasListeners => false;
@override
void notifyListeners() { }
@override
void removeListener(ui.VoidCallback listener) { }
}
......@@ -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;
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('RasterWidget can rasterize child', (WidgetTester tester) async {
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final Key key = UniqueKey();
await tester.pumpWidget(RepaintBoundary(
key: key,
child: Center(
child: RasterWidget(
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
),
));
// Rasterization is not actually complete until a frame has been pumped through
// the engine.
await tester.pumpAndSettle();
await expectLater(find.byKey(key), matchesGoldenFile('raster_widget.yellow.png'));
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget is not a repaint boundary when rasterizing', (WidgetTester tester) async {
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(RepaintBoundary(
child: Center(
child: RasterWidget(
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
),
));
expect(tester.layers, hasLength(3));
expect(tester.layers.last, isA<PictureLayer>());
controller.rasterize = false;
await tester.pump();
expect(tester.layers, hasLength(3));
expect(tester.layers.last, isA<PictureLayer>());
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget repaints when RasterWidgetDelegate notifies listeners', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(RepaintBoundary(
child: Center(
child: RasterWidget(
delegate: delegate,
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
),
));
expect(delegate.count, 1);
delegate.notify();
await tester.pump();
expect(delegate.count, 2);
// Remove widget and verify removal of listeners.
await tester.pumpWidget(const SizedBox());
delegate.notify();
await tester.pump();
expect(delegate.count, 2);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget will recreate raster when controller calls clear', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(RepaintBoundary(
child: Center(
child: RasterWidget(
delegate: delegate,
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
),
));
expect(delegate.lastImage, isNotNull);
final ui.Image lastImage = delegate.lastImage!;
await tester.pump();
// Raster is re-used
expect(lastImage, equals(delegate.lastImage));
controller.clear();
await tester.pump();
// Raster is re-created.
expect(delegate.lastImage, isNotNull);
expect(lastImage, isNot(delegate.lastImage));
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget can update the delegate', (WidgetTester tester) async {
final TestDelegate delegateA = TestDelegate();
final TestDelegate delegateB = TestDelegate()
..shouldRepaintValue = true;
TestDelegate delegate = delegateA;
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
late void Function(void Function()) setStateFn;
await tester.pumpWidget(StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
setStateFn = setState;
return Center(
child: RasterWidget(
delegate: delegate,
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
);
})
);
expect(delegateA.count, 1);
expect(delegateB.count, 0);
setStateFn(() {
delegate = delegateB;
});
await tester.pump();
expect(delegateA.count, 1);
expect(delegateB.count, 1);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget can update the ValueNotifier', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controllerA = RasterWidgetController(rasterize: true);
final RasterWidgetController controllerB = RasterWidgetController();
RasterWidgetController controller = controllerA;
late void Function(void Function()) setStateFn;
await tester.pumpWidget(StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
setStateFn = setState;
return Center(
child: RasterWidget(
delegate: delegate,
controller: controller,
child: Container(
width: 100,
height: 100,
color: const Color(0xFFAABB11),
),
),
);
})
);
expect(delegate.count, 1);
expect(tester.layers.last, isA<OffsetLayer>());
setStateFn(() {
controller = controllerB;
});
await tester.pump();
expect(delegate.count, 1);
expect(tester.layers.last, isA<PictureLayer>());
// changes to old notifier do not impact widget.
controllerA.rasterize = false;
await tester.pump();
expect(delegate.count, 1);
expect(tester.layers.last, isA<PictureLayer>());
await tester.pumpWidget(const SizedBox());
// changes to notifier do not impact widget after disposal.
controllerB.rasterize = true;
await tester.pump();
expect(delegate.count, 1);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget correctly attaches and detaches delegate callbacks', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final RenderRasterWidget rasterWidget = RenderRasterWidget(
delegate: delegate,
controller: controller,
devicePixelRatio: 1.0,
mode: RasterizeMode.enabled,
);
expect(delegate.addedListenerCount, 0);
expect(delegate.removedListenerCount, 0);
final PipelineOwner owner = PipelineOwner();
rasterWidget.attach(owner);
expect(delegate.addedListenerCount, 1);
expect(delegate.removedListenerCount, 0);
rasterWidget.detach();
expect(delegate.addedListenerCount, 1);
expect(delegate.removedListenerCount, 1);
final TestDelegate updatedDelegate = TestDelegate();
rasterWidget.delegate = updatedDelegate;
// No listeners added or removed while not attached.
expect(updatedDelegate.addedListenerCount, 0);
expect(updatedDelegate.removedListenerCount, 0);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget correctly attaches and detaches controller callbacks', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final TestController controller = TestController();
final RenderRasterWidget rasterWidget = RenderRasterWidget(
delegate: delegate,
controller: controller,
devicePixelRatio: 1.0,
mode: RasterizeMode.enabled,
);
expect(controller.addedListenerCount, 0);
expect(controller.removedListenerCount, 0);
final PipelineOwner owner = PipelineOwner();
rasterWidget.attach(owner);
expect(controller.addedListenerCount, 1);
expect(controller.removedListenerCount, 0);
rasterWidget.detach();
expect(controller.addedListenerCount, 1);
expect(controller.removedListenerCount, 1);
final TestController updatedController = TestController();
rasterWidget.controller = updatedController;
// No listeners added or removed while not attached.
expect(updatedController.addedListenerCount, 0);
expect(updatedController.removedListenerCount, 0);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget does not error on rasterization of child with empty size', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RasterWidget(
delegate: delegate,
controller: controller,
child: const SizedBox(),
),
),
),
);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget throws assertion if platform view is encountered', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RasterWidget(
delegate: delegate,
controller: controller,
child: const SizedBox(
width: 100,
height: 100,
child: TestPlatformView(),
),
),
),
),
);
expect(tester.takeException(), isA<FlutterError>()
.having((FlutterError error) => error.message, 'message', contains('RasterWidget used with a child that contains a PlatformView')));
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget does not assert if RasterizeMode.forced', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RasterWidget(
delegate: delegate,
controller: controller,
mode: RasterizeMode.forced,
child: const SizedBox(
width: 100,
height: 100,
child: TestPlatformView(),
),
),
),
),
);
expect(tester.takeException(), isNull);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget fallbacks to delegate if PlatformView is present', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final TestFallback fallback = TestFallback();
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RasterWidget(
delegate: delegate,
controller: controller,
fallback: fallback,
child: const SizedBox(
width: 100,
height: 100,
child: TestPlatformView(),
),
),
),
),
);
expect(fallback.calledFallback, 1);
expect(delegate.count, 0);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget fallbacks to delegate if mode: RasterizeMode.fallback', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate();
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final TestFallback fallback = TestFallback();
await tester.pumpWidget(
Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: RasterWidget(
delegate: delegate,
controller: controller,
fallback: fallback,
mode: RasterizeMode.fallback,
child: const SizedBox(
width: 100,
height: 100,
),
),
),
),
);
expect(fallback.calledFallback, 1);
expect(delegate.count, 0);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
}
class TestFallback extends RasterWidgetFallbackDelegate {
int calledFallback = 0;
@override
void paintFallback(PaintingContext context, ui.Offset offset, ui.Size size, PaintingContextCallback painter) {
calledFallback += 1;
}
}
class TestController extends RasterWidgetController {
int addedListenerCount = 0;
int removedListenerCount = 0;
@override
void addListener(ui.VoidCallback listener) {
addedListenerCount += 1;
super.addListener(listener);
}
@override
void removeListener(ui.VoidCallback listener) {
removedListenerCount += 1;
super.removeListener(listener);
}
}
class TestDelegate extends RasterWidgetDelegate {
int count = 0;
bool shouldRepaintValue = false;
ui.Image? lastImage;
int addedListenerCount = 0;
int removedListenerCount = 0;
@override
void addListener(ui.VoidCallback listener) {
addedListenerCount += 1;
super.addListener(listener);
}
@override
void removeListener(ui.VoidCallback listener) {
removedListenerCount += 1;
super.removeListener(listener);
}
void notify() {
notifyListeners();
}
@override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
count += 1;
lastImage = image;
}
@override
bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate) => shouldRepaintValue;
}
class TestPlatformView extends SingleChildRenderObjectWidget {
const TestPlatformView({super.key});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderTestPlatformView();
}
}
class RenderTestPlatformView extends RenderProxyBox {
@override
void paint(PaintingContext context, ui.Offset offset) {
context.addLayer(PlatformViewLayer(rect: offset & size, viewId: 1));
}
}
......@@ -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