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'; |
6 | library; |
7 | |
8 | import 'dart:ui' as ui; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | import 'package:flutter/rendering.dart'; |
12 | |
13 | import 'basic.dart'; |
14 | import 'debug.dart'; |
15 | import 'framework.dart'; |
16 | import 'media_query.dart'; |
17 | |
18 | /// Controls how the [SnapshotWidget] paints its child. |
19 | enum 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]. |
50 | class 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. |
109 | class 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. |
168 | class _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} |
392 | abstract 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 | |
463 | class _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 | |