1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:ui' as ui;
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/rendering.dart';
9
10import 'basic.dart';
11import 'debug.dart';
12import 'framework.dart';
13import 'media_query.dart';
14
15/// Controls how the [SnapshotWidget] paints its child.
16enum SnapshotMode {
17 /// The child is snapshotted, but only if all descendants can be snapshotted.
18 ///
19 /// If there is a platform view in the children of a snapshot widget, the
20 /// snapshot will not be used and the child will be rendered using
21 /// [SnapshotPainter.paint]. This uses an un-snapshotted child and by default
22 /// paints it with no additional modification.
23 permissive,
24
25 /// An error is thrown if the child cannot be snapshotted.
26 ///
27 /// This setting is the default state of the [SnapshotWidget].
28 normal,
29
30 /// The child is snapshotted and any child platform views are ignored.
31 ///
32 /// This mode can be useful if there is a platform view descendant that does
33 /// not need to be included in the snapshot.
34 forced,
35}
36
37/// A controller for the [SnapshotWidget] that controls when the child image is displayed
38/// and when to regenerated the child image.
39///
40/// When the value of [allowSnapshotting] is true, the [SnapshotWidget] will paint the child
41/// widgets based on the [SnapshotMode] of the snapshot widget.
42///
43/// The controller notifies its listeners when the value of [allowSnapshotting] changes
44/// or when [clear] is called.
45///
46/// To force [SnapshotWidget] to recreate the child image, call [clear].
47class SnapshotController extends ChangeNotifier {
48 /// Create a new [SnapshotController].
49 ///
50 /// By default, [allowSnapshotting] is `false` and cannot be `null`.
51 SnapshotController({
52 bool allowSnapshotting = false,
53 }) : _allowSnapshotting = allowSnapshotting;
54
55 /// Reset the snapshot held by any listening [SnapshotWidget].
56 ///
57 /// This has no effect if [allowSnapshotting] is `false`.
58 void clear() {
59 notifyListeners();
60 }
61
62 /// Whether a snapshot of this child widget is painted in its place.
63 bool get allowSnapshotting => _allowSnapshotting;
64 bool _allowSnapshotting;
65 set allowSnapshotting(bool value) {
66 if (value == allowSnapshotting) {
67 return;
68 }
69 _allowSnapshotting = value;
70 notifyListeners();
71 }
72}
73
74/// A widget that can replace its child with a snapshotted version of the child.
75///
76/// A snapshot is a frozen texture-backed representation of all child pictures
77/// and layers stored as a [ui.Image].
78///
79/// This widget is useful for performing short animations that would otherwise
80/// be expensive or that cannot rely on raster caching. For example, scale and
81/// skew animations are often expensive to perform on complex children, as are
82/// blurs. For a short animation, a widget that contains these expensive effects
83/// can be replaced with a snapshot of itself and manipulated instead.
84///
85/// For example, the Android Q [ZoomPageTransitionsBuilder] uses a snapshot widget
86/// for the forward and entering route to avoid the expensive scale animation.
87/// This also has the effect of briefly pausing any animations on the page.
88///
89/// Generally, this widget should not be used in places where users expect the
90/// child widget to continue animating or to be responsive, such as an unbounded
91/// animation.
92///
93/// Caveats:
94///
95/// * The contents of platform views cannot be captured by a snapshot
96/// widget. If a platform view is encountered, then the snapshot widget will
97/// determine how to render its children based on the [SnapshotMode]. This
98/// defaults to [SnapshotMode.normal] which will throw an exception if a
99/// platform view is encountered.
100///
101/// * The snapshotting functionality of this widget is not supported on the HTML
102/// backend of Flutter for the Web. Setting [SnapshotController.allowSnapshotting] to true
103/// may cause an error to be thrown. On the CanvasKit backend of Flutter, the
104/// performance of using this widget may regress performance due to the fact
105/// that both the UI and engine share a single thread.
106class SnapshotWidget extends SingleChildRenderObjectWidget {
107 /// Create a new [SnapshotWidget].
108 ///
109 /// The [controller] and [child] arguments are required.
110 const SnapshotWidget({
111 super.key,
112 this.mode = SnapshotMode.normal,
113 this.painter = const _DefaultSnapshotPainter(),
114 this.autoresize = false,
115 required this.controller,
116 required super.child
117 });
118
119 /// The controller that determines when to display the children as a snapshot.
120 final SnapshotController controller;
121
122 /// Configuration that controls how the snapshot widget decides to paint its children.
123 ///
124 /// Defaults to [SnapshotMode.normal], which throws an error when a platform view
125 /// or texture view is encountered.
126 ///
127 /// See [SnapshotMode] for more information.
128 final SnapshotMode mode;
129
130 /// Whether or not changes in render object size should automatically re-create
131 /// the snapshot.
132 ///
133 /// Defaults to false.
134 final bool autoresize;
135
136 /// The painter used to paint the child snapshot or child widgets.
137 final SnapshotPainter painter;
138
139 @override
140 RenderObject createRenderObject(BuildContext context) {
141 debugCheckHasMediaQuery(context);
142 return _RenderSnapshotWidget(
143 controller: controller,
144 mode: mode,
145 devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
146 painter: painter,
147 autoresize: autoresize,
148 );
149 }
150
151 @override
152 void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
153 debugCheckHasMediaQuery(context);
154 (renderObject as _RenderSnapshotWidget)
155 ..controller = controller
156 ..mode = mode
157 ..devicePixelRatio = MediaQuery.devicePixelRatioOf(context)
158 ..painter = painter
159 ..autoresize = autoresize;
160 }
161}
162
163// A render object that conditionally converts its child into a [ui.Image]
164// and then paints it in place of the child.
165class _RenderSnapshotWidget extends RenderProxyBox {
166 // Create a new [_RenderSnapshotWidget].
167 _RenderSnapshotWidget({
168 required double devicePixelRatio,
169 required SnapshotController controller,
170 required SnapshotMode mode,
171 required SnapshotPainter painter,
172 required bool autoresize,
173 }) : _devicePixelRatio = devicePixelRatio,
174 _controller = controller,
175 _mode = mode,
176 _painter = painter,
177 _autoresize = autoresize;
178
179 /// The device pixel ratio used to create the child image.
180 double get devicePixelRatio => _devicePixelRatio;
181 double _devicePixelRatio;
182 set devicePixelRatio(double value) {
183 if (value == devicePixelRatio) {
184 return;
185 }
186 _devicePixelRatio = value;
187 if (_childRaster == null) {
188 return;
189 } else {
190 _childRaster?.dispose();
191 _childRaster = null;
192 markNeedsPaint();
193 }
194 }
195
196 /// The painter used to paint the child snapshot or child widgets.
197 SnapshotPainter get painter => _painter;
198 SnapshotPainter _painter;
199 set painter(SnapshotPainter value) {
200 if (value == painter) {
201 return;
202 }
203 final SnapshotPainter oldPainter = painter;
204 oldPainter.removeListener(markNeedsPaint);
205 _painter = value;
206 if (oldPainter.runtimeType != painter.runtimeType ||
207 painter.shouldRepaint(oldPainter)) {
208 markNeedsPaint();
209 }
210 if (attached) {
211 painter.addListener(markNeedsPaint);
212 }
213 }
214
215 /// A controller that determines whether to paint the child normally or to
216 /// paint a snapshotted version of that child.
217 SnapshotController get controller => _controller;
218 SnapshotController _controller;
219 set controller(SnapshotController value) {
220 if (value == controller) {
221 return;
222 }
223 controller.removeListener(_onRasterValueChanged);
224 final bool oldValue = controller.allowSnapshotting;
225 _controller = value;
226 if (attached) {
227 controller.addListener(_onRasterValueChanged);
228 if (oldValue != controller.allowSnapshotting) {
229 _onRasterValueChanged();
230 }
231 }
232 }
233
234 /// How the snapshot widget will handle platform views in child layers.
235 SnapshotMode get mode => _mode;
236 SnapshotMode _mode;
237 set mode(SnapshotMode value) {
238 if (value == _mode) {
239 return;
240 }
241 _mode = value;
242 markNeedsPaint();
243 }
244
245 /// Whether or not changes in render object size should automatically re-rasterize.
246 bool get autoresize => _autoresize;
247 bool _autoresize;
248 set autoresize(bool value) {
249 if (value == autoresize) {
250 return;
251 }
252 _autoresize = value;
253 markNeedsPaint();
254 }
255
256 ui.Image? _childRaster;
257 Size? _childRasterSize;
258 // Set to true if the snapshot mode was not forced and a platform view
259 // was encountered while attempting to snapshot the child.
260 bool _disableSnapshotAttempt = false;
261
262 @override
263 void attach(covariant PipelineOwner owner) {
264 controller.addListener(_onRasterValueChanged);
265 painter.addListener(markNeedsPaint);
266 super.attach(owner);
267 }
268
269 @override
270 void detach() {
271 _disableSnapshotAttempt = false;
272 controller.removeListener(_onRasterValueChanged);
273 painter.removeListener(markNeedsPaint);
274 _childRaster?.dispose();
275 _childRaster = null;
276 _childRasterSize = null;
277 super.detach();
278 }
279
280 @override
281 void dispose() {
282 controller.removeListener(_onRasterValueChanged);
283 painter.removeListener(markNeedsPaint);
284 _childRaster?.dispose();
285 _childRaster = null;
286 _childRasterSize = null;
287 super.dispose();
288 }
289
290 void _onRasterValueChanged() {
291 _disableSnapshotAttempt = false;
292 _childRaster?.dispose();
293 _childRaster = null;
294 _childRasterSize = null;
295 markNeedsPaint();
296 }
297
298 // Paint [child] with this painting context, then convert to a raster and detach all
299 // children from this layer.
300 ui.Image? _paintAndDetachToImage() {
301 final OffsetLayer offsetLayer = OffsetLayer();
302 final PaintingContext context = PaintingContext(offsetLayer, Offset.zero & size);
303 super.paint(context, Offset.zero);
304 // This ignore is here because this method is protected by the `PaintingContext`. Adding a new
305 // method that performs the work of `_paintAndDetachToImage` would avoid the need for this, but
306 // that would conflict with our goals of minimizing painting context.
307 // ignore: invalid_use_of_protected_member
308 context.stopRecordingIfNeeded();
309 if (mode != SnapshotMode.forced && !offsetLayer.supportsRasterization()) {
310 offsetLayer.dispose();
311 if (mode == SnapshotMode.normal) {
312 throw FlutterError('SnapshotWidget used with a child that contains a PlatformView.');
313 }
314 _disableSnapshotAttempt = true;
315 return null;
316 }
317 final ui.Image image = offsetLayer.toImageSync(Offset.zero & size, pixelRatio: devicePixelRatio);
318 offsetLayer.dispose();
319 _lastCachedSize = size;
320 return image;
321 }
322
323 Size? _lastCachedSize;
324
325 @override
326 void paint(PaintingContext context, Offset offset) {
327 if (size.isEmpty) {
328 _childRaster?.dispose();
329 _childRaster = null;
330 _childRasterSize = null;
331 return;
332 }
333 if (!controller.allowSnapshotting || _disableSnapshotAttempt) {
334 _childRaster?.dispose();
335 _childRaster = null;
336 _childRasterSize = null;
337 painter.paint(context, offset, size, super.paint);
338 return;
339 }
340
341 if (autoresize && size != _lastCachedSize && _lastCachedSize != null) {
342 _childRaster?.dispose();
343 _childRaster = null;
344 }
345
346 if (_childRaster == null) {
347 _childRaster = _paintAndDetachToImage();
348 _childRasterSize = size * devicePixelRatio;
349 }
350 if (_childRaster == null) {
351 painter.paint(context, offset, size, super.paint);
352 } else {
353 painter.paintSnapshot(context, offset, size, _childRaster!, _childRasterSize!, devicePixelRatio);
354 }
355 }
356}
357
358/// A painter used to paint either a snapshot or the child widgets that
359/// would be a snapshot.
360///
361/// The painter can call [notifyListeners] to have the [SnapshotWidget]
362/// re-paint (re-using the same raster). This allows animations to be performed
363/// without re-snapshotting of children. For certain scale or perspective changing
364/// transforms, such as a rotation, this can be significantly faster than performing
365/// the same animation at the widget level.
366///
367/// By default, the [SnapshotWidget] includes a delegate that draws the child raster
368/// exactly as the child widgets would have been drawn. Nevertheless, this can
369/// also be used to efficiently transform the child raster and apply complex paint
370/// effects.
371///
372/// {@tool snippet}
373///
374/// The following method shows how to efficiently rotate the child raster.
375///
376/// ```dart
377/// void paint(PaintingContext context, Offset offset, Size size, ui.Image image, double pixelRatio) {
378/// const double radians = 0.5; // Could be driven by an animation.
379/// final Matrix4 transform = Matrix4.rotationZ(radians);
380/// context.canvas.transform(transform.storage);
381/// final Rect src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
382/// final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
383/// final Paint paint = Paint()
384/// ..filterQuality = FilterQuality.low;
385/// context.canvas.drawImageRect(image, src, dst, paint);
386/// }
387/// ```
388/// {@end-tool}
389abstract class SnapshotPainter extends ChangeNotifier {
390 /// Creates an instance of [SnapshotPainter].
391 SnapshotPainter() {
392 if (kFlutterMemoryAllocationsEnabled) {
393 ChangeNotifier.maybeDispatchObjectCreation(this);
394 }
395 }
396
397 /// Called whenever the [image] that represents a [SnapshotWidget]s child should be painted.
398 ///
399 /// The image is rasterized at the physical pixel resolution and should be scaled down by
400 /// [pixelRatio] to account for device independent pixels.
401 ///
402 /// {@tool snippet}
403 ///
404 /// The following method shows how the default implementation of the delegate used by the
405 /// [SnapshotPainter] paints the snapshot. This must account for the fact that the image
406 /// width and height will be given in physical pixels, while the image must be painted with
407 /// device independent pixels. That is, the width and height of the image is the widget and
408 /// height of the provided `size`, multiplied by the `pixelRatio`. In addition, the actual
409 /// size of the scene captured by the `image` is not `image.width` or `image.height`, but
410 /// indeed `sourceSize`, because the former is a rounded inaccurate integer:
411 ///
412 /// ```dart
413 /// void paint(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
414 /// final Rect src = Rect.fromLTWH(0, 0, sourceSize.width, sourceSize.height);
415 /// final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
416 /// final Paint paint = Paint()
417 /// ..filterQuality = FilterQuality.low;
418 /// context.canvas.drawImageRect(image, src, dst, paint);
419 /// }
420 /// ```
421 /// {@end-tool}
422 void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio);
423
424 /// Paint the child via [painter], applying any effects that would have been painted
425 /// in [SnapshotPainter.paintSnapshot].
426 ///
427 /// This method is called when snapshotting is disabled, or when [SnapshotMode.permissive]
428 /// is used and a child platform view prevents snapshotting.
429 ///
430 /// The [offset] and [size] are the location and dimensions of the render object.
431 void paint(PaintingContext context, Offset offset, Size size, PaintingContextCallback painter);
432
433 /// Called whenever a new instance of the snapshot widget delegate class is
434 /// provided to the [SnapshotWidget] object, or any time that a new
435 /// [SnapshotPainter] object is created with a new instance of the
436 /// delegate class (which amounts to the same thing, because the latter is
437 /// implemented in terms of the former).
438 ///
439 /// If the new instance represents different information than the old
440 /// instance, then the method should return true, otherwise it should return
441 /// false.
442 ///
443 /// If the method returns false, then the [paint] call might be optimized
444 /// away.
445 ///
446 /// It's possible that the [paint] method will get called even if
447 /// [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to
448 /// be repainted). It's also possible that the [paint] method will get called
449 /// without [shouldRepaint] being called at all (e.g. if the box changes
450 /// size).
451 ///
452 /// Changing the delegate will not cause the child image retained by the
453 /// [SnapshotWidget] to be updated. Instead, [SnapshotController.clear] can
454 /// be used to force the generation of a new image.
455 ///
456 /// The `oldPainter` argument will never be null.
457 bool shouldRepaint(covariant SnapshotPainter oldPainter);
458}
459
460class _DefaultSnapshotPainter implements SnapshotPainter {
461 const _DefaultSnapshotPainter();
462
463 @override
464 void addListener(ui.VoidCallback listener) { }
465
466 @override
467 void dispose() { }
468
469 @override
470 bool get hasListeners => false;
471
472 @override
473 void notifyListeners() { }
474
475 @override
476 void paint(PaintingContext context, ui.Offset offset, ui.Size size, PaintingContextCallback painter) {
477 painter(context, offset);
478 }
479
480 @override
481 void paintSnapshot(PaintingContext context, ui.Offset offset, ui.Size size, ui.Image image, Size sourceSize, double pixelRatio) {
482 final Rect src = Rect.fromLTWH(0, 0, sourceSize.width, sourceSize.height);
483 final Rect dst = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
484 final Paint paint = Paint()
485 ..filterQuality = FilterQuality.low;
486 context.canvas.drawImageRect(image, src, dst, paint);
487 }
488
489 @override
490 void removeListener(ui.VoidCallback listener) { }
491
492 @override
493 bool shouldRepaint(covariant _DefaultSnapshotPainter oldPainter) => false;
494}
495