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