// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'media_query.dart'; /// Controls how the [SnapshotWidget] paints its child. enum SnapshotMode { /// The child is snapshotted, but only if all descendants can be snapshotted. /// /// 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 /// [SnapshotPainter.paint]. This uses an un-snapshotted child and by default /// paints it with no additional modification. permissive, /// An error is thrown if the child cannot be snapshotted. /// /// This setting is the default state of the [SnapshotWidget]. normal, /// The child is snapshotted and any child platform views are ignored. /// /// This mode can be useful if there is a platform view descendant that does /// not need to be included in the snapshot. forced, } /// A controller for the [SnapshotWidget] that controls when the child image is displayed /// and when to regenerated the child image. /// /// When the value of [allowSnapshotting] is true, the [SnapshotWidget] will paint the child /// widgets based on the [SnapshotMode] of the snapshot widget. /// /// The controller notifies its listeners when the value of [allowSnapshotting] changes /// or when [clear] is called. /// /// To force [SnapshotWidget] to recreate the child image, call [clear]. class SnapshotController extends ChangeNotifier { /// Create a new [SnapshotController]. /// /// By default, [allowSnapshotting] is `false` and cannot be `null`. SnapshotController({ bool allowSnapshotting = false, }) : _allowSnapshotting = allowSnapshotting; /// Reset the snapshot held by any listening [SnapshotWidget]. /// /// This has no effect if [allowSnapshotting] is `false`. void clear() { notifyListeners(); } /// Whether a snapshot of this child widget is painted in its place. bool get allowSnapshotting => _allowSnapshotting; bool _allowSnapshotting; set allowSnapshotting(bool value) { if (value == allowSnapshotting) { return; } _allowSnapshotting = value; notifyListeners(); } } /// A widget that can replace its child with a snapshotted 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. /// /// For example, the Android Q [ZoomPageTransitionsBuilder] uses a snapshot widget /// for the forward and entering route to avoid the expensive scale animation. /// This also has the effect of briefly pausing any animations on the page. /// /// 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: /// /// * The contents of platform views cannot be captured by a snapshot /// widget. If a platform view is encountered, then the snapshot widget will /// determine how to render its children based on the [SnapshotMode]. This /// defaults to [SnapshotMode.normal] which will throw an exception if a /// platform view is encountered. /// /// * The snapshotting functionality of this widget is not supported on the HTML /// backend of Flutter for the Web. Setting [SnapshotController.allowSnapshotting] to true /// 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. const SnapshotWidget({ super.key, this.mode = SnapshotMode.normal, this.painter = const _DefaultSnapshotPainter(), required this.controller, required super.child }); /// The controller that determines when to display the children as a snapshot. final SnapshotController controller; /// Configuration that controls how the snapshot widget decides to paint its children. /// /// Defaults to [SnapshotMode.normal], which throws an error when a platform view /// or texture view is encountered. /// /// See [SnapshotMode] for more information. final SnapshotMode mode; /// The painter used to paint the child snapshot or child widgets. final SnapshotPainter painter; @override RenderObject createRenderObject(BuildContext context) { debugCheckHasMediaQuery(context); return _RenderSnapshotWidget( controller: controller, mode: mode, devicePixelRatio: MediaQuery.of(context).devicePixelRatio, painter: painter, ); } @override void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { debugCheckHasMediaQuery(context); (renderObject as _RenderSnapshotWidget) ..controller = controller ..mode = mode ..devicePixelRatio = MediaQuery.of(context).devicePixelRatio ..painter = painter; } } // A render object that conditionally converts its child into a [ui.Image] // and then paints it in place of the child. class _RenderSnapshotWidget extends RenderProxyBox { // Create a new [_RenderSnapshotWidget]. _RenderSnapshotWidget({ required double devicePixelRatio, required SnapshotController controller, required SnapshotMode mode, required SnapshotPainter painter, }) : _devicePixelRatio = devicePixelRatio, _controller = controller, _mode = mode, _painter = painter; /// The device pixel ratio used to create the child image. double get devicePixelRatio => _devicePixelRatio; double _devicePixelRatio; set devicePixelRatio(double value) { if (value == devicePixelRatio) { return; } _devicePixelRatio = value; if (_childRaster == null) { return; } else { _childRaster?.dispose(); _childRaster = null; markNeedsPaint(); } } /// The painter used to paint the child snapshot or child widgets. SnapshotPainter get painter => _painter; SnapshotPainter _painter; set painter(SnapshotPainter value) { if (value == painter) { return; } final SnapshotPainter oldPainter = painter; oldPainter.removeListener(markNeedsPaint); _painter = value; if (oldPainter.runtimeType != painter.runtimeType || painter.shouldRepaint(oldPainter)) { markNeedsPaint(); } if (attached) { painter.addListener(markNeedsPaint); } } /// A controller that determines whether to paint the child normally or to /// paint a snapshotted version of that child. SnapshotController get controller => _controller; SnapshotController _controller; set controller(SnapshotController value) { if (value == controller) { return; } controller.removeListener(_onRasterValueChanged); final bool oldValue = controller.allowSnapshotting; _controller = value; if (attached) { controller.addListener(_onRasterValueChanged); if (oldValue != controller.allowSnapshotting) { _onRasterValueChanged(); } } } /// How the snapshot widget will handle platform views in child layers. SnapshotMode get mode => _mode; SnapshotMode _mode; set mode(SnapshotMode value) { if (value == _mode) { return; } _mode = value; markNeedsPaint(); } 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 void attach(covariant PipelineOwner owner) { controller.addListener(_onRasterValueChanged); painter.addListener(markNeedsPaint); super.attach(owner); } @override void detach() { _disableSnapshotAttempt = false; controller.removeListener(_onRasterValueChanged); painter.removeListener(markNeedsPaint); _childRaster?.dispose(); _childRaster = null; super.detach(); } @override void dispose() { controller.removeListener(_onRasterValueChanged); painter.removeListener(markNeedsPaint); _childRaster?.dispose(); _childRaster = null; super.dispose(); } void _onRasterValueChanged() { _disableSnapshotAttempt = false; _childRaster?.dispose(); _childRaster = null; markNeedsPaint(); } // Paint [child] with this painting context, then convert to a raster and detach all // children from this layer. ui.Image? _paintAndDetachToImage() { final OffsetLayer offsetLayer = OffsetLayer(); final PaintingContext context = PaintingContext(offsetLayer, Offset.zero & size); super.paint(context, Offset.zero); // This ignore is here because this method is protected by the `PaintingContext`. Adding a new // method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but // that would conflict with our goals of minimizing painting context. // ignore: invalid_use_of_protected_member context.stopRecordingIfNeeded(); if (mode != SnapshotMode.forced && !offsetLayer.supportsRasterization()) { if (mode == SnapshotMode.normal) { throw FlutterError('SnapshotWidget used with a child that contains a PlatformView.'); } _disableSnapshotAttempt = true; return null; } final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio); offsetLayer.dispose(); return image; } @override void paint(PaintingContext context, Offset offset) { if (size.isEmpty) { _childRaster?.dispose(); _childRaster = null; return; } if (!controller.allowSnapshotting || _disableSnapshotAttempt) { _childRaster?.dispose(); _childRaster = null; painter.paint(context, offset, size, super.paint); return; } _childRaster ??= _paintAndDetachToImage(); if (_childRaster == null) { painter.paint(context, offset, size, super.paint); } else { painter.paintSnapshot(context, offset, size, _childRaster!, devicePixelRatio); } return; } } /// A painter used to paint either a snapshot or the child widgets that /// would be a snapshot. /// /// 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); /// Called whenever a new instance of the snapshot widget delegate class is /// provided to the [SnapshotWidget] object, or any time that a new /// [SnapshotPainter] 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 /// [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); } class _DefaultSnapshotPainter implements SnapshotPainter { const _DefaultSnapshotPainter(); @override void addListener(ui.VoidCallback listener) { } @override void dispose() { } @override bool get hasListeners => false; @override 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 void removeListener(ui.VoidCallback listener) { } @override bool shouldRepaint(covariant _DefaultSnapshotPainter oldPainter) => false; }