Unverified Commit 637bd0fb authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] simplify raster widget, rename, combine painters (#109485)

parent 7f260672
...@@ -342,7 +342,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi ...@@ -342,7 +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, super.allowSnapshotting = true,
}) : assert(builder != null), }) : assert(builder != null),
assert(maintainState != null), assert(maintainState != null),
assert(fullscreenDialog != null) { assert(fullscreenDialog != null) {
...@@ -372,7 +372,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMi ...@@ -372,7 +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, super.allowSnapshotting = true,
}) : assert(page != null), }) : assert(page != null),
super(settings: page) { super(settings: page) {
assert(opaque); assert(opaque);
...@@ -419,7 +419,7 @@ class CupertinoPage<T> extends Page<T> { ...@@ -419,7 +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, this.allowSnapshotting = true,
super.key, super.key,
super.name, super.name,
super.arguments, super.arguments,
...@@ -440,12 +440,12 @@ class CupertinoPage<T> extends Page<T> { ...@@ -440,12 +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} /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting}
final bool preferRasterization; final bool allowSnapshotting;
@override @override
Route<T> createRoute(BuildContext context) { Route<T> createRoute(BuildContext context) {
return _PageBasedCupertinoPageRoute<T>(page: this, preferRasterization: preferRasterization); return _PageBasedCupertinoPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting);
} }
} }
......
...@@ -39,7 +39,7 @@ class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixi ...@@ -39,7 +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, super.allowSnapshotting = true,
}) : assert(builder != null), }) : assert(builder != null),
assert(maintainState != null), assert(maintainState != null),
assert(fullscreenDialog != null) { assert(fullscreenDialog != null) {
...@@ -158,7 +158,7 @@ class MaterialPage<T> extends Page<T> { ...@@ -158,7 +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, this.allowSnapshotting = true,
super.key, super.key,
super.name, super.name,
super.arguments, super.arguments,
...@@ -176,12 +176,12 @@ class MaterialPage<T> extends Page<T> { ...@@ -176,12 +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} /// {@macro flutter.widgets.TransitionRoute.allowSnapshotting}
final bool preferRasterization; final bool allowSnapshotting;
@override @override
Route<T> createRoute(BuildContext context) { Route<T> createRoute(BuildContext context) {
return _PageBasedMaterialPageRoute<T>(page: this, preferRasterization: preferRasterization); return _PageBasedMaterialPageRoute<T>(page: this, allowSnapshotting: allowSnapshotting);
} }
} }
...@@ -192,7 +192,7 @@ class MaterialPage<T> extends Page<T> { ...@@ -192,7 +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, super.allowSnapshotting,
}) : assert(page != null), }) : assert(page != null),
super(settings: page) { super(settings: page) {
assert(opaque); assert(opaque);
......
...@@ -156,7 +156,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -156,7 +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, required this.allowSnapshotting,
this.child, this.child,
}) : assert(animation != null), }) : assert(animation != null),
assert(secondaryAnimation != null); assert(secondaryAnimation != null);
...@@ -194,13 +194,12 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -194,13 +194,12 @@ 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 /// Whether the [SnapshotWidget] will be used.
/// will be used.
/// ///
/// Notably, this improves performance by disabling animations on both the outgoing and /// Notably, this improves performance by disabling animations on both the outgoing and
/// incoming route. This also implies that ink-splashes or similar animations will /// incoming route. This also implies that ink-splashes or similar animations will
/// not animate during the transition. /// not animate during the transition.
final bool preferRasterization; final bool allowSnapshotting;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -219,7 +218,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -219,7 +218,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomEnterTransition( return _ZoomEnterTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization, allowSnapshotting: allowSnapshotting,
child: child, child: child,
); );
}, },
...@@ -230,7 +229,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -230,7 +229,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomExitTransition( return _ZoomExitTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization, allowSnapshotting: allowSnapshotting,
reverse: true, reverse: true,
child: child, child: child,
); );
...@@ -244,7 +243,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -244,7 +243,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomEnterTransition( return _ZoomEnterTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization, allowSnapshotting: allowSnapshotting,
reverse: true, reverse: true,
child: child, child: child,
); );
...@@ -256,7 +255,7 @@ class _ZoomPageTransition extends StatelessWidget { ...@@ -256,7 +255,7 @@ class _ZoomPageTransition extends StatelessWidget {
) { ) {
return _ZoomExitTransition( return _ZoomExitTransition(
animation: animation, animation: animation,
preferRasterization: preferRasterization, allowSnapshotting: allowSnapshotting,
child: child, child: child,
); );
}, },
...@@ -270,14 +269,14 @@ class _ZoomEnterTransition extends StatefulWidget { ...@@ -270,14 +269,14 @@ class _ZoomEnterTransition extends StatefulWidget {
const _ZoomEnterTransition({ const _ZoomEnterTransition({
required this.animation, required this.animation,
this.reverse = false, this.reverse = false,
required this.preferRasterization, required this.allowSnapshotting,
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 allowSnapshotting;
final bool reverse; final bool reverse;
@override @override
...@@ -285,10 +284,13 @@ class _ZoomEnterTransition extends StatefulWidget { ...@@ -285,10 +284,13 @@ class _ZoomEnterTransition extends StatefulWidget {
} }
class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTransitionBase { class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTransitionBase {
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 // See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
bool get allowRasterization => !kIsWeb && widget.preferRasterization; // support this functionality and the canvaskit backend uses a single thread for UI and raster
// work which diminishes the impact of this performance improvement.
@override
bool get useSnapshot => !kIsWeb && widget.allowSnapshotting;
late _ZoomEnterTransitionDelegate delegate; late _ZoomEnterTransitionPainter delegate;
static final Animatable<double> _fadeInTransition = Tween<double>( static final Animatable<double> _fadeInTransition = Tween<double>(
begin: 0.0, begin: 0.0,
...@@ -327,7 +329,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -327,7 +329,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
@override @override
void initState() { void initState() {
_updateAnimations(); _updateAnimations();
delegate = _ZoomEnterTransitionDelegate( delegate = _ZoomEnterTransitionPainter(
reverse: widget.reverse, reverse: widget.reverse,
fade: fadeTransition, fade: fadeTransition,
scale: scaleTransition, scale: scaleTransition,
...@@ -343,7 +345,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -343,7 +345,7 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
oldWidget.animation.removeStatusListener(onAnimationStatusChange); oldWidget.animation.removeStatusListener(onAnimationStatusChange);
_updateAnimations(); _updateAnimations();
delegate.dispose(); delegate.dispose();
delegate = _ZoomEnterTransitionDelegate( delegate = _ZoomEnterTransitionPainter(
reverse: widget.reverse, reverse: widget.reverse,
fade: fadeTransition, fade: fadeTransition,
scale: scaleTransition, scale: scaleTransition,
...@@ -363,11 +365,10 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr ...@@ -363,11 +365,10 @@ class _ZoomEnterTransitionState extends State<_ZoomEnterTransition> with _ZoomTr
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RasterWidget( return SnapshotWidget(
delegate: delegate, painter: delegate,
controller: controller, controller: controller,
fallback: delegate, mode: SnapshotMode.permissive,
mode: allowRasterization ? RasterizeMode.enabled : RasterizeMode.fallback,
child: widget.child, child: widget.child,
); );
} }
...@@ -377,13 +378,13 @@ class _ZoomExitTransition extends StatefulWidget { ...@@ -377,13 +378,13 @@ class _ZoomExitTransition extends StatefulWidget {
const _ZoomExitTransition({ const _ZoomExitTransition({
required this.animation, required this.animation,
this.reverse = false, this.reverse = false,
required this.preferRasterization, required this.allowSnapshotting,
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 allowSnapshotting;
final bool reverse; final bool reverse;
final Widget? child; final Widget? child;
...@@ -392,10 +393,13 @@ class _ZoomExitTransition extends StatefulWidget { ...@@ -392,10 +393,13 @@ class _ZoomExitTransition extends StatefulWidget {
} }
class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase { class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTransitionBase {
late _ZoomExitTransitionDelegate delegate; late _ZoomExitTransitionPainter delegate;
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 // See SnapshotWidget doc comment, this is disabled on web because the HTML backend doesn't
bool get allowRasterization => !kIsWeb && widget.preferRasterization; // support this functionality and the canvaskit backend uses a single thread for UI and raster
// work which diminishes the impact of this performance improvement.
@override
bool get useSnapshot => !kIsWeb && widget.allowSnapshotting;
static final Animatable<double> _fadeOutTransition = Tween<double>( static final Animatable<double> _fadeOutTransition = Tween<double>(
begin: 1.0, begin: 1.0,
...@@ -428,10 +432,11 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran ...@@ -428,10 +432,11 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran
@override @override
void initState() { void initState() {
_updateAnimations(); _updateAnimations();
delegate = _ZoomExitTransitionDelegate( delegate = _ZoomExitTransitionPainter(
reverse: widget.reverse, reverse: widget.reverse,
fade: fadeTransition, fade: fadeTransition,
scale: scaleTransition, scale: scaleTransition,
animation: widget.animation,
); );
super.initState(); super.initState();
} }
...@@ -443,10 +448,11 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran ...@@ -443,10 +448,11 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran
oldWidget.animation.removeStatusListener(onAnimationStatusChange); oldWidget.animation.removeStatusListener(onAnimationStatusChange);
_updateAnimations(); _updateAnimations();
delegate.dispose(); delegate.dispose();
delegate = _ZoomExitTransitionDelegate( delegate = _ZoomExitTransitionPainter(
reverse: widget.reverse, reverse: widget.reverse,
fade: fadeTransition, fade: fadeTransition,
scale: scaleTransition, scale: scaleTransition,
animation: widget.animation,
); );
} }
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
...@@ -462,11 +468,10 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran ...@@ -462,11 +468,10 @@ class _ZoomExitTransitionState extends State<_ZoomExitTransition> with _ZoomTran
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RasterWidget( return SnapshotWidget(
delegate: delegate, painter: delegate,
controller: controller, controller: controller,
fallback: delegate, mode: SnapshotMode.permissive,
mode: allowRasterization ? RasterizeMode.enabled : RasterizeMode.fallback,
child: widget.child, child: widget.child,
); );
} }
...@@ -602,7 +607,7 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder { ...@@ -602,7 +607,7 @@ class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
return _ZoomPageTransition( return _ZoomPageTransition(
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
preferRasterization: route?.preferRasterization ?? true, allowSnapshotting: route?.allowSnapshotting ?? true,
child: child, child: child,
); );
} }
...@@ -765,12 +770,14 @@ void _updateScaledTransform(Matrix4 transform, double scale, Size size) { ...@@ -765,12 +770,14 @@ void _updateScaledTransform(Matrix4 transform, double scale, Size size) {
} }
mixin _ZoomTransitionBase { mixin _ZoomTransitionBase {
bool get useSnapshot;
// Don't rasterize if: // Don't rasterize if:
// 1. Rasterization is disabled by the platform. // 1. Rasterization is disabled by the platform.
// 2. The animation is paused/stopped. // 2. The animation is paused/stopped.
// 3. The values of the scale/fade transition do not // 3. The values of the scale/fade transition do not
// benefit from rasterization. // benefit from rasterization.
final RasterWidgetController controller = RasterWidgetController(); final SnapshotController controller = SnapshotController();
late Animation<double> fadeTransition; late Animation<double> fadeTransition;
late Animation<double> scaleTransition; late Animation<double> scaleTransition;
...@@ -779,9 +786,9 @@ mixin _ZoomTransitionBase { ...@@ -779,9 +786,9 @@ mixin _ZoomTransitionBase {
if ((scaleTransition.value == 1.0) && if ((scaleTransition.value == 1.0) &&
(fadeTransition.value == 0.0 || (fadeTransition.value == 0.0 ||
fadeTransition.value == 1.0)) { fadeTransition.value == 1.0)) {
controller.rasterize = false; controller.allowSnapshotting = false;
} else { } else {
controller.rasterize = true; controller.allowSnapshotting = useSnapshot;
} }
} }
...@@ -789,28 +796,33 @@ mixin _ZoomTransitionBase { ...@@ -789,28 +796,33 @@ mixin _ZoomTransitionBase {
switch (status) { switch (status) {
case AnimationStatus.dismissed: case AnimationStatus.dismissed:
case AnimationStatus.completed: case AnimationStatus.completed:
controller.rasterize = false; controller.allowSnapshotting = false;
break; break;
case AnimationStatus.forward: case AnimationStatus.forward:
case AnimationStatus.reverse: case AnimationStatus.reverse:
controller.rasterize = true; controller.allowSnapshotting = useSnapshot;
break; break;
} }
} }
} }
class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements RasterWidgetFallbackDelegate { class _ZoomEnterTransitionPainter extends SnapshotPainter {
_ZoomEnterTransitionDelegate({ _ZoomEnterTransitionPainter({
required this.reverse, required this.reverse,
required this.scale, required this.scale,
required this.fade, required this.fade,
required this.animation, required this.animation,
}) { }) {
animation.addListener(notifyListeners); animation.addListener(notifyListeners);
animation.addStatusListener(_onStatusChange);
scale.addListener(notifyListeners); scale.addListener(notifyListeners);
fade.addListener(notifyListeners); fade.addListener(notifyListeners);
} }
void _onStatusChange(_) {
notifyListeners();
}
final bool reverse; final bool reverse;
final Animation<double> animation; final Animation<double> animation;
final Animation<double> scale; final Animation<double> scale;
...@@ -845,7 +857,15 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste ...@@ -845,7 +857,15 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste
} }
@override @override
void paintFallback(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) { void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
switch (animation.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
return painter(context, offset);
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
_drawScrim(context, offset, size); _drawScrim(context, offset, size);
_updateScaledTransform(_transform, scale.value, size); _updateScaledTransform(_transform, scale.value, size);
_transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) { _transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) {
...@@ -854,7 +874,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste ...@@ -854,7 +874,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste
} }
@override @override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) { void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
_drawScrim(context, offset, size); _drawScrim(context, offset, size);
_drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio); _drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio);
} }
...@@ -862,6 +882,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste ...@@ -862,6 +882,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste
@override @override
void dispose() { void dispose() {
animation.removeListener(notifyListeners); animation.removeListener(notifyListeners);
animation.removeStatusListener(_onStatusChange);
scale.removeListener(notifyListeners); scale.removeListener(notifyListeners);
fade.removeListener(notifyListeners); fade.removeListener(notifyListeners);
_opacityHandle.layer = null; _opacityHandle.layer = null;
...@@ -870,7 +891,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste ...@@ -870,7 +891,7 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste
} }
@override @override
bool shouldRepaint(covariant _ZoomEnterTransitionDelegate oldDelegate) { bool shouldRepaint(covariant _ZoomEnterTransitionPainter oldDelegate) {
return oldDelegate.reverse != reverse return oldDelegate.reverse != reverse
|| oldDelegate.animation.value != animation.value || oldDelegate.animation.value != animation.value
|| oldDelegate.scale.value != scale.value || oldDelegate.scale.value != scale.value
...@@ -878,30 +899,46 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste ...@@ -878,30 +899,46 @@ class _ZoomEnterTransitionDelegate extends RasterWidgetDelegate implements Raste
} }
} }
class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements RasterWidgetFallbackDelegate { class _ZoomExitTransitionPainter extends SnapshotPainter {
_ZoomExitTransitionDelegate({ _ZoomExitTransitionPainter({
required this.reverse, required this.reverse,
required this.scale, required this.scale,
required this.fade, required this.fade,
required this.animation,
}) { }) {
scale.addListener(notifyListeners); scale.addListener(notifyListeners);
fade.addListener(notifyListeners); fade.addListener(notifyListeners);
animation.addStatusListener(_onStatusChange);
}
void _onStatusChange(_) {
notifyListeners();
} }
final bool reverse; final bool reverse;
final Animation<double> scale; final Animation<double> scale;
final Animation<double> fade; final Animation<double> fade;
final Animation<double> animation;
final Matrix4 _transform = Matrix4.zero(); final Matrix4 _transform = Matrix4.zero();
final LayerHandle<OpacityLayer> _opacityHandle = LayerHandle<OpacityLayer>(); final LayerHandle<OpacityLayer> _opacityHandle = LayerHandle<OpacityLayer>();
final LayerHandle<TransformLayer> _transformHandler = LayerHandle<TransformLayer>(); final LayerHandle<TransformLayer> _transformHandler = LayerHandle<TransformLayer>();
@override @override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) { void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
_drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio); _drawImageScaledAndCentered(context, image, scale.value, fade.value, pixelRatio);
} }
@override @override
void paintFallback(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) { void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
switch (animation.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
return painter(context, offset);
case AnimationStatus.forward:
case AnimationStatus.reverse:
break;
}
_updateScaledTransform(_transform, scale.value, size); _updateScaledTransform(_transform, scale.value, size);
_transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) { _transformHandler.layer = context.pushTransform(true, offset, _transform, (PaintingContext context, Offset offset) {
_opacityHandle.layer = context.pushOpacity(offset, (fade.value * 255).round(), painter, oldLayer: _opacityHandle.layer); _opacityHandle.layer = context.pushOpacity(offset, (fade.value * 255).round(), painter, oldLayer: _opacityHandle.layer);
...@@ -909,7 +946,7 @@ class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements Raster ...@@ -909,7 +946,7 @@ class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements Raster
} }
@override @override
bool shouldRepaint(covariant _ZoomExitTransitionDelegate oldDelegate) { bool shouldRepaint(covariant _ZoomExitTransitionPainter oldDelegate) {
return oldDelegate.reverse != reverse || oldDelegate.fade.value != fade.value || oldDelegate.scale.value != scale.value; return oldDelegate.reverse != reverse || oldDelegate.fade.value != fade.value || oldDelegate.scale.value != scale.value;
} }
...@@ -919,6 +956,7 @@ class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements Raster ...@@ -919,6 +956,7 @@ class _ZoomExitTransitionDelegate extends RasterWidgetDelegate implements Raster
_transformHandler.layer = null; _transformHandler.layer = null;
scale.removeListener(notifyListeners); scale.removeListener(notifyListeners);
fade.removeListener(notifyListeners); fade.removeListener(notifyListeners);
animation.removeStatusListener(_onStatusChange);
super.dispose(); super.dispose();
} }
} }
...@@ -19,7 +19,7 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -19,7 +19,7 @@ abstract class PageRoute<T> extends ModalRoute<T> {
PageRoute({ PageRoute({
super.settings, super.settings,
this.fullscreenDialog = false, this.fullscreenDialog = false,
this.preferRasterization = true, this.allowSnapshotting = true,
}); });
/// {@template flutter.widgets.PageRoute.fullscreenDialog} /// {@template flutter.widgets.PageRoute.fullscreenDialog}
...@@ -33,7 +33,7 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -33,7 +33,7 @@ abstract class PageRoute<T> extends ModalRoute<T> {
final bool fullscreenDialog; final bool fullscreenDialog;
@override @override
final bool preferRasterization; final bool allowSnapshotting;
@override @override
bool get opaque => true; bool get opaque => true;
...@@ -80,7 +80,7 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -80,7 +80,7 @@ class PageRouteBuilder<T> extends PageRoute<T> {
this.barrierLabel, this.barrierLabel,
this.maintainState = true, this.maintainState = true,
super.fullscreenDialog, super.fullscreenDialog,
super.preferRasterization = true, super.allowSnapshotting = true,
}) : assert(pageBuilder != null), }) : assert(pageBuilder != null),
assert(transitionsBuilder != null), assert(transitionsBuilder != null),
assert(opaque != null), assert(opaque != null),
......
...@@ -134,20 +134,20 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -134,20 +134,20 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// {@endtemplate} /// {@endtemplate}
bool get opaque; bool get opaque;
/// {@template flutter.widgets.TransitionRoute.preferRasterization} /// {@template flutter.widgets.TransitionRoute.allowSnapshotting}
/// Whether the route transition will prefer to animate a rasterized /// Whether the route transition will prefer to animate a snapshot of the
/// snapshot of the entering/exiting routes. /// entering/exiting routes.
/// ///
/// When this value is true, certain route transitions (such as the Android /// When this value is true, certain route transitions (such as the Android
/// zoom page transition) will rasterize the entering and exiting routes. /// zoom page transition) will snapshot the entering and exiting routes.
/// These textures are then animated in place of the underlying widgets to /// These snapshots are then animated in place of the underlying widgets to
/// improve performance of the transition. /// improve performance of the transition.
/// ///
/// Generally this means that animations that occur on the entering/exiting /// Generally this means that animations that occur on the entering/exiting
/// route while the route animation plays may appear frozen - unless they /// route while the route animation plays may appear frozen - unless they
/// are a hero animation or something that is drawn in a separate overlay. /// are a hero animation or something that is drawn in a separate overlay.
/// {@endtemplate} /// {@endtemplate}
bool get preferRasterization => true; bool get allowSnapshotting => 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.
...@@ -1749,7 +1749,7 @@ abstract class PopupRoute<T> extends ModalRoute<T> { ...@@ -1749,7 +1749,7 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
bool get maintainState => true; bool get maintainState => true;
@override @override
bool get preferRasterization => false; bool get allowSnapshotting => false;
} }
/// A [Navigator] observer that notifies [RouteAware]s of changes to the /// A [Navigator] observer that notifies [RouteAware]s of changes to the
......
...@@ -7,264 +7,162 @@ import 'dart:ui' as ui; ...@@ -7,264 +7,162 @@ import 'dart:ui' as ui;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
/// Controls how the [RasterWidget] paints its children via the [RasterWidgetController]. /// Controls how the [SnapshotWidget] paints its child.
enum RasterizeMode { enum SnapshotMode {
/// the children are rasterized, but only if all descendants can be rasterized. /// The child is snapshotted, but only if all descendants can be snapshotted.
/// ///
/// This setting is the default state of the [RasterWidgetController]. /// If there is a platform view in the children of a snapshot widget, the
/// /// snapshot will not be used and the child will be rendered using
/// If there is a platform view in the children of a raster widget /// [SnapshotPainter.paint]. This uses an un-snapshotted child and by default
/// and a [RasterWidgetFallbackDelegate]h as been provided to the raster_widget, /// paints it with no additional modification.
/// this fallback delegate will be used to render the children instead of the image. permissive,
/// If there is no fallback delegate, an excpetion will be thrown
enabled,
/// The children are rasterized and any child platform views are ignored. /// An error is thrown if the child cannot be snapshotted.
/// ///
/// In this state a [RasterWidgetFallbackDelegate] is never used. Generally this /// This setting is the default state of the [SnapshotWidget].
/// can be useful if there is a platform view descendant that does not need to normal,
/// be included in the raster.
forced,
/// the children are not rasterized and the [RasterWidgetFallbackDelegate], /// The child is snapshotted and any child platform views are ignored.
/// if provided, is used to draw the children.
/// ///
/// /// This mode can be useful if there is a platform view descendant that does
fallback, /// not need to be included in the snapshot.
forced,
} }
/// A controller for the [RasterWidget] that controls when the child image is displayed /// A controller for the [SnapshotWidget] that controls when the child image is displayed
/// and when to regenerated the child image. /// and when to regenerated the child image.
/// ///
/// When the value of [rasterize] is true, the [RasterWidget] will paint the child /// When the value of [allowSnapshotting] is true, the [SnapshotWidget] will paint the child
/// widgets based on the [RasterizeMode] of the raster widget. /// widgets based on the [SnapshotMode] of the snapshot widget.
/// ///
/// To force [RasterWidget] to recreate the child image, call [clear]. /// The controller notifies its listeners when the value of [allowSnapshotting] changes
class RasterWidgetController extends ChangeNotifier { /// or when [clear] is called.
/// Create a new [RasterWidgetController]. ///
/// To force [SnapshotWidget] to recreate the child image, call [clear].
class SnapshotController extends ChangeNotifier {
/// Create a new [SnapshotController].
/// ///
/// By default, [rasterize] is `false` and cannot be `null`. /// By default, [allowSnapshotting] is `false` and cannot be `null`.
RasterWidgetController({ SnapshotController({
bool rasterize = false, bool allowSnapshotting = false,
}) : _rasterize = rasterize; }) : _allowSnapshotting = allowSnapshotting;
/// Reset the raster held by any listening [RasterWidget]. /// Reset the snapshot held by any listening [SnapshotWidget].
/// ///
/// This has no effect if [rasterize] is `false`. /// This has no effect if [allowSnapshotting] is `false`.
void clear() { void clear() {
notifyListeners(); notifyListeners();
} }
/// Whether a rasterized version of this render objects child is drawn in /// Whether a snapshot of this child widget is painted in its place.
/// place of the child. bool get allowSnapshotting => _allowSnapshotting;
bool get rasterize => _rasterize; bool _allowSnapshotting;
bool _rasterize; set allowSnapshotting(bool value) {
set rasterize(bool value) { if (value == allowSnapshotting) {
if (value == rasterize) {
return; return;
} }
_rasterize = value; _allowSnapshotting = value;
notifyListeners(); notifyListeners();
} }
} }
/// A widget that replaces its child with a rasterized version of the child. /// A widget that can replace its child with a snapshoted version of the child.
///
/// A snapshot is a frozen texture-backed representation of all child pictures
/// and layers stored as a [ui.Image].
///
/// This widget is useful for performing short animations that would otherwise
/// be expensive or that cannot rely on raster caching. For example, scale and
/// skew animations are often expensive to perform on complex children, as are
/// blurs. For a short animation, a widget that contains these expensive effects
/// can be replaced with a snapshot of itself and manipulated instead.
/// ///
/// By default, the child is drawn as is. The default [delegate] simply scales /// For example, the Android Q [ZoomPageTransitionsBuilder] uses a snapshot widget
/// down the image by the current device pixel ratio and paints it into the /// for the forward and entering route to avoid the expensive scale animation.
/// canvas. How this image is drawn can be customized by providing a new /// This also has the effect of briefly pausing any animations on the page.
/// subclass of [RasterWidgetDelegate] to the [delegate] argument. ///
/// Generally, this widget should not be used in places where users expect the
/// child widget to continue animating or to be responsive, such as an unbounded
/// animation.
/// ///
/// Caveats: /// Caveats:
/// ///
/// The contents of platform views cannot be captured by a raster /// * The contents of platform views cannot be captured by a snapshot
/// widget. If a platform view is encountered, then the raster widget will /// widget. If a platform view is encountered, then the snapshot widget will
/// determine how to render its children based on the [RasterizeMode]. This /// determine how to render its children based on the [SnapshotMode]. This
/// defaults to [RasterizeMode.enabled] which will throw an exception if a platform /// defaults to [SnapshotMode.normal] which will throw an exception if a
/// view is encountered. /// platform view is encountered.
/// ///
/// This widget is not supported on the HTML backend of Flutter for the web. /// * The snapshotting functionality of this widget is not supported on the HTML
class RasterWidget extends SingleChildRenderObjectWidget { /// backend of Flutter for the Web. Setting [SnapshotController.allowSnapshotting] to true
/// Create a new [RasterWidget]. /// may cause an error to be thrown. On the CanvasKit backend of Flutter, the
/// performance of using this widget may regress performance due to the fact
/// that both the UI and engine share a single thread.
class SnapshotWidget extends SingleChildRenderObjectWidget {
/// Create a new [SnapshotWidget].
/// ///
/// The [controller] and [child] arguments are required. /// The [controller] and [child] arguments are required.
const RasterWidget({ const SnapshotWidget({
super.key, super.key,
this.delegate = const _RasterDefaultDelegate(), this.mode = SnapshotMode.normal,
this.fallback, this.painter = const _DefaultSnapshotPainter(),
this.mode = RasterizeMode.enabled,
required this.controller, required this.controller,
required super.child required super.child
}); });
/// A delegate that allows customization of how the image is painted. /// The controller that determines when to display the children as a snapshot.
/// final SnapshotController controller;
/// 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. /// Configuration that controls how the snapshot widget decides to paint its children.
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 /// Defaults to [SnapshotMode.normal], which throws an error when a platform view
/// or other un-rasterizable view is encountered. /// or texture view is encountered.
/// ///
/// See [RasterizeMode] for more information. /// See [SnapshotMode] for more information.
final RasterizeMode mode; final SnapshotMode mode;
/// The painter used to paint the child snapshot or child widgets.
final SnapshotPainter painter;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return RenderRasterWidget( debugCheckHasMediaQuery(context);
delegate: delegate, return _RenderSnapshotWidget(
controller: controller, controller: controller,
fallback: fallback,
mode: mode, mode: mode,
devicePixelRatio: MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0, devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
painter: painter,
); );
} }
@override @override
void updateRenderObject(BuildContext context, covariant RenderRasterWidget renderObject) { void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
renderObject debugCheckHasMediaQuery(context);
..delegate = delegate (renderObject as _RenderSnapshotWidget)
..controller = controller ..controller = controller
..fallback = fallback
..mode = mode ..mode = mode
..devicePixelRatio = MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0; ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio
..painter = painter;
} }
} }
/// A delegate which the [RasterWidget] can use to fallback to regular rendering // A render object that conditionally converts its child into a [ui.Image]
/// if a platform view is present in the layer tree. // and then paints it in place of the child.
/// class _RenderSnapshotWidget extends RenderProxyBox {
/// Consumers of [RasterWidget] should almost never use this delegate. For the most part, // Create a new [_RenderSnapshotWidget].
/// the raster widget only functions as a performance improvement. If a platform view is _RenderSnapshotWidget({
/// 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 double devicePixelRatio,
required RasterWidgetController controller, required SnapshotController controller,
required RasterizeMode mode, required SnapshotMode mode,
RasterWidgetFallbackDelegate? fallback, required SnapshotPainter painter,
}) : _delegate = delegate, }) : _devicePixelRatio = devicePixelRatio,
_devicePixelRatio = devicePixelRatio,
_controller = controller, _controller = controller,
_fallback = fallback, _mode = mode,
_mode = mode; _painter = painter;
/// The device pixel ratio used to create the child image. /// The device pixel ratio used to create the child image.
double get devicePixelRatio => _devicePixelRatio; double get devicePixelRatio => _devicePixelRatio;
...@@ -274,61 +172,57 @@ class RenderRasterWidget extends RenderProxyBox { ...@@ -274,61 +172,57 @@ class RenderRasterWidget extends RenderProxyBox {
return; return;
} }
_devicePixelRatio = value; _devicePixelRatio = value;
markNeedsPaint(); if (_childRaster == null) {
return;
} else {
_childRaster?.dispose();
_childRaster = null;
markNeedsPaint();
}
} }
/// Whether a rasterized version of this render objects child is drawn in /// The painter used to paint the child snapshot or child widgets.
/// place of the child. SnapshotPainter get painter => _painter;
RasterWidgetController get controller => _controller; SnapshotPainter _painter;
RasterWidgetController _controller; set painter(SnapshotPainter value) {
set controller(RasterWidgetController value) { if (value == painter) {
if (value == controller) {
return; return;
} }
controller.removeListener(_onRasterValueChanged); final SnapshotPainter oldPainter = painter;
final bool oldValue = controller.rasterize; oldPainter.removeListener(markNeedsPaint);
_controller = value; _painter = value;
if (oldPainter.runtimeType != painter.runtimeType ||
painter.shouldRepaint(oldPainter)) {
markNeedsPaint();
}
if (attached) { if (attached) {
controller.addListener(_onRasterValueChanged); painter.addListener(markNeedsPaint);
if (oldValue != controller.rasterize) {
_onRasterValueChanged();
}
} }
} }
/// The delegate used to draw the image representing the child. /// A controller that determines whether to paint the child normally or to
RasterWidgetDelegate get delegate => _delegate; /// paint a snapshotted version of that child.
RasterWidgetDelegate _delegate; SnapshotController get controller => _controller;
set delegate(RasterWidgetDelegate value) { SnapshotController _controller;
if (value == delegate) { set controller(SnapshotController value) {
if (value == controller) {
return; return;
} }
delegate.removeListener(markNeedsPaint); controller.removeListener(_onRasterValueChanged);
final RasterWidgetDelegate oldDelegate = _delegate; final bool oldValue = controller.allowSnapshotting;
_delegate = value; _controller = value;
if (attached) { if (attached) {
delegate.addListener(markNeedsPaint); controller.addListener(_onRasterValueChanged);
if (delegate.shouldRepaint(oldDelegate)) { if (oldValue != controller.allowSnapshotting) {
markNeedsPaint(); _onRasterValueChanged();
} }
} }
} }
/// A fallback delegate which is used if the child layers contains a platform view. /// How the snapshot widget will handle platform views in child layers.
RasterWidgetFallbackDelegate? get fallback => _fallback; SnapshotMode get mode => _mode;
RasterWidgetFallbackDelegate? _fallback; SnapshotMode _mode;
set fallback(RasterWidgetFallbackDelegate? value) { set mode(SnapshotMode 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) { if (value == _mode) {
return; return;
} }
...@@ -337,18 +231,22 @@ class RenderRasterWidget extends RenderProxyBox { ...@@ -337,18 +231,22 @@ class RenderRasterWidget extends RenderProxyBox {
} }
ui.Image? _childRaster; ui.Image? _childRaster;
// Set to true if the snapshot mode was not forced and a platform view
// was encountered while attempting to snapshot the child.
bool _disableSnapshotAttempt = false;
@override @override
void attach(covariant PipelineOwner owner) { void attach(covariant PipelineOwner owner) {
delegate.addListener(markNeedsPaint);
controller.addListener(_onRasterValueChanged); controller.addListener(_onRasterValueChanged);
painter.addListener(markNeedsPaint);
super.attach(owner); super.attach(owner);
} }
@override @override
void detach() { void detach() {
delegate.removeListener(markNeedsPaint); _disableSnapshotAttempt = false;
controller.removeListener(_onRasterValueChanged); controller.removeListener(_onRasterValueChanged);
painter.removeListener(markNeedsPaint);
_childRaster?.dispose(); _childRaster?.dispose();
_childRaster = null; _childRaster = null;
super.detach(); super.detach();
...@@ -356,22 +254,20 @@ class RenderRasterWidget extends RenderProxyBox { ...@@ -356,22 +254,20 @@ class RenderRasterWidget extends RenderProxyBox {
@override @override
void dispose() { void dispose() {
delegate.removeListener(markNeedsPaint);
controller.removeListener(_onRasterValueChanged); controller.removeListener(_onRasterValueChanged);
painter.removeListener(markNeedsPaint);
_childRaster?.dispose(); _childRaster?.dispose();
_childRaster = null; _childRaster = null;
super.dispose(); super.dispose();
} }
void _onRasterValueChanged() { void _onRasterValueChanged() {
_disableSnapshotAttempt = false;
_childRaster?.dispose(); _childRaster?.dispose();
_childRaster = null; _childRaster = null;
markNeedsPaint(); 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 // Paint [child] with this painting context, then convert to a raster and detach all
// children from this layer. // children from this layer.
ui.Image? _paintAndDetachToImage() { ui.Image? _paintAndDetachToImage() {
...@@ -383,15 +279,11 @@ class RenderRasterWidget extends RenderProxyBox { ...@@ -383,15 +279,11 @@ class RenderRasterWidget extends RenderProxyBox {
// that would conflict with our goals of minimizing painting context. // that would conflict with our goals of minimizing painting context.
// ignore: invalid_use_of_protected_member // ignore: invalid_use_of_protected_member
context.stopRecordingIfNeeded(); context.stopRecordingIfNeeded();
if (mode != RasterizeMode.forced && !offsetLayer.supportsRasterization()) { if (mode != SnapshotMode.forced && !offsetLayer.supportsRasterization()) {
_hitPlatformView = true; if (mode == SnapshotMode.normal) {
if (fallback == null) { throw FlutterError('SnapshotWidget used with a child that contains a PlatformView.');
assert(() {
throw FlutterError(
'RasterWidget used with a child that contains a PlatformView.'
);
}());
} }
_disableSnapshotAttempt = true;
return null; return null;
} }
final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio); final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio);
...@@ -406,40 +298,117 @@ class RenderRasterWidget extends RenderProxyBox { ...@@ -406,40 +298,117 @@ class RenderRasterWidget extends RenderProxyBox {
_childRaster = null; _childRaster = null;
return; return;
} }
if (controller.rasterize) { if (!controller.allowSnapshotting || _disableSnapshotAttempt) {
if (_useFallback) { _childRaster?.dispose();
fallback?.paintFallback(context, offset, size, super.paint); _childRaster = null;
} else { painter.paint(context, offset, size, super.paint);
_childRaster ??= _paintAndDetachToImage();
if (_childRaster == null && _useFallback) {
fallback?.paintFallback(context, offset, size, super.paint);
} else {
delegate.paint(context, offset, size, _childRaster!, devicePixelRatio);
}
}
return; return;
} }
_childRaster?.dispose(); _childRaster ??= _paintAndDetachToImage();
_childRaster = null; if (_childRaster == null) {
super.paint(context, offset); painter.paint(context, offset, size, super.paint);
} else {
painter.paintSnapshot(context, offset, size, _childRaster!, devicePixelRatio);
}
return;
} }
} }
// A delegate that paints the child widget as is. /// A painter used to paint either a snapshot or the child widgets that
class _RasterDefaultDelegate implements RasterWidgetDelegate { /// would be a snapshot.
const _RasterDefaultDelegate(); ///
/// The painter can call [notifyListeners] to have the [SnapshotWidget]
/// re-paint (re-using the same raster). This allows animations to be performed
/// without re-snapshotting 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 [SnapshotWidget] 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 SnapshotPainter extends ChangeNotifier {
/// Called whenever the [image] that represents a [SnapshotWidget]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.
///
/// {@tool snippet}
///
/// The following method shows how the default implementation of the delegate used by the
/// [SnapshotPainter] paints the snapshot. 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 paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio);
/// Paint the child via [painter], applying any effects that would have been painted
/// in [SnapshotPainter.paintSnapshot].
///
/// This method is called when snapshotting is disabled, or when [SnapshotMode.permissive]
/// is used and a child platform view prevents snapshotting.
///
/// The [offset] and [size] are the location and dimensions of the render object.
void paint(PaintingContext context, Offset offset, Size size, PaintingContextCallback painter);
@override /// Called whenever a new instance of the snapshot widget delegate class is
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) { /// provided to the [SnapshotWidget] object, or any time that a new
final Rect src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); /// [SnapshotPainter] object is created with a new instance of the
final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); /// delegate class (which amounts to the same thing, because the latter is
final Paint paint = Paint() /// implemented in terms of the former).
..filterQuality = FilterQuality.low; ///
context.canvas.drawImageRect(image, src, dst, paint); /// 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
/// [SnapshotWidget] to be updated. Instead, [SnapshotController.clear] can
/// be used to force the generation of a new image.
///
/// The `oldPainter` argument will never be null.
bool shouldRepaint(covariant SnapshotPainter oldPainter);
}
@override class _DefaultSnapshotPainter implements SnapshotPainter {
bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate) => false; const _DefaultSnapshotPainter();
@override @override
void addListener(ui.VoidCallback listener) { } void addListener(ui.VoidCallback listener) { }
...@@ -453,6 +422,23 @@ class _RasterDefaultDelegate implements RasterWidgetDelegate { ...@@ -453,6 +422,23 @@ class _RasterDefaultDelegate implements RasterWidgetDelegate {
@override @override
void notifyListeners() { } void notifyListeners() { }
@override
void paint(PaintingContext context, ui.Offset offset, ui.Size size, PaintingContextCallback painter) {
painter(context, offset);
}
@override
void paintSnapshot(PaintingContext context, ui.Offset offset, ui.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 @override
void removeListener(ui.VoidCallback listener) { } void removeListener(ui.VoidCallback listener) { }
@override
bool shouldRepaint(covariant _DefaultSnapshotPainter oldPainter) => false;
} }
...@@ -91,7 +91,6 @@ export 'src/widgets/platform_selectable_region_context_menu.dart'; ...@@ -91,7 +91,6 @@ export 'src/widgets/platform_selectable_region_context_menu.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';
...@@ -127,6 +126,7 @@ export 'src/widgets/sliver_layout_builder.dart'; ...@@ -127,6 +126,7 @@ export 'src/widgets/sliver_layout_builder.dart';
export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_persistent_header.dart';
export 'src/widgets/sliver_prototype_extent_list.dart'; export 'src/widgets/sliver_prototype_extent_list.dart';
export 'src/widgets/slotted_render_object_widget.dart'; export 'src/widgets/slotted_render_object_widget.dart';
export 'src/widgets/snapshot_widget.dart';
export 'src/widgets/spacer.dart'; export 'src/widgets/spacer.dart';
export 'src/widgets/spell_check.dart'; export 'src/widgets/spell_check.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
......
...@@ -399,18 +399,21 @@ void main() { ...@@ -399,18 +399,21 @@ void main() {
Widget build() { Widget build() {
return Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Navigator( child: MediaQuery(
initialRoute: '/', data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
onGenerateRoute: (RouteSettings settings) { child: Navigator(
return MaterialPageRoute<void>( initialRoute: '/',
settings: settings, onGenerateRoute: (RouteSettings settings) {
builder: (BuildContext context) { return MaterialPageRoute<void>(
return Material( settings: settings,
child: buildFrame(value: 'one', onChanged: didChangeValue), builder: (BuildContext context) {
); return Material(
}, child: buildFrame(value: 'one', onChanged: didChangeValue),
); );
}, },
);
},
),
), ),
); );
} }
......
...@@ -158,7 +158,7 @@ void main() { ...@@ -158,7 +158,7 @@ void main() {
testWidgets('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async { testWidgets('test page transition (_ZoomPageTransition) without rasterization', (WidgetTester tester) async {
Iterable<Layer> findLayers(Finder of) { Iterable<Layer> findLayers(Finder of) {
return tester.layerListOf( return tester.layerListOf(
find.ancestor(of: of, matching: find.byType(RasterWidget)).first, find.ancestor(of: of, matching: find.byType(SnapshotWidget)).first,
); );
} }
...@@ -174,7 +174,7 @@ void main() { ...@@ -174,7 +174,7 @@ void main() {
MaterialApp( MaterialApp(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
preferRasterization: false, allowSnapshotting: false,
builder: (BuildContext context) { builder: (BuildContext context) {
if (settings.name == '/') { if (settings.name == '/') {
return const Material(child: Text('Page 1')); return const Material(child: Text('Page 1'));
...@@ -1004,8 +1004,7 @@ void main() { ...@@ -1004,8 +1004,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
RootRestorationScope( RootRestorationScope(
restorationId: 'root', restorationId: 'root',
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
onPopPage: (Route<dynamic> route, dynamic result) { return false; }, onPopPage: (Route<dynamic> route, dynamic result) { return false; },
pages: const <Page<Object?>>[ pages: const <Page<Object?>>[
...@@ -1177,3 +1176,20 @@ class _TestRestorableWidgetState extends State<TestRestorableWidget> with Restor ...@@ -1177,3 +1176,20 @@ class _TestRestorableWidgetState extends State<TestRestorableWidget> with Restor
); );
} }
} }
class TestDependencies extends StatelessWidget {
const TestDependencies({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: child,
),
);
}
}
...@@ -1821,7 +1821,7 @@ void main() { ...@@ -1821,7 +1821,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(RasterWidget), of: find.byType(SnapshotWidget),
matching: find.byType(SizedBox), matching: find.byType(SizedBox),
).first); ).first);
expect(container.size, Size.zero); expect(container.size, Size.zero);
......
...@@ -327,8 +327,7 @@ Future<void> main() async { ...@@ -327,8 +327,7 @@ Future<void> main() async {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: HeroController(), controller: HeroController(),
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key, key: key,
initialRoute: 'navigator1', initialRoute: 'navigator1',
...@@ -382,8 +381,7 @@ Future<void> main() async { ...@@ -382,8 +381,7 @@ Future<void> main() async {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: HeroController(), controller: HeroController(),
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key, key: key,
initialRoute: 'navigator1', initialRoute: 'navigator1',
...@@ -3160,3 +3158,20 @@ Future<void> main() async { ...@@ -3160,3 +3158,20 @@ Future<void> main() async {
}, },
); );
} }
class TestDependencies extends StatelessWidget {
const TestDependencies({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: child,
),
);
}
}
...@@ -1042,9 +1042,12 @@ class PagedTestWidget extends StatelessWidget { ...@@ -1042,9 +1042,12 @@ class PagedTestWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RootRestorationScope( return RootRestorationScope(
restorationId: restorationId, restorationId: restorationId,
child: const Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: PagedTestNavigator(), child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: const PagedTestNavigator(),
),
), ),
); );
} }
...@@ -1167,20 +1170,23 @@ class TestWidget extends StatelessWidget { ...@@ -1167,20 +1170,23 @@ class TestWidget extends StatelessWidget {
restorationId: restorationId, restorationId: restorationId,
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Navigator( child: MediaQuery(
initialRoute: 'home', data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
restorationScopeId: 'app', child: Navigator(
onGenerateRoute: (RouteSettings settings) { initialRoute: 'home',
return MaterialPageRoute<int>( restorationScopeId: 'app',
settings: settings, onGenerateRoute: (RouteSettings settings) {
builder: (BuildContext context) { return MaterialPageRoute<int>(
return RouteWidget( settings: settings,
name: settings.name!, builder: (BuildContext context) {
arguments: settings.arguments, return RouteWidget(
); name: settings.name!,
}, arguments: settings.arguments,
); );
}, },
);
},
),
), ),
), ),
); );
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// 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';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
...@@ -201,8 +199,7 @@ void main() { ...@@ -201,8 +199,7 @@ void main() {
}; };
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: navigator, key: navigator,
pages: pages, pages: pages,
...@@ -566,8 +563,7 @@ void main() { ...@@ -566,8 +563,7 @@ void main() {
} }
final TabController controller = TabController(length: 3, vsync: tester); final TabController controller = TabController(length: 3, vsync: tester);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: TabBarView( child: TabBarView(
controller: controller, controller: controller,
children: <Widget>[ children: <Widget>[
...@@ -598,8 +594,7 @@ void main() { ...@@ -598,8 +594,7 @@ void main() {
); );
} }
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: buildNavigator(), child: buildNavigator(),
), ),
); );
...@@ -608,8 +603,7 @@ void main() { ...@@ -608,8 +603,7 @@ void main() {
pages.add(const MaterialPage<void>(child: Text('Page 2'))); pages.add(const MaterialPage<void>(child: Text('Page 2')));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: buildNavigator(), child: buildNavigator(),
), ),
); );
...@@ -641,8 +635,7 @@ void main() { ...@@ -641,8 +635,7 @@ void main() {
); );
} }
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: buildNavigator(), child: buildNavigator(),
), ),
); );
...@@ -659,8 +652,7 @@ void main() { ...@@ -659,8 +652,7 @@ void main() {
]; ];
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: buildNavigator(), child: buildNavigator(),
), ),
); );
...@@ -1904,8 +1896,7 @@ void main() { ...@@ -1904,8 +1896,7 @@ void main() {
testWidgets('initial routes below opaque route are offstage', (WidgetTester tester) async { testWidgets('initial routes below opaque route are offstage', (WidgetTester tester) async {
final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: testKey, key: testKey,
initialRoute: '/a/b', initialRoute: '/a/b',
...@@ -1947,8 +1938,7 @@ void main() { ...@@ -1947,8 +1938,7 @@ void main() {
bool onGenerateInitialRoutesCalled = false; bool onGenerateInitialRoutesCalled = false;
final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> testKey = GlobalKey<NavigatorState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: testKey, key: testKey,
initialRoute: 'Hello World', initialRoute: 'Hello World',
...@@ -2074,8 +2064,7 @@ void main() { ...@@ -2074,8 +2064,7 @@ void main() {
final Map<String, MaterialPageRoute<dynamic>> routeNameToContext = <String, MaterialPageRoute<dynamic>>{}; final Map<String, MaterialPageRoute<dynamic>> routeNameToContext = <String, MaterialPageRoute<dynamic>>{};
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: navigator, key: navigator,
initialRoute: 'root', initialRoute: 'root',
...@@ -2154,8 +2143,7 @@ void main() { ...@@ -2154,8 +2143,7 @@ void main() {
Widget widgetUnderTest({required bool enabled}) { Widget widgetUnderTest({required bool enabled}) {
return TickerMode( return TickerMode(
enabled: enabled, enabled: enabled,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
initialRoute: 'root', initialRoute: 'root',
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
...@@ -2261,8 +2249,7 @@ void main() { ...@@ -2261,8 +2249,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: spy, controller: spy,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: top, key: top,
initialRoute: 'top1', initialRoute: 'top1',
...@@ -2332,8 +2319,7 @@ void main() { ...@@ -2332,8 +2319,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: spy, controller: spy,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key1, key: key1,
initialRoute: 'navigator1', initialRoute: 'navigator1',
...@@ -2353,8 +2339,7 @@ void main() { ...@@ -2353,8 +2339,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: spy, controller: spy,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key2, key: key2,
initialRoute: 'navigator2', initialRoute: 'navigator2',
...@@ -2412,8 +2397,7 @@ void main() { ...@@ -2412,8 +2397,7 @@ void main() {
); );
}; };
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
HeroControllerScope( HeroControllerScope(
...@@ -2459,8 +2443,7 @@ void main() { ...@@ -2459,8 +2443,7 @@ void main() {
// Swaps the spies. // Swaps the spies.
await tester.pumpWidget( await tester.pumpWidget(
Directionality( TestDependencies(
textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
HeroControllerScope( HeroControllerScope(
...@@ -2532,8 +2515,7 @@ void main() { ...@@ -2532,8 +2515,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: spy, controller: spy,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Navigator( Navigator(
...@@ -2571,8 +2553,7 @@ void main() { ...@@ -2571,8 +2553,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
HeroControllerScope( HeroControllerScope(
controller: spy, controller: spy,
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Navigator( Navigator(
...@@ -2637,8 +2618,7 @@ void main() { ...@@ -2637,8 +2618,7 @@ void main() {
DefaultMaterialLocalizations.delegate, DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate, DefaultWidgetsLocalizations.delegate,
], ],
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key, key: key,
pages: pages, pages: pages,
...@@ -2736,8 +2716,7 @@ void main() { ...@@ -2736,8 +2716,7 @@ void main() {
DefaultMaterialLocalizations.delegate, DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate, DefaultWidgetsLocalizations.delegate,
], ],
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
pages: myPages, pages: myPages,
), ),
...@@ -2832,8 +2811,7 @@ void main() { ...@@ -2832,8 +2811,7 @@ void main() {
DefaultMaterialLocalizations.delegate, DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate, DefaultWidgetsLocalizations.delegate,
], ],
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
pages: myPages, pages: myPages,
), ),
...@@ -3086,8 +3064,7 @@ void main() { ...@@ -3086,8 +3064,7 @@ void main() {
testWidgets('Pop no animation page does not crash', (WidgetTester tester) async { testWidgets('Pop no animation page does not crash', (WidgetTester tester) async {
// Regression Test for https://github.com/flutter/flutter/issues/86604. // Regression Test for https://github.com/flutter/flutter/issues/86604.
Widget buildNavigator(bool secondPage) { Widget buildNavigator(bool secondPage) {
return Directionality( return TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
pages: <Page<void>>[ pages: <Page<void>>[
const ZeroDurationPage( const ZeroDurationPage(
...@@ -3691,8 +3668,7 @@ void main() { ...@@ -3691,8 +3668,7 @@ void main() {
testWidgets('Can reuse NavigatorObserver in rebuilt tree', (WidgetTester tester) async { testWidgets('Can reuse NavigatorObserver in rebuilt tree', (WidgetTester tester) async {
final NavigatorObserver observer = NavigatorObserver(); final NavigatorObserver observer = NavigatorObserver();
Widget build([Key? key]) { Widget build([Key? key]) {
return Directionality( return TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key, key: key,
observers: <NavigatorObserver>[observer], observers: <NavigatorObserver>[observer],
...@@ -3869,8 +3845,7 @@ void main() { ...@@ -3869,8 +3845,7 @@ void main() {
testWidgets('class implementing NavigatorObserver can be used without problems', (WidgetTester tester) async { testWidgets('class implementing NavigatorObserver can be used without problems', (WidgetTester tester) async {
final _MockNavigatorObserver observer = _MockNavigatorObserver(); final _MockNavigatorObserver observer = _MockNavigatorObserver();
Widget build([Key? key]) { Widget build([Key? key]) {
return Directionality( return TestDependencies(
textDirection: TextDirection.ltr,
child: Navigator( child: Navigator(
key: key, key: key,
observers: <NavigatorObserver>[observer], observers: <NavigatorObserver>[observer],
...@@ -4122,7 +4097,7 @@ class ZeroDurationPage extends Page<void> { ...@@ -4122,7 +4097,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, preferRasterization: false); : super(settings: page, allowSnapshotting: false);
@override @override
Duration get transitionDuration => Duration.zero; Duration get transitionDuration => Duration.zero;
...@@ -4163,3 +4138,20 @@ class _MockNavigatorObserver implements NavigatorObserver { ...@@ -4163,3 +4138,20 @@ class _MockNavigatorObserver implements NavigatorObserver {
return null; return null;
} }
} }
class TestDependencies extends StatelessWidget {
const TestDependencies({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Directionality(
textDirection: TextDirection.ltr,
child: child,
)
);
}
}
...@@ -33,24 +33,27 @@ Future<void> performTest(WidgetTester tester, bool maintainState) async { ...@@ -33,24 +33,27 @@ Future<void> performTest(WidgetTester tester, bool maintainState) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Navigator( child: MediaQuery(
key: navigatorKey, data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
onGenerateRoute: (RouteSettings settings) { child: Navigator(
if (settings.name == '/') { key: navigatorKey,
return MaterialPageRoute<void>( onGenerateRoute: (RouteSettings settings) {
settings: settings, if (settings.name == '/') {
builder: (_) => const ThePositiveNumbers(from: 0), return MaterialPageRoute<void>(
maintainState: maintainState, settings: settings,
); builder: (_) => const ThePositiveNumbers(from: 0),
} else if (settings.name == '/second') { maintainState: maintainState,
return MaterialPageRoute<void>( );
settings: settings, } else if (settings.name == '/second') {
builder: (_) => const ThePositiveNumbers(from: 10000), return MaterialPageRoute<void>(
maintainState: maintainState, settings: settings,
); builder: (_) => const ThePositiveNumbers(from: 10000),
} maintainState: maintainState,
return null; );
}, }
return null;
},
),
), ),
), ),
); );
......
...@@ -14,13 +14,13 @@ import 'package:flutter/widgets.dart'; ...@@ -14,13 +14,13 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('RasterWidget can rasterize child', (WidgetTester tester) async { testWidgets('SnapshotWidget can rasterize child', (WidgetTester tester) async {
final RasterWidgetController controller = RasterWidgetController(rasterize: true); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final Key key = UniqueKey(); final Key key = UniqueKey();
await tester.pumpWidget(RepaintBoundary( await tester.pumpWidget(RepaintBoundary(
key: key, key: key,
child: Center( child: TestDependencies(
child: RasterWidget( child: SnapshotWidget(
controller: controller, controller: controller,
child: Container( child: Container(
width: 100, width: 100,
...@@ -30,151 +30,107 @@ void main() { ...@@ -30,151 +30,107 @@ void main() {
), ),
), ),
)); ));
// 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')); 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 { // Now change the color and assert the old snapshot still matches.
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
await tester.pumpWidget(RepaintBoundary( await tester.pumpWidget(RepaintBoundary(
child: Center( key: key,
child: RasterWidget( child: TestDependencies(
child: SnapshotWidget(
controller: controller, controller: controller,
child: Container( child: Container(
width: 100, width: 100,
height: 100, height: 100,
color: const Color(0xFFAABB11), color: const Color(0xFFAA0000),
), ),
), ),
), ),
)); ));
await expectLater(find.byKey(key), matchesGoldenFile('raster_widget.yellow.png'));
expect(tester.layers, hasLength(3)); // Now invoke clear and the raster is re-generated.
expect(tester.layers.last, isA<PictureLayer>()); controller.clear();
controller.rasterize = false;
await tester.pump(); await tester.pump();
expect(tester.layers, hasLength(3)); await expectLater(find.byKey(key), matchesGoldenFile('raster_widget.red.png'));
expect(tester.layers.last, isA<PictureLayer>());
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget repaints when RasterWidgetDelegate notifies listeners', (WidgetTester tester) async { testWidgets('Changing devicePixelRatio does not repaint if snapshotting is not enabled', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate(); final SnapshotController controller = SnapshotController();
final RasterWidgetController controller = RasterWidgetController(rasterize: true); final TestPainter painter = TestPainter();
await tester.pumpWidget(RepaintBoundary( double devicePixelRatio = 1.0;
child: Center( late StateSetter localSetState;
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 { await tester.pumpWidget(
final TestDelegate delegate = TestDelegate(); StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
final RasterWidgetController controller = RasterWidgetController(rasterize: true); localSetState = setState;
await tester.pumpWidget(RepaintBoundary( return Center(
child: Center( child: TestDependencies(
child: RasterWidget( devicePixelRatio: devicePixelRatio,
delegate: delegate, child: SnapshotWidget(
controller: controller, controller: controller,
child: Container( painter: painter,
width: 100, child: const SizedBox(width: 100, height: 100),
height: 100, ),
color: const Color(0xFFAABB11),
), ),
), );
), }),
)); );
expect(delegate.lastImage, isNotNull); expect(painter.count, 1);
final ui.Image lastImage = delegate.lastImage!;
localSetState(() {
devicePixelRatio = 2.0;
});
await tester.pump(); await tester.pump();
// Raster is re-used // Not repainted as dpr was not used.
expect(lastImage, equals(delegate.lastImage)); expect(painter.count, 1);
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 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget can update the delegate', (WidgetTester tester) async { testWidgets('Changing devicePixelRatio forces raster regeneration', (WidgetTester tester) async {
final TestDelegate delegateA = TestDelegate(); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final TestDelegate delegateB = TestDelegate() final TestPainter painter = TestPainter();
..shouldRepaintValue = true; double devicePixelRatio = 1.0;
TestDelegate delegate = delegateA; late StateSetter localSetState;
final RasterWidgetController controller = RasterWidgetController(rasterize: true); await tester.pumpWidget(
late void Function(void Function()) setStateFn; StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
await tester.pumpWidget(StatefulBuilder( localSetState = setState;
builder: (BuildContext context, void Function(void Function()) setState) {
setStateFn = setState;
return Center( return Center(
child: RasterWidget( child: TestDependencies(
delegate: delegate, devicePixelRatio: devicePixelRatio,
controller: controller, child: SnapshotWidget(
child: Container( controller: controller,
width: 100, painter: painter,
height: 100, child: const SizedBox(width: 100, height: 100),
color: const Color(0xFFAABB11),
), ),
), ),
); );
}) }),
); );
final ui.Image? raster = painter.lastImage;
expect(raster, isNotNull);
expect(painter.count, 1);
expect(delegateA.count, 1); localSetState(() {
expect(delegateB.count, 0); devicePixelRatio = 2.0;
setStateFn(() {
delegate = delegateB;
}); });
await tester.pump(); await tester.pump();
expect(delegateA.count, 1); final ui.Image? newRaster = painter.lastImage;
expect(delegateB.count, 1);
expect(painter.count, 2);
expect(raster, isNot(newRaster));
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RasterWidget can update the ValueNotifier', (WidgetTester tester) async { testWidgets('SnapshotWidget paints its child as a single picture layer', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate(); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final RasterWidgetController controllerA = RasterWidgetController(rasterize: true); await tester.pumpWidget(RepaintBoundary(
final RasterWidgetController controllerB = RasterWidgetController(); child: Center(
RasterWidgetController controller = controllerA; child: TestDependencies(
late void Function(void Function()) setStateFn; child: SnapshotWidget(
await tester.pumpWidget(StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
setStateFn = setState;
return Center(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
child: Container( child: Container(
width: 100, width: 100,
...@@ -182,155 +138,73 @@ void main() { ...@@ -182,155 +138,73 @@ void main() {
color: const Color(0xFFAABB11), 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, hasLength(3));
expect(tester.layers.last, isA<PictureLayer>()); expect(tester.layers.last, isA<PictureLayer>());
// changes to old notifier do not impact widget. controller.allowSnapshotting = false;
controllerA.rasterize = false;
await tester.pump(); await tester.pump();
expect(delegate.count, 1); expect(tester.layers, hasLength(3));
expect(tester.layers.last, isA<PictureLayer>()); 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 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget correctly attaches and detaches delegate callbacks', (WidgetTester tester) async { testWidgets('SnapshotWidget can update the painter type', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate(); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final RasterWidgetController controller = RasterWidgetController(rasterize: true); await tester.pumpWidget(
final RenderRasterWidget rasterWidget = RenderRasterWidget( Center(
delegate: delegate, child: TestDependencies(
controller: controller, child: SnapshotWidget(
devicePixelRatio: 1.0, controller: controller,
mode: RasterizeMode.enabled, painter: TestPainter(),
); child: const SizedBox(),
),
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( await tester.pumpWidget(
Center( Center(
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr, child: SnapshotWidget(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
painter: TestPainter2(),
child: const SizedBox(), 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 { expect(tester.takeException(), isNull);
final TestDelegate delegate = TestDelegate(); }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
testWidgets('RenderSnapshotWidget does not error on rasterization of child with empty size', (WidgetTester tester) async {
final SnapshotController controller = SnapshotController(allowSnapshotting: true);
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr, child: SnapshotWidget(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
child: const SizedBox( child: const SizedBox(),
width: 100,
height: 100,
child: TestPlatformView(),
),
), ),
), ),
), ),
); );
expect(tester.takeException(), isA<FlutterError>() expect(tester.takeException(), isNull);
.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 }, 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);
testWidgets('RenderSnapshotWidget throws assertion if platform view is encountered', (WidgetTester tester) async {
final SnapshotController controller = SnapshotController(allowSnapshotting: true);
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr, child: SnapshotWidget(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
mode: RasterizeMode.forced,
child: const SizedBox( child: const SizedBox(
width: 100, width: 100,
height: 100, height: 100,
...@@ -341,21 +215,18 @@ void main() { ...@@ -341,21 +215,18 @@ void main() {
), ),
); );
expect(tester.takeException(), isNull); expect(tester.takeException(), isA<FlutterError>()
.having((FlutterError error) => error.message, 'message', contains('SnapshotWidget used with a child that contains a PlatformView')));
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget fallbacks to delegate if PlatformView is present', (WidgetTester tester) async { testWidgets('RenderSnapshotWidget does not assert if SnapshotMode.forced', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate(); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final TestFallback fallback = TestFallback();
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr, child: SnapshotWidget(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
fallback: fallback, mode: SnapshotMode.forced,
child: const SizedBox( child: const SizedBox(
width: 100, width: 100,
height: 100, height: 100,
...@@ -366,48 +237,33 @@ void main() { ...@@ -366,48 +237,33 @@ void main() {
), ),
); );
expect(fallback.calledFallback, 1); expect(tester.takeException(), isNull);
expect(delegate.count, 0);
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
testWidgets('RenderRasterWidget fallbacks to delegate if mode: RasterizeMode.fallback', (WidgetTester tester) async { testWidgets('RenderSnapshotWidget does not take a snapshot if a platform view is encounted with SnapshotMode.permissive', (WidgetTester tester) async {
final TestDelegate delegate = TestDelegate(); final SnapshotController controller = SnapshotController(allowSnapshotting: true);
final RasterWidgetController controller = RasterWidgetController(rasterize: true);
final TestFallback fallback = TestFallback();
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: Directionality( child: TestDependencies(
textDirection: TextDirection.ltr, child: SnapshotWidget(
child: RasterWidget(
delegate: delegate,
controller: controller, controller: controller,
fallback: fallback, mode: SnapshotMode.permissive,
mode: RasterizeMode.fallback,
child: const SizedBox( child: const SizedBox(
width: 100, width: 100,
height: 100, height: 100,
child: TestPlatformView(),
), ),
), ),
), ),
), ),
); );
expect(fallback.calledFallback, 1); expect(tester.takeException(), isNull);
expect(delegate.count, 0); expect(tester.layers.last, isA<PlatformViewLayer>());
}, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689 }, skip: kIsWeb); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/106689
} }
class TestFallback extends RasterWidgetFallbackDelegate { class TestController extends SnapshotController {
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 addedListenerCount = 0;
int removedListenerCount = 0; int removedListenerCount = 0;
...@@ -424,7 +280,23 @@ class TestController extends RasterWidgetController { ...@@ -424,7 +280,23 @@ class TestController extends RasterWidgetController {
} }
} }
class TestDelegate extends RasterWidgetDelegate { 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));
}
}
class TestPainter extends SnapshotPainter {
int count = 0; int count = 0;
bool shouldRepaintValue = false; bool shouldRepaintValue = false;
ui.Image? lastImage; ui.Image? lastImage;
...@@ -449,29 +321,40 @@ class TestDelegate extends RasterWidgetDelegate { ...@@ -449,29 +321,40 @@ class TestDelegate extends RasterWidgetDelegate {
} }
@override @override
void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) { void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
count += 1; count += 1;
lastImage = image; lastImage = image;
} }
@override @override
bool shouldRepaint(covariant RasterWidgetDelegate oldDelegate) => shouldRepaintValue; void paint(PaintingContext context, ui.Offset offset, ui.Size size, PaintingContextCallback painter) {
} count += 1;
}
class TestPlatformView extends SingleChildRenderObjectWidget { @override
const TestPlatformView({super.key}); bool shouldRepaint(covariant TestPainter oldDelegate) => shouldRepaintValue;
}
class TestPainter2 extends TestPainter {
@override @override
RenderObject createRenderObject(BuildContext context) { bool shouldRepaint(covariant TestPainter2 oldDelegate) => shouldRepaintValue;
return RenderTestPlatformView();
}
} }
class RenderTestPlatformView extends RenderProxyBox { class TestDependencies extends StatelessWidget {
const TestDependencies({required this.child, super.key, this.devicePixelRatio});
final Widget child;
final double? devicePixelRatio;
@override @override
void paint(PaintingContext context, ui.Offset offset) { Widget build(BuildContext context) {
context.addLayer(PlatformViewLayer(rect: offset & size, viewId: 1)); return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window)
.copyWith(devicePixelRatio: devicePixelRatio),
child: child,
),
);
} }
} }
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