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 | import 'dart:ui' as ui; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | |
10 | import 'basic.dart'; |
11 | import 'debug.dart'; |
12 | import 'framework.dart'; |
13 | import 'media_query.dart'; |
14 | |
15 | /// Controls how the [SnapshotWidget] paints its child. |
16 | enum 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]. |
47 | class 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. |
106 | class 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. |
165 | class _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} |
389 | abstract 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 | |
460 | class _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 | |