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