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 'dart:ui';
6///
7/// @docImport 'package:flutter/widgets.dart';
8///
9/// @docImport 'image_cache.dart';
10/// @docImport 'image_provider.dart';
11library;
12
13import 'dart:async';
14import 'dart:ui' as ui show Codec, FrameInfo, Image;
15
16import 'package:flutter/foundation.dart';
17import 'package:flutter/scheduler.dart';
18
19/// A [dart:ui.Image] object with its corresponding scale.
20///
21/// ImageInfo objects are used by [ImageStream] objects to represent the
22/// actual data of the image once it has been obtained.
23///
24/// The disposing contract for [ImageInfo] (as well as for [ui.Image])
25/// is different from traditional one, where
26/// an object should dispose a member if the object created the member.
27/// Instead, the disposal contract is as follows:
28///
29/// * [ImageInfo] disposes [image], even if it is received as a constructor argument.
30/// * [ImageInfo] is expected to be disposed not by the object, that created it,
31/// but by the object that owns reference to it.
32/// * It is expected that only one object owns reference to [ImageInfo] object.
33///
34/// Safety tips:
35///
36/// * To share the [ImageInfo] or [ui.Image] between objects, use the [clone] method,
37/// which will not clone the entire underlying image, but only reference to it and information about it.
38/// * After passing a [ui.Image] or [ImageInfo] reference to another object,
39/// release the reference.
40@immutable
41class ImageInfo {
42 /// Creates an [ImageInfo] object for the given [image] and [scale].
43 ///
44 /// The [debugLabel] may be used to identify the source of this image.
45 ///
46 /// See details for disposing contract in the class description.
47 ImageInfo({required this.image, this.scale = 1.0, this.debugLabel}) {
48 assert(debugMaybeDispatchCreated('painting', 'ImageInfo', this));
49 }
50
51 /// Creates an [ImageInfo] with a cloned [image].
52 ///
53 /// This method must be used in cases where a client holding an [ImageInfo]
54 /// needs to share the image info object with another client and will still
55 /// need to access the underlying image data at some later point, e.g. to
56 /// share it again with another client.
57 ///
58 /// See details for disposing contract in the class description.
59 ///
60 /// See also:
61 ///
62 /// * [ui.Image.clone], which describes how and why to clone images.
63 ImageInfo clone() {
64 return ImageInfo(image: image.clone(), scale: scale, debugLabel: debugLabel);
65 }
66
67 /// Whether this [ImageInfo] is a [clone] of the `other`.
68 ///
69 /// This method is a convenience wrapper for [ui.Image.isCloneOf], and is
70 /// useful for clients that are trying to determine whether new layout or
71 /// painting logic is required when receiving a new image reference.
72 ///
73 /// {@tool snippet}
74 ///
75 /// The following sample shows how to appropriately check whether the
76 /// [ImageInfo] reference refers to new image data or not (in this case in a
77 /// setter).
78 ///
79 /// ```dart
80 /// ImageInfo? get imageInfo => _imageInfo;
81 /// ImageInfo? _imageInfo;
82 /// set imageInfo (ImageInfo? value) {
83 /// // If the image reference is exactly the same, do nothing.
84 /// if (value == _imageInfo) {
85 /// return;
86 /// }
87 /// // If it is a clone of the current reference, we must dispose of it and
88 /// // can do so immediately. Since the underlying image has not changed,
89 /// // We don't have any additional work to do here.
90 /// if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
91 /// value.dispose();
92 /// return;
93 /// }
94 /// // It is a new image. Dispose of the old one and take a reference
95 /// // to the new one.
96 /// _imageInfo?.dispose();
97 /// _imageInfo = value;
98 /// // Perform work to determine size, paint the image, etc.
99 /// // ...
100 /// }
101 /// ```
102 /// {@end-tool}
103 bool isCloneOf(ImageInfo other) {
104 return other.image.isCloneOf(image) && scale == scale && other.debugLabel == debugLabel;
105 }
106
107 /// The raw image pixels.
108 ///
109 /// This is the object to pass to the [Canvas.drawImage],
110 /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
111 /// the image.
112 final ui.Image image;
113
114 /// The size of raw image pixels in bytes.
115 int get sizeBytes => image.height * image.width * 4;
116
117 /// The linear scale factor for drawing this image at its intended size.
118 ///
119 /// The scale factor applies to the width and the height.
120 ///
121 /// {@template flutter.painting.imageInfo.scale}
122 /// For example, if this is 2.0, it means that there are four image pixels for
123 /// every one logical pixel, and the image's actual width and height (as given
124 /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are
125 /// double the height and width that should be used when painting the image
126 /// (e.g. in the arguments given to [Canvas.drawImage]).
127 /// {@endtemplate}
128 final double scale;
129
130 /// A string used for debugging purposes to identify the source of this image.
131 final String? debugLabel;
132
133 /// Disposes of this object.
134 ///
135 /// Once this method has been called, the object should not be used anymore,
136 /// and no clones of it or the image it contains can be made.
137 void dispose() {
138 assert((image.debugGetOpenHandleStackTraces()?.length ?? 1) > 0);
139 assert(debugMaybeDispatchDisposed(this));
140 image.dispose();
141 }
142
143 @override
144 String toString() =>
145 '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
146
147 @override
148 int get hashCode => Object.hash(image, scale, debugLabel);
149
150 @override
151 bool operator ==(Object other) {
152 if (other.runtimeType != runtimeType) {
153 return false;
154 }
155 return other is ImageInfo &&
156 other.image == image &&
157 other.scale == scale &&
158 other.debugLabel == debugLabel;
159 }
160}
161
162/// Interface for receiving notifications about the loading of an image.
163///
164/// This class overrides [operator ==] and [hashCode] to compare the individual
165/// callbacks in the listener, meaning that if you add an instance of this class
166/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
167/// _different_ instance of this class when you remove the listener, and the
168/// listener will be properly removed as long as all associated callbacks are
169/// equal.
170///
171/// Used by [ImageStream] and [ImageStreamCompleter].
172@immutable
173class ImageStreamListener {
174 /// Creates a new [ImageStreamListener].
175 const ImageStreamListener(this.onImage, {this.onChunk, this.onError});
176
177 /// Callback for getting notified that an image is available.
178 ///
179 /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
180 /// that drives the notifications fires multiple times). An example of such a
181 /// case would be an image with multiple frames within it (such as an animated
182 /// GIF).
183 ///
184 /// For more information on how to interpret the parameters to the callback,
185 /// see the documentation on [ImageListener].
186 ///
187 /// See also:
188 ///
189 /// * [onError], which will be called instead of [onImage] if an error occurs
190 /// during loading.
191 final ImageListener onImage;
192
193 /// Callback for getting notified when a chunk of bytes has been received
194 /// during the loading of the image.
195 ///
196 /// This callback may fire many times (e.g. when used with a [NetworkImage],
197 /// where the image bytes are loaded incrementally over the wire) or not at
198 /// all (e.g. when used with a [MemoryImage], where the image bytes are
199 /// already available in memory).
200 ///
201 /// This callback may also continue to fire after the [onImage] callback has
202 /// fired (e.g. for multi-frame images that continue to load after the first
203 /// frame is available).
204 final ImageChunkListener? onChunk;
205
206 /// Callback for getting notified when an error occurs while loading an image.
207 ///
208 /// If an error occurs during loading, [onError] will be called instead of
209 /// [onImage].
210 ///
211 /// If [onError] is called and does not throw, then the error is considered to
212 /// be handled. An error handler can explicitly rethrow the exception reported
213 /// to it to safely indicate that it did not handle the exception.
214 ///
215 /// If an image stream has no listeners that handled the error when the error
216 /// was first encountered, then the error is reported using
217 /// [FlutterError.reportError], with the [FlutterErrorDetails.silent] flag set
218 /// to true.
219 final ImageErrorListener? onError;
220
221 @override
222 int get hashCode => Object.hash(onImage, onChunk, onError);
223
224 @override
225 bool operator ==(Object other) {
226 if (other.runtimeType != runtimeType) {
227 return false;
228 }
229 return other is ImageStreamListener &&
230 other.onImage == onImage &&
231 other.onChunk == onChunk &&
232 other.onError == onError;
233 }
234}
235
236/// Signature for callbacks reporting that an image is available.
237///
238/// Used in [ImageStreamListener].
239///
240/// The `image` argument contains information about the image to be rendered.
241/// The implementer of [ImageStreamListener.onImage] is expected to call dispose
242/// on the [ui.Image] it receives.
243///
244/// The `synchronousCall` argument is true if the listener is being invoked
245/// during the call to `addListener`. This can be useful if, for example,
246/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
247/// frame is requested if the call was asynchronous (after the current frame)
248/// and no rendering frame is requested if the call was synchronous (within the
249/// same stack frame as the call to [ImageStream.addListener]).
250typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
251
252/// Signature for listening to [ImageChunkEvent] events.
253///
254/// Used in [ImageStreamListener].
255typedef ImageChunkListener = void Function(ImageChunkEvent event);
256
257/// Signature for reporting errors when resolving images.
258///
259/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
260/// [precacheImage], to report errors.
261typedef ImageErrorListener = void Function(Object exception, StackTrace? stackTrace);
262
263/// An immutable notification of image bytes that have been incrementally loaded.
264///
265/// Chunk events represent progress notifications while an image is being
266/// loaded (e.g. from disk or over the network).
267///
268/// See also:
269///
270/// * [ImageChunkListener], the means by which callers get notified of
271/// these events.
272@immutable
273class ImageChunkEvent with Diagnosticable {
274 /// Creates a new chunk event.
275 const ImageChunkEvent({required this.cumulativeBytesLoaded, required this.expectedTotalBytes})
276 : assert(cumulativeBytesLoaded >= 0),
277 assert(expectedTotalBytes == null || expectedTotalBytes >= 0);
278
279 /// The number of bytes that have been received across the wire thus far.
280 final int cumulativeBytesLoaded;
281
282 /// The expected number of bytes that need to be received to finish loading
283 /// the image.
284 ///
285 /// This value is not necessarily equal to the expected _size_ of the image
286 /// in bytes, as the bytes required to load the image may be compressed.
287 ///
288 /// This value will be null if the number is not known in advance.
289 ///
290 /// When this value is null, the chunk event may still be useful as an
291 /// indication that data is loading (and how much), but it cannot represent a
292 /// loading completion percentage.
293 final int? expectedTotalBytes;
294
295 @override
296 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
297 super.debugFillProperties(properties);
298 properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
299 properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
300 }
301}
302
303/// A handle to an image resource.
304///
305/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
306/// (together represented by an [ImageInfo] object). The underlying image object
307/// might change over time, either because the image is animating or because the
308/// underlying image resource was mutated.
309///
310/// ImageStream objects can also represent an image that hasn't finished
311/// loading.
312///
313/// ImageStream objects are backed by [ImageStreamCompleter] objects.
314///
315/// The [ImageCache] will consider an image to be live until the listener count
316/// drops to zero after adding at least one listener. The
317/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
318/// tracking this information.
319///
320/// See also:
321///
322/// * [ImageProvider], which has an example that includes the use of an
323/// [ImageStream] in a [Widget].
324class ImageStream with Diagnosticable {
325 /// Create an initially unbound image stream.
326 ///
327 /// Once an [ImageStreamCompleter] is available, call [setCompleter].
328 ImageStream();
329
330 /// The completer that has been assigned to this image stream.
331 ///
332 /// Generally there is no need to deal with the completer directly.
333 ImageStreamCompleter? get completer => _completer;
334 ImageStreamCompleter? _completer;
335
336 List<ImageStreamListener>? _listeners;
337
338 /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
339 ///
340 /// This is usually done automatically by the [ImageProvider] that created the
341 /// [ImageStream].
342 ///
343 /// This method can only be called once per stream. To have an [ImageStream]
344 /// represent multiple images over time, assign it a completer that
345 /// completes several images in succession.
346 void setCompleter(ImageStreamCompleter value) {
347 assert(_completer == null);
348 _completer = value;
349 if (_listeners != null) {
350 final List<ImageStreamListener> initialListeners = _listeners!;
351 _listeners = null;
352 _completer!._addingInitialListeners = true;
353 initialListeners.forEach(_completer!.addListener);
354 _completer!._addingInitialListeners = false;
355 }
356 }
357
358 /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
359 /// object is available. If a concrete image is already available, this object
360 /// will call the listener synchronously.
361 ///
362 /// If the assigned [completer] completes multiple images over its lifetime,
363 /// this listener will fire multiple times.
364 ///
365 /// {@template flutter.painting.imageStream.addListener}
366 /// The listener will be passed a flag indicating whether a synchronous call
367 /// occurred. If the listener is added within a render object paint function,
368 /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
369 /// a paint.
370 ///
371 /// If a duplicate `listener` is registered N times, then it will be called N
372 /// times when the image stream completes (whether because a new image is
373 /// available or because an error occurs). Likewise, to remove all instances
374 /// of the listener, [removeListener] would need to called N times as well.
375 ///
376 /// When a `listener` receives an [ImageInfo] object, the `listener` is
377 /// responsible for disposing of the [ImageInfo.image].
378 /// {@endtemplate}
379 void addListener(ImageStreamListener listener) {
380 if (_completer != null) {
381 return _completer!.addListener(listener);
382 }
383 _listeners ??= <ImageStreamListener>[];
384 _listeners!.add(listener);
385 }
386
387 /// Stops listening for events from this stream's [ImageStreamCompleter].
388 ///
389 /// If [listener] has been added multiple times, this removes the _first_
390 /// instance of the listener.
391 void removeListener(ImageStreamListener listener) {
392 if (_completer != null) {
393 return _completer!.removeListener(listener);
394 }
395 assert(_listeners != null);
396 for (int i = 0; i < _listeners!.length; i += 1) {
397 if (_listeners![i] == listener) {
398 _listeners!.removeAt(i);
399 break;
400 }
401 }
402 }
403
404 /// Returns an object which can be used with `==` to determine if this
405 /// [ImageStream] shares the same listeners list as another [ImageStream].
406 ///
407 /// This can be used to avoid un-registering and re-registering listeners
408 /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
409 /// [ImageProvider].
410 ///
411 /// The key may change once in the lifetime of the object. When it changes, it
412 /// will go from being different than other [ImageStream]'s keys to
413 /// potentially being the same as others'. No notification is sent when this
414 /// happens.
415 Object get key => _completer ?? this;
416
417 @override
418 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
419 super.debugFillProperties(properties);
420 properties.add(
421 ObjectFlagProperty<ImageStreamCompleter>(
422 'completer',
423 _completer,
424 ifPresent: _completer?.toStringShort(),
425 ifNull: 'unresolved',
426 ),
427 );
428 properties.add(
429 ObjectFlagProperty<List<ImageStreamListener>>(
430 'listeners',
431 _listeners,
432 ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s"}',
433 ifNull: 'no listeners',
434 level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
435 ),
436 );
437 _completer?.debugFillProperties(properties);
438 }
439}
440
441/// An opaque handle that keeps an [ImageStreamCompleter] alive even if it has
442/// lost its last listener.
443///
444/// To create a handle, use [ImageStreamCompleter.keepAlive].
445///
446/// Such handles are useful when an image cache needs to keep a completer alive
447/// but does not actually have a listener subscribed, or when a widget that
448/// displays an image needs to temporarily unsubscribe from the completer but
449/// may re-subscribe in the future, for example when the [TickerMode] changes.
450class ImageStreamCompleterHandle {
451 ImageStreamCompleterHandle._(ImageStreamCompleter this._completer) {
452 _completer!._keepAliveHandles += 1;
453 assert(debugMaybeDispatchCreated('painting', 'ImageStreamCompleterHandle', this));
454 }
455
456 ImageStreamCompleter? _completer;
457
458 /// Call this method to signal the [ImageStreamCompleter] that it can now be
459 /// disposed when its last listener drops.
460 ///
461 /// This method must only be called once per object.
462 void dispose() {
463 assert(_completer != null);
464 assert(_completer!._keepAliveHandles > 0);
465 assert(!_completer!._disposed);
466
467 _completer!._keepAliveHandles -= 1;
468 _completer!._maybeDispose();
469 _completer = null;
470 assert(debugMaybeDispatchDisposed(this));
471 }
472}
473
474/// Base class for those that manage the loading of [dart:ui.Image] objects for
475/// [ImageStream]s.
476///
477/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
478/// [ImageProvider] subclass will return an [ImageStream] and automatically
479/// configure it with the right [ImageStreamCompleter] when possible.
480abstract class ImageStreamCompleter with Diagnosticable {
481 final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
482 final List<ImageErrorListener> _ephemeralErrorListeners = <ImageErrorListener>[];
483 ImageInfo? _currentImage;
484 FlutterErrorDetails? _currentError;
485
486 /// A string identifying the source of the underlying image.
487 String? debugLabel;
488
489 /// Whether any listeners are currently registered.
490 ///
491 /// Clients should not depend on this value for their behavior, because having
492 /// one listener's logic change when another listener happens to start or stop
493 /// listening will lead to extremely hard-to-track bugs. Subclasses might use
494 /// this information to determine whether to do any work when there are no
495 /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
496 /// to determine when to iterate through frames of an animated image.
497 ///
498 /// Typically this is used by overriding [addListener], checking if
499 /// [hasListeners] is false before calling `super.addListener()`, and if so,
500 /// starting whatever work is needed to determine when to notify listeners;
501 /// and similarly, by overriding [removeListener], checking if [hasListeners]
502 /// is false after calling `super.removeListener()`, and if so, stopping that
503 /// same work.
504 ///
505 /// The ephemeral error listeners (added through [addEphemeralErrorListener])
506 /// will not be taken into consideration in this property.
507 @protected
508 @visibleForTesting
509 bool get hasListeners => _listeners.isNotEmpty;
510
511 /// We must avoid disposing a completer if it has never had a listener, even
512 /// if all [keepAlive] handles get disposed.
513 bool _hadAtLeastOneListener = false;
514
515 /// Whether the future listeners added to this completer are initial listeners.
516 ///
517 /// This can be set to true when an [ImageStream] adds its initial listeners to
518 /// this completer. This ultimately controls the synchronousCall parameter for
519 /// the listener callbacks. When adding cached listeners to a completer,
520 /// [_addingInitialListeners] can be set to false to indicate to the listeners
521 /// that they are being called asynchronously.
522 bool _addingInitialListeners = false;
523
524 /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
525 /// object is available or an error is reported. If a concrete image is
526 /// already available, or if an error has been already reported, this object
527 /// will notify the listener synchronously.
528 ///
529 /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
530 /// this listener's [ImageStreamListener.onImage] will fire multiple times.
531 ///
532 /// {@macro flutter.painting.imageStream.addListener}
533 ///
534 /// See also:
535 ///
536 /// * [addEphemeralErrorListener], which adds an error listener that is
537 /// automatically removed after first image load or error.
538 void addListener(ImageStreamListener listener) {
539 _checkDisposed();
540 _hadAtLeastOneListener = true;
541 _listeners.add(listener);
542 if (_currentImage != null) {
543 try {
544 listener.onImage(_currentImage!.clone(), !_addingInitialListeners);
545 } catch (exception, stack) {
546 reportError(
547 context: ErrorDescription('by a synchronously-called image listener'),
548 exception: exception,
549 stack: stack,
550 );
551 }
552 }
553 if (_currentError != null && listener.onError != null) {
554 try {
555 listener.onError!(_currentError!.exception, _currentError!.stack);
556 } catch (newException, newStack) {
557 if (newException != _currentError!.exception) {
558 FlutterError.reportError(
559 FlutterErrorDetails(
560 exception: newException,
561 library: 'image resource service',
562 context: ErrorDescription('by a synchronously-called image error listener'),
563 stack: newStack,
564 ),
565 );
566 }
567 }
568 }
569 }
570
571 /// Adds an error listener callback that is called when the first error is reported.
572 ///
573 /// The callback will be removed automatically after the first successful
574 /// image load or the first error - that is why it is called "ephemeral".
575 ///
576 /// If a concrete image is already available, the listener will be discarded
577 /// synchronously. If an error has been already reported, the listener
578 /// will be notified synchronously.
579 ///
580 /// The presence of a listener will affect neither the lifecycle of this object
581 /// nor what [hasListeners] reports.
582 ///
583 /// It is different from [addListener] in a few points: Firstly, this one only
584 /// listens to errors, while [addListener] listens to all kinds of events.
585 /// Secondly, this listener will be automatically removed according to the
586 /// rules mentioned above, while [addListener] will need manual removal.
587 /// Thirdly, this listener will not affect how this object is disposed, while
588 /// any non-removed listener added via [addListener] will forbid this object
589 /// from disposal.
590 ///
591 /// When you want to know full information and full control, use [addListener].
592 /// When you only want to get notified for an error ephemerally, use this function.
593 ///
594 /// See also:
595 ///
596 /// * [addListener], which adds a full-featured listener and needs manual
597 /// removal.
598 void addEphemeralErrorListener(ImageErrorListener listener) {
599 _checkDisposed();
600 if (_currentError != null) {
601 // immediately fire the listener, and no need to add to _ephemeralErrorListeners
602 try {
603 listener(_currentError!.exception, _currentError!.stack);
604 } catch (newException, newStack) {
605 if (newException != _currentError!.exception) {
606 FlutterError.reportError(
607 FlutterErrorDetails(
608 exception: newException,
609 library: 'image resource service',
610 context: ErrorDescription('by a synchronously-called image error listener'),
611 stack: newStack,
612 ),
613 );
614 }
615 }
616 } else if (_currentImage == null) {
617 // add to _ephemeralErrorListeners to wait for the error,
618 // only if no image has been loaded
619 _ephemeralErrorListeners.add(listener);
620 }
621 }
622
623 int _keepAliveHandles = 0;
624
625 /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from
626 /// being disposed at least until the handle is disposed.
627 ///
628 /// Such handles are useful when an image cache needs to keep a completer
629 /// alive but does not itself have a listener subscribed, or when a widget
630 /// that displays an image needs to temporarily unsubscribe from the completer
631 /// but may re-subscribe in the future, for example when the [TickerMode]
632 /// changes.
633 ImageStreamCompleterHandle keepAlive() {
634 _checkDisposed();
635 return ImageStreamCompleterHandle._(this);
636 }
637
638 /// Stops the specified [listener] from receiving image stream events.
639 ///
640 /// If [listener] has been added multiple times, this removes the _first_
641 /// instance of the listener.
642 ///
643 /// Once all listeners have been removed and all [keepAlive] handles have been
644 /// disposed, this image stream is no longer usable.
645 void removeListener(ImageStreamListener listener) {
646 _checkDisposed();
647 for (int i = 0; i < _listeners.length; i += 1) {
648 if (_listeners[i] == listener) {
649 _listeners.removeAt(i);
650 break;
651 }
652 }
653 if (_listeners.isEmpty) {
654 final List<VoidCallback> callbacks = _onLastListenerRemovedCallbacks.toList();
655 for (final VoidCallback callback in callbacks) {
656 callback();
657 }
658 _onLastListenerRemovedCallbacks.clear();
659 _maybeDispose();
660 }
661 }
662
663 bool _disposed = false;
664
665 /// Called when this [ImageStreamCompleter] has actually been disposed.
666 ///
667 /// Subclasses should override this if they need to clean up resources when
668 /// they are disposed.
669 @mustCallSuper
670 @protected
671 void onDisposed() {}
672
673 /// Disposes this [ImageStreamCompleter] unless:
674 /// 1. It has never had a listener
675 /// 2. It is already disposed
676 /// 3. It has listeners.
677 /// 4. It has active "keep alive" handles.
678 @nonVirtual
679 void maybeDispose() {
680 _maybeDispose();
681 }
682
683 @mustCallSuper
684 void _maybeDispose() {
685 if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
686 return;
687 }
688
689 _ephemeralErrorListeners.clear();
690 _currentImage?.dispose();
691 _currentImage = null;
692 _disposed = true;
693 onDisposed();
694 }
695
696 void _checkDisposed() {
697 if (_disposed) {
698 throw StateError(
699 'Stream has been disposed.\n'
700 'An ImageStream is considered disposed once at least one listener has '
701 'been added and subsequently all listeners have been removed and no '
702 'handles are outstanding from the keepAlive method.\n'
703 'To resolve this error, maintain at least one listener on the stream, '
704 'or create an ImageStreamCompleterHandle from the keepAlive '
705 'method, or create a new stream for the image.',
706 );
707 }
708 }
709
710 final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];
711
712 /// Adds a callback to call when [removeListener] results in an empty
713 /// list of listeners and there are no [keepAlive] handles outstanding.
714 ///
715 /// This callback will never fire if [removeListener] is never called.
716 void addOnLastListenerRemovedCallback(VoidCallback callback) {
717 _checkDisposed();
718 _onLastListenerRemovedCallbacks.add(callback);
719 }
720
721 /// Removes a callback previously supplied to
722 /// [addOnLastListenerRemovedCallback].
723 void removeOnLastListenerRemovedCallback(VoidCallback callback) {
724 _checkDisposed();
725 _onLastListenerRemovedCallbacks.remove(callback);
726 }
727
728 /// Calls all the registered listeners to notify them of a new image.
729 @protected
730 @pragma('vm:notify-debugger-on-exception')
731 void setImage(ImageInfo image) {
732 _checkDisposed();
733 _currentImage?.dispose();
734 _currentImage = image;
735
736 _ephemeralErrorListeners.clear();
737
738 if (_listeners.isEmpty) {
739 return;
740 }
741 // Make a copy to allow for concurrent modification.
742 final List<ImageStreamListener> localListeners = List<ImageStreamListener>.of(_listeners);
743 for (final ImageStreamListener listener in localListeners) {
744 try {
745 listener.onImage(image.clone(), false);
746 } catch (exception, stack) {
747 reportError(
748 context: ErrorDescription('by an image listener'),
749 exception: exception,
750 stack: stack,
751 );
752 }
753 }
754 }
755
756 /// Calls all the registered error listeners to notify them of an error that
757 /// occurred while resolving the image.
758 ///
759 /// If no error listeners (listeners with an [ImageStreamListener.onError]
760 /// specified) are attached, or if the handlers all rethrow the exception
761 /// verbatim (with `throw exception`), a [FlutterError] will be reported using
762 /// [FlutterError.reportError].
763 ///
764 /// The `context` should be a string describing where the error was caught, in
765 /// a form that will make sense in English when following the word "thrown",
766 /// as in "thrown while obtaining the image from the network" (for the context
767 /// "while obtaining the image from the network").
768 ///
769 /// The `exception` is the error being reported; the `stack` is the
770 /// [StackTrace] associated with the exception.
771 ///
772 /// The `informationCollector` is a callback (of type [InformationCollector])
773 /// that is called when the exception is used by [FlutterError.reportError].
774 /// It is used to obtain further details to include in the logs, which may be
775 /// expensive to collect, and thus should only be collected if the error is to
776 /// be logged in the first place.
777 ///
778 /// The `silent` argument causes the exception to not be reported to the logs
779 /// in release builds, if passed to [FlutterError.reportError]. (It is still
780 /// sent to error handlers.) It should be set to true if the error is one that
781 /// is expected to be encountered in release builds, for example network
782 /// errors. That way, logs on end-user devices will not have spurious
783 /// messages, but errors during development will still be reported.
784 ///
785 /// See [FlutterErrorDetails] for further details on these values.
786 @pragma('vm:notify-debugger-on-exception')
787 void reportError({
788 DiagnosticsNode? context,
789 required Object exception,
790 StackTrace? stack,
791 InformationCollector? informationCollector,
792 bool silent = false,
793 }) {
794 _currentError = FlutterErrorDetails(
795 exception: exception,
796 stack: stack,
797 library: 'image resource service',
798 context: context,
799 informationCollector: informationCollector,
800 silent: silent,
801 );
802
803 // Make a copy to allow for concurrent modification.
804 final List<ImageErrorListener> localErrorListeners = <ImageErrorListener>[
805 ..._listeners
806 .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
807 .whereType<ImageErrorListener>(),
808 ..._ephemeralErrorListeners,
809 ];
810
811 _ephemeralErrorListeners.clear();
812
813 bool handled = false;
814 for (final ImageErrorListener errorListener in localErrorListeners) {
815 try {
816 errorListener(exception, stack);
817 handled = true;
818 } catch (newException, newStack) {
819 if (newException != exception) {
820 FlutterError.reportError(
821 FlutterErrorDetails(
822 context: ErrorDescription('when reporting an error to an image listener'),
823 library: 'image resource service',
824 exception: newException,
825 stack: newStack,
826 ),
827 );
828 }
829 }
830 }
831 if (!handled) {
832 FlutterError.reportError(_currentError!);
833 }
834 }
835
836 /// Calls all the registered [ImageChunkListener]s (listeners with an
837 /// [ImageStreamListener.onChunk] specified) to notify them of a new
838 /// [ImageChunkEvent].
839 @protected
840 void reportImageChunkEvent(ImageChunkEvent event) {
841 _checkDisposed();
842 if (hasListeners) {
843 // Make a copy to allow for concurrent modification.
844 final List<ImageChunkListener> localListeners =
845 _listeners
846 .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
847 .whereType<ImageChunkListener>()
848 .toList();
849 for (final ImageChunkListener listener in localListeners) {
850 listener(event);
851 }
852 }
853 }
854
855 /// Accumulates a list of strings describing the object's state. Subclasses
856 /// should override this to have their information included in [toString].
857 @override
858 void debugFillProperties(DiagnosticPropertiesBuilder description) {
859 super.debugFillProperties(description);
860 description.add(
861 DiagnosticsProperty<ImageInfo>(
862 'current',
863 _currentImage,
864 ifNull: 'unresolved',
865 showName: false,
866 ),
867 );
868 description.add(
869 ObjectFlagProperty<List<ImageStreamListener>>(
870 'listeners',
871 _listeners,
872 ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s"}',
873 ),
874 );
875 description.add(
876 ObjectFlagProperty<List<ImageErrorListener>>(
877 'ephemeralErrorListeners',
878 _ephemeralErrorListeners,
879 ifPresent:
880 '${_ephemeralErrorListeners.length} ephemeralErrorListener${_ephemeralErrorListeners.length == 1 ? "" : "s"}',
881 ),
882 );
883 description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
884 }
885}
886
887/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
888/// with only one frame).
889class OneFrameImageStreamCompleter extends ImageStreamCompleter {
890 /// Creates a manager for one-frame [ImageStream]s.
891 ///
892 /// The image resource awaits the given [Future]. When the future resolves,
893 /// it notifies the [ImageListener]s that have been registered with
894 /// [addListener].
895 ///
896 /// The [InformationCollector], if provided, is invoked if the given [Future]
897 /// resolves with an error, and can be used to supplement the reported error
898 /// message (for example, giving the image's URL).
899 ///
900 /// Errors are reported using [FlutterError.reportError] with the `silent`
901 /// argument on [FlutterErrorDetails] set to true, meaning that by default the
902 /// message is only dumped to the console in debug mode (see [
903 /// FlutterErrorDetails]).
904 OneFrameImageStreamCompleter(
905 Future<ImageInfo> image, {
906 InformationCollector? informationCollector,
907 }) {
908 image.then<void>(
909 setImage,
910 onError: (Object error, StackTrace stack) {
911 reportError(
912 context: ErrorDescription('resolving a single-frame image stream'),
913 exception: error,
914 stack: stack,
915 informationCollector: informationCollector,
916 silent: true,
917 );
918 },
919 );
920 }
921}
922
923/// Manages the decoding and scheduling of image frames.
924///
925/// New frames will only be emitted while there are registered listeners to the
926/// stream (registered with [addListener]).
927///
928/// This class deals with 2 types of frames:
929///
930/// * image frames - image frames of an animated image.
931/// * app frames - frames that the flutter engine is drawing to the screen to
932/// show the app GUI.
933///
934/// For single frame images the stream will only complete once.
935///
936/// For animated images, this class eagerly decodes the next image frame,
937/// and notifies the listeners that a new frame is ready on the first app frame
938/// that is scheduled after the image frame duration has passed.
939///
940/// Scheduling new timers only from scheduled app frames, makes sure we pause
941/// the animation when the app is not visible (as new app frames will not be
942/// scheduled).
943///
944/// See the following timeline example:
945///
946/// | Time | Event | Comment |
947/// |------|--------------------------------------------|---------------------------|
948/// | t1 | App frame scheduled (image frame A posted) | |
949/// | t2 | App frame scheduled | |
950/// | t3 | App frame scheduled | |
951/// | t4 | Image frame B decoded | |
952/// | t5 | App frame scheduled | t5 - t1 < frameB_duration |
953/// | t6 | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
954///
955class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
956 /// Creates a image stream completer.
957 ///
958 /// Immediately starts decoding the first image frame when the codec is ready.
959 ///
960 /// The `codec` parameter is a future for an initialized [ui.Codec] that will
961 /// be used to decode the image. This completer takes ownership of the passed
962 /// `codec` and will dispose it once it is no longer needed.
963 ///
964 /// The `scale` parameter is the linear scale factor for drawing this frames
965 /// of this image at their intended size.
966 ///
967 /// The `tag` parameter is passed on to created [ImageInfo] objects to
968 /// help identify the source of the image.
969 ///
970 /// The `chunkEvents` parameter is an optional stream of notifications about
971 /// the loading progress of the image. If this stream is provided, the events
972 /// produced by the stream will be delivered to registered [ImageChunkListener]s
973 /// (see [addListener]).
974 MultiFrameImageStreamCompleter({
975 required Future<ui.Codec> codec,
976 required double scale,
977 String? debugLabel,
978 Stream<ImageChunkEvent>? chunkEvents,
979 InformationCollector? informationCollector,
980 }) : _informationCollector = informationCollector,
981 _scale = scale {
982 this.debugLabel = debugLabel;
983 codec.then<void>(
984 _handleCodecReady,
985 onError: (Object error, StackTrace stack) {
986 reportError(
987 context: ErrorDescription('resolving an image codec'),
988 exception: error,
989 stack: stack,
990 informationCollector: informationCollector,
991 silent: true,
992 );
993 },
994 );
995 if (chunkEvents != null) {
996 _chunkSubscription = chunkEvents.listen(
997 reportImageChunkEvent,
998 onError: (Object error, StackTrace stack) {
999 reportError(
1000 context: ErrorDescription('loading an image'),
1001 exception: error,
1002 stack: stack,
1003 informationCollector: informationCollector,
1004 silent: true,
1005 );
1006 },
1007 );
1008 }
1009 }
1010
1011 StreamSubscription<ImageChunkEvent>? _chunkSubscription;
1012 ui.Codec? _codec;
1013 final double _scale;
1014 final InformationCollector? _informationCollector;
1015 ui.FrameInfo? _nextFrame;
1016 // When the current was first shown.
1017 late Duration _shownTimestamp;
1018 // The requested duration for the current frame;
1019 Duration? _frameDuration;
1020 // How many frames have been emitted so far.
1021 int _framesEmitted = 0;
1022 Timer? _timer;
1023
1024 // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
1025 bool _frameCallbackScheduled = false;
1026
1027 void _handleCodecReady(ui.Codec codec) {
1028 _codec = codec;
1029 assert(_codec != null);
1030
1031 if (hasListeners) {
1032 _decodeNextFrameAndSchedule();
1033 }
1034 }
1035
1036 void _handleAppFrame(Duration timestamp) {
1037 _frameCallbackScheduled = false;
1038 if (!hasListeners) {
1039 return;
1040 }
1041 assert(_nextFrame != null);
1042 if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
1043 _emitFrame(
1044 ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel),
1045 );
1046 _shownTimestamp = timestamp;
1047 _frameDuration = _nextFrame!.duration;
1048 _nextFrame!.image.dispose();
1049 _nextFrame = null;
1050 if (_codec == null) {
1051 // codec was disposed during _emitFrame
1052 return;
1053 }
1054 final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
1055 if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
1056 _decodeNextFrameAndSchedule();
1057 return;
1058 }
1059
1060 _codec!.dispose();
1061 _codec = null;
1062 return;
1063 }
1064 final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
1065 _timer = Timer(delay * timeDilation, () {
1066 _scheduleAppFrame();
1067 });
1068 }
1069
1070 bool _isFirstFrame() {
1071 return _frameDuration == null;
1072 }
1073
1074 bool _hasFrameDurationPassed(Duration timestamp) {
1075 return timestamp - _shownTimestamp >= _frameDuration!;
1076 }
1077
1078 Future<void> _decodeNextFrameAndSchedule() async {
1079 // This will be null if we gave it away. If not, it's still ours and it
1080 // must be disposed of.
1081 _nextFrame?.image.dispose();
1082 _nextFrame = null;
1083 try {
1084 _nextFrame = await _codec!.getNextFrame();
1085 } catch (exception, stack) {
1086 reportError(
1087 context: ErrorDescription('resolving an image frame'),
1088 exception: exception,
1089 stack: stack,
1090 informationCollector: _informationCollector,
1091 silent: true,
1092 );
1093 return;
1094 }
1095 if (_codec == null) {
1096 // codec was disposed during getNextFrame
1097 return;
1098 }
1099
1100 if (_codec!.frameCount == 1) {
1101 // ImageStreamCompleter listeners removed while waiting for next frame to
1102 // be decoded.
1103 // There's no reason to emit the frame without active listeners.
1104 if (!hasListeners) {
1105 return;
1106 }
1107 // This is not an animated image, just return it and don't schedule more
1108 // frames.
1109 _emitFrame(
1110 ImageInfo(image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel),
1111 );
1112 _nextFrame!.image.dispose();
1113 _nextFrame = null;
1114
1115 _codec?.dispose();
1116 _codec = null;
1117 return;
1118 }
1119 _scheduleAppFrame();
1120 }
1121
1122 void _scheduleAppFrame() {
1123 if (_frameCallbackScheduled) {
1124 return;
1125 }
1126 _frameCallbackScheduled = true;
1127 SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
1128 }
1129
1130 void _emitFrame(ImageInfo imageInfo) {
1131 setImage(imageInfo);
1132 _framesEmitted += 1;
1133 }
1134
1135 @override
1136 void addListener(ImageStreamListener listener) {
1137 if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) {
1138 _decodeNextFrameAndSchedule();
1139 }
1140 super.addListener(listener);
1141 }
1142
1143 @override
1144 void removeListener(ImageStreamListener listener) {
1145 super.removeListener(listener);
1146 if (!hasListeners) {
1147 _timer?.cancel();
1148 _timer = null;
1149 }
1150 }
1151
1152 @override
1153 void _maybeDispose() {
1154 super._maybeDispose();
1155 if (_disposed) {
1156 _chunkSubscription?.onData(null);
1157 _chunkSubscription?.cancel();
1158 _chunkSubscription = null;
1159
1160 _codec?.dispose();
1161 _codec = null;
1162 }
1163 }
1164}
1165

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com