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 ...@@ -342,6 +342,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi
super.settings, super.settings,
this.maintainState = true, this.maintainState = true,
super.fullscreenDialog, super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(builder != null), }) : assert(builder != null),
assert(maintainState != null), assert(maintainState != null),
assert(fullscreenDialog != null) { assert(fullscreenDialog != null) {
...@@ -371,6 +372,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi ...@@ -371,6 +372,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi
class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> { class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
_PageBasedCupertinoPageRoute({ _PageBasedCupertinoPageRoute({
required CupertinoPage<T> page, required CupertinoPage<T> page,
super.preferRasterization = true,
}) : assert(page != null), }) : assert(page != null),
super(settings: page) { super(settings: page) {
assert(opaque); assert(opaque);
...@@ -417,6 +419,7 @@ class CupertinoPage<T> extends Page<T> { ...@@ -417,6 +419,7 @@ class CupertinoPage<T> extends Page<T> {
this.maintainState = true, this.maintainState = true,
this.title, this.title,
this.fullscreenDialog = false, this.fullscreenDialog = false,
this.preferRasterization = true,
super.key, super.key,
super.name, super.name,
super.arguments, super.arguments,
...@@ -437,9 +440,12 @@ class CupertinoPage<T> extends Page<T> { ...@@ -437,9 +440,12 @@ class CupertinoPage<T> extends Page<T> {
/// {@macro flutter.widgets.PageRoute.fullscreenDialog} /// {@macro flutter.widgets.PageRoute.fullscreenDialog}
final bool fullscreenDialog; final bool fullscreenDialog;
/// {@macro flutter.widgets.TransitionRoute.preferRasterization}
final bool preferRasterization;
@override @override
Route<T> createRoute(BuildContext context) { 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 ...@@ -39,6 +39,7 @@ class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixi
super.settings, super.settings,
this.maintainState = true, this.maintainState = true,
super.fullscreenDialog, super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(builder != null), }) : assert(builder != null),
assert(maintainState != null), assert(maintainState != null),
assert(fullscreenDialog != null) { assert(fullscreenDialog != null) {
...@@ -157,6 +158,7 @@ class MaterialPage<T> extends Page<T> { ...@@ -157,6 +158,7 @@ class MaterialPage<T> extends Page<T> {
required this.child, required this.child,
this.maintainState = true, this.maintainState = true,
this.fullscreenDialog = false, this.fullscreenDialog = false,
this.preferRasterization = true,
super.key, super.key,
super.name, super.name,
super.arguments, super.arguments,
...@@ -174,9 +176,12 @@ class MaterialPage<T> extends Page<T> { ...@@ -174,9 +176,12 @@ class MaterialPage<T> extends Page<T> {
/// {@macro flutter.widgets.PageRoute.fullscreenDialog} /// {@macro flutter.widgets.PageRoute.fullscreenDialog}
final bool fullscreenDialog; final bool fullscreenDialog;
/// {@macro flutter.widgets.TransitionRoute.preferRasterization}
final bool preferRasterization;
@override @override
Route<T> createRoute(BuildContext context) { 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> { ...@@ -187,6 +192,7 @@ class MaterialPage<T> extends Page<T> {
class _PageBasedMaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> { class _PageBasedMaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {
_PageBasedMaterialPageRoute({ _PageBasedMaterialPageRoute({
required MaterialPage<T> page, required MaterialPage<T> page,
super.preferRasterization,
}) : assert(page != null), }) : assert(page != null),
super(settings: page) { super(settings: page) {
assert(opaque); assert(opaque);
......
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'colors.dart'; import 'colors.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -153,6 +156,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -153,6 +156,7 @@ class _ZoomPageTransition extends StatelessWidget {
const _ZoomPageTransition({ const _ZoomPageTransition({
required this.animation, required this.animation,
required this.secondaryAnimation, required this.secondaryAnimation,
required this.preferRasterization,
this.child, this.child,
}) : assert(animation != null), }) : assert(animation != null),
assert(secondaryAnimation != null); assert(secondaryAnimation != null);
...@@ -190,6 +194,14 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -190,6 +194,14 @@ class _ZoomPageTransition extends StatelessWidget {
/// property when the [_ZoomPageTransition] is used as a page transition. /// property when the [_ZoomPageTransition] is used as a page transition.
final Animation<double> secondaryAnimation; 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. /// The widget below this widget in the tree.
/// ///
/// This widget will transition in and out as driven by [animation] and /// This widget will transition in and out as driven by [animation] and
...@@ -207,6 +219,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -207,6 +219,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomEnterTransition( return _ZoomEnterTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization,
child: child, child: child,
); );
}, },
...@@ -217,6 +230,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -217,6 +230,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomExitTransition( return _ZoomExitTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization,
reverse: true, reverse: true,
child: child, child: child,
); );
...@@ -230,6 +244,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -230,6 +244,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomEnterTransition( return _ZoomEnterTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization,
reverse: true, reverse: true,
child: child, child: child,
); );
...@@ -241,6 +256,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -241,6 +256,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomExitTransition( return _ZoomExitTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization,
child: child, child: child,
); );
}, },
...@@ -250,18 +266,30 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -250,18 +266,30 @@ class _ZoomPageTransition extends StatelessWidget {
} }
} }
class _ZoomEnterTransition extends StatelessWidget { class _ZoomEnterTransition extends StatefulWidget {
const _ZoomEnterTransition({ const _ZoomEnterTransition({
required this.animation, required this.animation,
this.reverse = false, this.reverse = false,
required this.preferRasterization,
this.child, this.child,
}) : assert(animation != null), }) : assert(animation != null),
assert(reverse != null); assert(reverse != null);
final Animation<double> animation; final Animation<double> animation;
final Widget? child; final Widget? child;
final bool preferRasterization;
final bool reverse; 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>( static final Animatable<double> _fadeInTransition = Tween<double>(
begin: 0.0, begin: 0.0,
end: 1.00, end: 1.00,
...@@ -282,60 +310,93 @@ class _ZoomEnterTransition extends StatelessWidget { ...@@ -282,60 +310,93 @@ class _ZoomEnterTransition extends StatelessWidget {
end: 0.60, end: 0.60,
).chain(CurveTween(curve: const Interval(0.2075, 0.4175))); ).chain(CurveTween(curve: const Interval(0.2075, 0.4175)));
@override void _updateAnimations() {
Widget build(BuildContext context) { fadeTransition = widget.reverse
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
? kAlwaysCompleteAnimation ? kAlwaysCompleteAnimation
: _fadeInTransition.animate(animation); : _fadeInTransition.animate(widget.animation);
final Animation<double> scaleTransition = (reverse scaleTransition = (widget.reverse
? _scaleDownTransition ? _scaleDownTransition
: _scaleUpTransition : _scaleUpTransition
).animate(animation); ).animate(widget.animation);
return AnimatedBuilder( widget.animation.addListener(onAnimationValueChange);
animation: animation, widget.animation.addStatusListener(onAnimationStatusChange);
builder: (BuildContext context, Widget? child) { }
return ColoredBox(
color: Colors.black.withOpacity(opacity), @override
child: child, void initState() {
_updateAnimations();
delegate = _ZoomEnterTransitionDelegate(
reverse: widget.reverse,
fade: fadeTransition,
scale: scaleTransition,
animation: widget.animation,
); );
}, super.initState();
child: FadeTransition( }
opacity: fadeTransition,
child: ScaleTransition(scale: scaleTransition, child: child), @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({ const _ZoomExitTransition({
required this.animation, required this.animation,
this.reverse = false, this.reverse = false,
required this.preferRasterization,
this.child, this.child,
}) : assert(animation != null), }) : assert(animation != null),
assert(reverse != null); assert(reverse != null);
final Animation<double> animation; final Animation<double> animation;
final bool preferRasterization;
final bool reverse; final bool reverse;
final Widget? child; 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>( static final Animatable<double> _fadeOutTransition = Tween<double>(
begin: 1.0, begin: 1.0,
end: 0.0, end: 0.0,
...@@ -351,19 +412,62 @@ class _ZoomExitTransition extends StatelessWidget { ...@@ -351,19 +412,62 @@ class _ZoomExitTransition extends StatelessWidget {
end: 0.90, end: 0.90,
).chain(_ZoomPageTransition._scaleCurveSequence); ).chain(_ZoomPageTransition._scaleCurveSequence);
@override void _updateAnimations() {
Widget build(BuildContext context) { fadeTransition = widget.reverse
final Animation<double> fadeTransition = reverse ? _fadeOutTransition.animate(widget.animation)
? _fadeOutTransition.animate(animation)
: kAlwaysCompleteAnimation; : kAlwaysCompleteAnimation;
final Animation<double> scaleTransition = (reverse scaleTransition = (widget.reverse
? _scaleDownTransition ? _scaleDownTransition
: _scaleUpTransition : _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( @override
opacity: fadeTransition, void dispose() {
child: ScaleTransition(scale: scaleTransition, child: child), 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 { ...@@ -498,6 +602,7 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
return _ZoomPageTransition( return _ZoomPageTransition(
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
preferRasterization: route?.preferRasterization ?? true,
child: child, child: child,
); );
} }
...@@ -629,3 +734,191 @@ class PageTransitionsTheme with Diagnosticable { ...@@ -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 { ...@@ -168,6 +168,18 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
bool _debugMutationsLocked = false; 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, /// Describes the clip that would be applied to contents of this layer,
/// if any. /// if any.
Rect? describeClipBounds() => null; Rect? describeClipBounds() => null;
...@@ -875,6 +887,12 @@ class TextureLayer extends Layer { ...@@ -875,6 +887,12 @@ class TextureLayer extends Layer {
/// The identity of the backend texture. /// The identity of the backend texture.
final int textureId; 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. /// When true the texture will not be updated with new frames.
/// ///
/// This is used for resizing embedded Android views: when resizing there /// This is used for resizing embedded Android views: when resizing there
...@@ -925,6 +943,11 @@ class PlatformViewLayer extends Layer { ...@@ -925,6 +943,11 @@ class PlatformViewLayer extends Layer {
/// A UIView with this identifier must have been created by [PlatformViewsService.initUiKitView]. /// A UIView with this identifier must have been created by [PlatformViewsService.initUiKitView].
final int viewId; final int viewId;
@override
bool supportsRasterization() {
return false;
}
@override @override
void addToScene(ui.SceneBuilder builder) { void addToScene(ui.SceneBuilder builder) {
builder.addPlatformView( builder.addPlatformView(
...@@ -1043,6 +1066,16 @@ class ContainerLayer extends Layer { ...@@ -1043,6 +1066,16 @@ class ContainerLayer extends Layer {
/// Returns whether this layer has at least one child layer. /// Returns whether this layer has at least one child layer.
bool get hasChildren => _firstChild != null; 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) /// Consider this layer as the root and build a scene (a tree of layers)
/// in the engine. /// in the engine.
// The reason this method is in the `ContainerLayer` class rather than // The reason this method is in the `ContainerLayer` class rather than
...@@ -1385,6 +1418,16 @@ class OffsetLayer extends ContainerLayer { ...@@ -1385,6 +1418,16 @@ class OffsetLayer extends ContainerLayer {
properties.add(DiagnosticsProperty<Offset>('offset', offset)); 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. /// 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 /// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
...@@ -1397,22 +1440,15 @@ class OffsetLayer extends ContainerLayer { ...@@ -1397,22 +1440,15 @@ class OffsetLayer extends ContainerLayer {
/// (the default) will give you a 1:1 mapping between logical pixels and the /// (the default) will give you a 1:1 mapping between logical pixels and the
/// output pixels in the image. /// output pixels in the image.
/// ///
/// This API functions like [toImageSync], except that it only returns after
/// rasterization is complete.
///
/// See also: /// See also:
/// ///
/// * [RenderRepaintBoundary.toImage] for a similar API at the render object level. /// * [RenderRepaintBoundary.toImage] for a similar API at the render object level.
/// * [dart:ui.Scene.toImage] for more information about the image returned. /// * [dart:ui.Scene.toImage] for more information about the image returned.
Future<ui.Image> toImage(Rect bounds, { double pixelRatio = 1.0 }) async { Future<ui.Image> toImage(Rect bounds, { double pixelRatio = 1.0 }) async {
assert(bounds != null); final ui.Scene scene = _createSceneForImage(bounds, pixelRatio: pixelRatio);
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);
try { try {
// Size is rounded up to the next pixel to make sure we don't clip off // Size is rounded up to the next pixel to make sure we don't clip off
...@@ -1425,6 +1461,40 @@ class OffsetLayer extends ContainerLayer { ...@@ -1425,6 +1461,40 @@ class OffsetLayer extends ContainerLayer {
scene.dispose(); 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. /// A composite layer that clips its children using a rectangle.
......
...@@ -12,6 +12,7 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -12,6 +12,7 @@ abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({ PageRoute({
super.settings, super.settings,
this.fullscreenDialog = false, this.fullscreenDialog = false,
this.preferRasterization = true,
}); });
/// {@template flutter.widgets.PageRoute.fullscreenDialog} /// {@template flutter.widgets.PageRoute.fullscreenDialog}
...@@ -24,6 +25,9 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -24,6 +25,9 @@ abstract class PageRoute<T> extends ModalRoute<T> {
/// {@endtemplate} /// {@endtemplate}
final bool fullscreenDialog; final bool fullscreenDialog;
@override
final bool preferRasterization;
@override @override
bool get opaque => true; bool get opaque => true;
...@@ -62,6 +66,7 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -62,6 +66,7 @@ class PageRouteBuilder<T> extends PageRoute<T> {
this.barrierLabel, this.barrierLabel,
this.maintainState = true, this.maintainState = true,
super.fullscreenDialog, super.fullscreenDialog,
super.preferRasterization = true,
}) : assert(pageBuilder != null), }) : assert(pageBuilder != null),
assert(transitionsBuilder != null), assert(transitionsBuilder != null),
assert(opaque != 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> { ...@@ -126,6 +126,21 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// {@endtemplate} /// {@endtemplate}
bool get opaque; 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, // This ensures that if we got to the dismissed state while still current,
// we will still be disposed when we are eventually popped. // we will still be disposed when we are eventually popped.
// //
...@@ -1719,6 +1734,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> { ...@@ -1719,6 +1734,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
@override @override
bool get maintainState => true; bool get maintainState => true;
@override
bool get preferRasterization => false;
} }
/// A [Navigator] observer that notifies [RouteAware]s of changes to the /// A [Navigator] observer that notifies [RouteAware]s of changes to the
......
...@@ -89,6 +89,7 @@ export 'src/widgets/platform_menu_bar.dart'; ...@@ -89,6 +89,7 @@ export 'src/widgets/platform_menu_bar.dart';
export 'src/widgets/platform_view.dart'; export 'src/widgets/platform_view.dart';
export 'src/widgets/preferred_size.dart'; export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raster_widget.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/reorderable_list.dart'; export 'src/widgets/reorderable_list.dart';
export 'src/widgets/restoration.dart'; export 'src/widgets/restoration.dart';
......
...@@ -155,32 +155,33 @@ void main() { ...@@ -155,32 +155,33 @@ void main() {
expect(widget1InitialTopLeft == widget1TransientTopLeft, true); expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('test page transition (_ZoomPageTransition)', (WidgetTester tester) async { testWidgets('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async {
Iterable<T> findWidgets<T extends Widget>(Finder of) { Iterable<Layer> findLayers(Finder of) {
return tester.widgetList<T>( return tester.layerListOf(
find.ancestor(of: of, matching: find.byType(T)), find.ancestor(of: of, matching: find.byType(RasterWidget)).first,
); );
} }
FadeTransition findForwardFadeTransition(Finder of) { OpacityLayer findForwardFadeTransition(Finder of) {
return findWidgets<FadeTransition>(of).where( return findLayers(of).whereType<OpacityLayer>().first;
(FadeTransition t) => t.opacity.status == AnimationStatus.forward,
).first;
} }
ScaleTransition findForwardScaleTransition(Finder of) { TransformLayer findForwardScaleTransition(Finder of) {
return findWidgets<ScaleTransition>(of).where( return findLayers(of).whereType<TransformLayer>().first;
(ScaleTransition t) => t.scale.status == AnimationStatus.forward,
).first;
} }
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: const Material(child: Text('Page 1')), onGenerateRoute: (RouteSettings settings) {
routes: <String, WidgetBuilder>{ return MaterialPageRoute<void>(
'/next': (BuildContext context) { preferRasterization: false,
builder: (BuildContext context) {
if (settings.name == '/') {
return const Material(child: Text('Page 1'));
}
return const Material(child: Text('Page 2')); return const Material(child: Text('Page 2'));
}, },
);
}, },
), ),
); );
...@@ -189,16 +190,20 @@ void main() { ...@@ -189,16 +190,20 @@ void main() {
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
ScaleTransition widget1Scale = findForwardScaleTransition(find.text('Page 1')); TransformLayer widget1Scale = findForwardScaleTransition(find.text('Page 1'));
ScaleTransition widget2Scale = findForwardScaleTransition(find.text('Page 2')); TransformLayer widget2Scale = findForwardScaleTransition(find.text('Page 2'));
FadeTransition widget2Opacity = findForwardFadeTransition(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. // 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. // 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. // 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: 250));
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
...@@ -216,11 +221,11 @@ void main() { ...@@ -216,11 +221,11 @@ void main() {
widget2Opacity = findForwardFadeTransition(find.text('Page 2')); widget2Opacity = findForwardFadeTransition(find.text('Page 2'));
// Page 1 is narrowing down, but still larger than 1.0. // 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. // 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. // 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: 200));
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
......
...@@ -1679,7 +1679,7 @@ void main() { ...@@ -1679,7 +1679,7 @@ void main() {
// Wait for context menu to be built. // Wait for context menu to be built.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final RenderBox container = tester.renderObject(find.descendant( final RenderBox container = tester.renderObject(find.descendant(
of: find.byType(FadeTransition), of: find.byType(RasterWidget),
matching: find.byType(SizedBox), matching: find.byType(SizedBox),
).first); ).first);
expect(container.size, Size.zero); expect(container.size, Size.zero);
......
...@@ -553,6 +553,24 @@ void main() { ...@@ -553,6 +553,24 @@ void main() {
parent.buildScene(SceneBuilder()); parent.buildScene(SceneBuilder());
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857 }, 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', () { test('PictureLayer does not let you call dispose unless refcount is 0', () {
PictureLayer layer = PictureLayer(Rect.zero); PictureLayer layer = PictureLayer(Rect.zero);
expect(layer.debugHandleCount, 0); expect(layer.debugHandleCount, 0);
...@@ -980,6 +998,35 @@ void main() { ...@@ -980,6 +998,35 @@ void main() {
root.dispose(); root.dispose();
expect(() => callback(), returnsNormally); 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 { class FakeEngineLayer extends Fake implements EngineLayer {
......
...@@ -4089,7 +4089,7 @@ class ZeroDurationPage extends Page<void> { ...@@ -4089,7 +4089,7 @@ class ZeroDurationPage extends Page<void> {
class ZeroDurationPageRoute extends PageRoute<void> { class ZeroDurationPageRoute extends PageRoute<void> {
ZeroDurationPageRoute({required ZeroDurationPage page}) ZeroDurationPageRoute({required ZeroDurationPage page})
: super(settings: page); : super(settings: page, preferRasterization: false);
@override @override
Duration get transitionDuration => Duration.zero; 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 { ...@@ -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). /// All elements currently in the widget tree (lazy pre-order traversal).
/// ///
/// The returned iterable is lazy. It does not walk the entire widget tree /// 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