image_stream.dart 36.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6 7
import 'dart:ui' as ui show Image, Codec, FrameInfo;
import 'dart:ui' show hashValues;
8 9

import 'package:flutter/foundation.dart';
10
import 'package:flutter/scheduler.dart';
11

12
/// A [dart:ui.Image] object with its corresponding scale.
13 14 15
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
16 17 18 19
///
/// The receiver of an [ImageInfo] object must call [dispose]. To safely share
/// the object with other clients, use the [clone] method before calling
/// dispose.
20
@immutable
21
class ImageInfo {
Dan Field's avatar
Dan Field committed
22
  /// Creates an [ImageInfo] object for the given [image] and [scale].
23
  ///
24
  /// Both the [image] and the [scale] must not be null.
25
  ///
26
  /// The [debugLabel] may be used to identify the source of this image.
27
  const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
28 29
    : assert(image != null),
      assert(scale != null);
30

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  /// Creates an [ImageInfo] with a cloned [image].
  ///
  /// Once all outstanding references to the [image] are disposed, it is no
  /// longer safe to access properties of it or attempt to draw it. Clones serve
  /// to create new references to the underlying image data that can safely be
  /// disposed without knowledge of whether some other reference holder will
  /// still need access to the underlying image. Once a client disposes of its
  /// own image reference, it can no longer access the image, but other clients
  /// will be able to access their own references.
  ///
  /// This method must be used in cases where a client holding an [ImageInfo]
  /// needs to share the image info object with another client and will still
  /// need to access the underlying image data at some later point, e.g. to
  /// share it again with another client.
  ///
  /// See also:
  ///
  ///  * [Image.clone], which describes how and why to clone images.
  ImageInfo clone() {
    return ImageInfo(
      image: image.clone(),
      scale: scale,
      debugLabel: debugLabel,
    );
  }

  /// Whether this [ImageInfo] is a [clone] of the `other`.
  ///
  /// This method is a convenience wrapper for [Image.isCloneOf], and is useful
  /// for clients that are trying to determine whether new layout or painting
61
  /// logic is required when receiving a new image reference.
62 63 64 65 66 67 68
  ///
  /// {@tool snippet}
  ///
  /// The following sample shows how to appropriately check whether the
  /// [ImageInfo] reference refers to new image data or not.
  ///
  /// ```dart
69 70
  /// ImageInfo? _imageInfo;
  /// set imageInfo (ImageInfo? value) {
71 72 73 74 75 76 77
  ///   // If the image reference is exactly the same, do nothing.
  ///   if (value == _imageInfo) {
  ///     return;
  ///   }
  ///   // If it is a clone of the current reference, we must dispose of it and
  ///   // can do so immediately. Since the underlying image has not changed,
  ///   // We don't have any additional work to do here.
78
  ///   if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  ///     value.dispose();
  ///     return;
  ///   }
  ///   _imageInfo?.dispose();
  ///   _imageInfo = value;
  ///   // Perform work to determine size, or paint the image.
  /// }
  /// ```
  /// {@end-tool}
  bool isCloneOf(ImageInfo other) {
    return other.image.isCloneOf(image)
        && scale == scale
        && other.debugLabel == debugLabel;
  }

94 95 96 97 98 99 100
  /// The raw image pixels.
  ///
  /// This is the object to pass to the [Canvas.drawImage],
  /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
  /// the image.
  final ui.Image image;

101 102 103
  /// The size of raw image pixels in bytes.
  int get sizeBytes => image.height * image.width * 4;

104 105 106 107
  /// The linear scale factor for drawing this image at its intended size.
  ///
  /// The scale factor applies to the width and the height.
  ///
108 109
  /// {@template flutter.painting.imageInfo.scale}
  /// For example, if this is 2.0, it means that there are four image pixels for
110
  /// every one logical pixel, and the image's actual width and height (as given
Dan Field's avatar
Dan Field committed
111 112 113
  /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are
  /// double the height and width that should be used when painting the image
  /// (e.g. in the arguments given to [Canvas.drawImage]).
114
  /// {@endtemplate}
115 116
  final double scale;

117
  /// A string used for debugging purposes to identify the source of this image.
118
  final String? debugLabel;
119

120 121 122 123 124 125 126 127 128
  /// Disposes of this object.
  ///
  /// Once this method has been called, the object should not be used anymore,
  /// and no clones of it or the image it contains can be made.
  void dispose() {
    assert((image.debugGetOpenHandleStackTraces()?.length ?? 1) > 0);
    image.dispose();
  }

129
  @override
130
  String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
131 132

  @override
133
  int get hashCode => hashValues(image, scale, debugLabel);
134 135 136 137 138

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
139 140
    return other is ImageInfo
        && other.image == image
141 142
        && other.scale == scale
        && other.debugLabel == debugLabel;
143
  }
144 145
}

146 147
/// Interface for receiving notifications about the loading of an image.
///
Dan Field's avatar
Dan Field committed
148
/// This class overrides [operator ==] and [hashCode] to compare the individual
149 150 151
/// callbacks in the listener, meaning that if you add an instance of this class
/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
/// _different_ instance of this class when you remove the listener, and the
Dan Field's avatar
Dan Field committed
152
/// listener will be properly removed as long as all associated callbacks are
153 154 155 156 157 158 159 160 161 162
/// equal.
///
/// Used by [ImageStream] and [ImageStreamCompleter].
@immutable
class ImageStreamListener {
  /// Creates a new [ImageStreamListener].
  ///
  /// The [onImage] parameter must not be null.
  const ImageStreamListener(
    this.onImage, {
163
    this.onChunk,
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
    this.onError,
  }) : assert(onImage != null);

  /// Callback for getting notified that an image is available.
  ///
  /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
  /// that drives the notifications fires multiple times). An example of such a
  /// case would be an image with multiple frames within it (such as an animated
  /// GIF).
  ///
  /// For more information on how to interpret the parameters to the callback,
  /// see the documentation on [ImageListener].
  ///
  /// See also:
  ///
  ///  * [onError], which will be called instead of [onImage] if an error occurs
  ///    during loading.
  final ImageListener onImage;

183 184 185 186 187 188 189 190 191 192 193
  /// Callback for getting notified when a chunk of bytes has been received
  /// during the loading of the image.
  ///
  /// This callback may fire many times (e.g. when used with a [NetworkImage],
  /// where the image bytes are loaded incrementally over the wire) or not at
  /// all (e.g. when used with a [MemoryImage], where the image bytes are
  /// already available in memory).
  ///
  /// This callback may also continue to fire after the [onImage] callback has
  /// fired (e.g. for multi-frame images that continue to load after the first
  /// frame is available).
194
  final ImageChunkListener? onChunk;
195

196 197 198 199
  /// Callback for getting notified when an error occurs while loading an image.
  ///
  /// If an error occurs during loading, [onError] will be called instead of
  /// [onImage].
200 201 202 203 204 205 206 207 208
  ///
  /// If [onError] is called and does not throw, then the error is considered to
  /// be handled. An error handler can explicitly rethrow the exception reported
  /// to it to safely indicate that it did not handle the exception.
  ///
  /// If an image stream has no listeners that handled the error when the error
  /// was first encountered, then the error is reported using
  /// [FlutterError.reportError], with the [FlutterErrorDetails.silent] flag set
  /// to true.
209
  final ImageErrorListener? onError;
210 211

  @override
212
  int get hashCode => hashValues(onImage, onChunk, onError);
213 214

  @override
215
  bool operator ==(Object other) {
216 217
    if (other.runtimeType != runtimeType)
      return false;
218 219 220 221
    return other is ImageStreamListener
        && other.onImage == onImage
        && other.onChunk == onChunk
        && other.onError == onError;
222 223 224
  }
}

225 226
/// Signature for callbacks reporting that an image is available.
///
227
/// Used in [ImageStreamListener].
228
///
229 230 231 232
/// The `image` argument contains information about the image to be rendered.
/// The implementer of [ImageStreamListener.onImage] is expected to call dispose
/// on the [ui.Image] it receives.
///
233
/// The `synchronousCall` argument is true if the listener is being invoked
234
/// during the call to `addListener`. This can be useful if, for example,
235 236 237 238
/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
/// frame is requested if the call was asynchronous (after the current frame)
/// and no rendering frame is requested if the call was synchronous (within the
/// same stack frame as the call to [ImageStream.addListener]).
239
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
240

241 242 243 244 245
/// Signature for listening to [ImageChunkEvent] events.
///
/// Used in [ImageStreamListener].
typedef ImageChunkListener = void Function(ImageChunkEvent event);

246 247
/// Signature for reporting errors when resolving images.
///
248 249
/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
/// [precacheImage], to report errors.
250
typedef ImageErrorListener = void Function(Object exception, StackTrace? stackTrace);
251

252 253 254 255 256 257 258 259 260 261
/// An immutable notification of image bytes that have been incrementally loaded.
///
/// Chunk events represent progress notifications while an image is being
/// loaded (e.g. from disk or over the network).
///
/// See also:
///
///  * [ImageChunkListener], the means by which callers get notified of
///    these events.
@immutable
262
class ImageChunkEvent with Diagnosticable {
263 264
  /// Creates a new chunk event.
  const ImageChunkEvent({
265 266
    required this.cumulativeBytesLoaded,
    required this.expectedTotalBytes,
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  }) : assert(cumulativeBytesLoaded >= 0),
       assert(expectedTotalBytes == null || expectedTotalBytes >= 0);

  /// The number of bytes that have been received across the wire thus far.
  final int cumulativeBytesLoaded;

  /// The expected number of bytes that need to be received to finish loading
  /// the image.
  ///
  /// This value is not necessarily equal to the expected _size_ of the image
  /// in bytes, as the bytes required to load the image may be compressed.
  ///
  /// This value will be null if the number is not known in advance.
  ///
  /// When this value is null, the chunk event may still be useful as an
  /// indication that data is loading (and how much), but it cannot represent a
  /// loading completion percentage.
284
  final int? expectedTotalBytes;
285 286 287 288 289 290 291 292 293

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
    properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
  }
}

294 295
/// A handle to an image resource.
///
296
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
297 298 299 300 301 302 303 304
/// (together represented by an [ImageInfo] object). The underlying image object
/// might change over time, either because the image is animating or because the
/// underlying image resource was mutated.
///
/// ImageStream objects can also represent an image that hasn't finished
/// loading.
///
/// ImageStream objects are backed by [ImageStreamCompleter] objects.
305
///
Dan Field's avatar
Dan Field committed
306 307
/// The [ImageCache] will consider an image to be live until the listener count
/// drops to zero after adding at least one listener. The
308 309
/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
/// tracking this information.
Dan Field's avatar
Dan Field committed
310
///
311 312 313 314
/// See also:
///
///  * [ImageProvider], which has an example that includes the use of an
///    [ImageStream] in a [Widget].
315
class ImageStream with Diagnosticable {
316 317 318 319 320 321 322 323
  /// Create an initially unbound image stream.
  ///
  /// Once an [ImageStreamCompleter] is available, call [setCompleter].
  ImageStream();

  /// The completer that has been assigned to this image stream.
  ///
  /// Generally there is no need to deal with the completer directly.
324 325
  ImageStreamCompleter? get completer => _completer;
  ImageStreamCompleter? _completer;
326

327
  List<ImageStreamListener>? _listeners;
328 329 330 331 332

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  ///
  /// This is usually done automatically by the [ImageProvider] that created the
  /// [ImageStream].
333 334 335 336
  ///
  /// This method can only be called once per stream. To have an [ImageStream]
  /// represent multiple images over time, assign it a completer that
  /// completes several images in succession.
337 338 339 340
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
341
      final List<ImageStreamListener> initialListeners = _listeners!;
342
      _listeners = null;
343
      initialListeners.forEach(_completer!.addListener);
344 345 346
    }
  }

347
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
348 349
  /// object is available. If a concrete image is already available, this object
  /// will call the listener synchronously.
350 351 352 353
  ///
  /// If the assigned [completer] completes multiple images over its lifetime,
  /// this listener will fire multiple times.
  ///
354
  /// {@template flutter.painting.imageStream.addListener}
355 356 357 358
  /// The listener will be passed a flag indicating whether a synchronous call
  /// occurred. If the listener is added within a render object paint function,
  /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
  /// a paint.
359
  ///
360 361 362 363
  /// If a duplicate `listener` is registered N times, then it will be called N
  /// times when the image stream completes (whether because a new image is
  /// available or because an error occurs). Likewise, to remove all instances
  /// of the listener, [removeListener] would need to called N times as well.
364 365 366
  ///
  /// When a `listener` receives an [ImageInfo] object, the `listener` is
  /// responsible for disposing of the [ImageInfo.image].
367 368
  /// {@endtemplate}
  void addListener(ImageStreamListener listener) {
369
    if (_completer != null)
370
      return _completer!.addListener(listener);
371
    _listeners ??= <ImageStreamListener>[];
372
    _listeners!.add(listener);
373 374
  }

375 376 377 378 379
  /// Stops listening for events from this stream's [ImageStreamCompleter].
  ///
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
  void removeListener(ImageStreamListener listener) {
380
    if (_completer != null)
381
      return _completer!.removeListener(listener);
382
    assert(_listeners != null);
383 384 385
    for (int i = 0; i < _listeners!.length; i += 1) {
      if (_listeners![i] == listener) {
        _listeners!.removeAt(i);
386
        break;
387 388
      }
    }
389 390 391 392 393
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
394 395
  /// This can be used to avoid un-registering and re-registering listeners
  /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
396 397 398 399 400 401
  /// [ImageProvider].
  ///
  /// The key may change once in the lifetime of the object. When it changes, it
  /// will go from being different than other [ImageStream]'s keys to
  /// potentially being the same as others'. No notification is sent when this
  /// happens.
402
  Object get key => _completer ?? this;
403

404 405 406
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
407
    properties.add(ObjectFlagProperty<ImageStreamCompleter>(
408 409 410 411 412
      'completer',
      _completer,
      ifPresent: _completer?.toStringShort(),
      ifNull: 'unresolved',
    ));
413
    properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
414 415 416 417
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
      ifNull: 'no listeners',
418
      level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
419 420
    ));
    _completer?.debugFillProperties(properties);
421 422 423
  }
}

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
/// An opaque handle that keeps an [ImageStreamCompleter] alive even if it has
/// lost its last listener.
///
/// To create a handle, use [ImageStreamCompleter.keepAlive].
///
/// Such handles are useful when an image cache needs to keep a completer alive
/// but does not actually have a listener subscribed, or when a widget that
/// displays an image needs to temporarily unsubscribe from the completer but
/// may re-subscribe in the future, for example when the [TickerMode] changes.
class ImageStreamCompleterHandle {
  ImageStreamCompleterHandle._(ImageStreamCompleter this._completer) {
    _completer!._keepAliveHandles += 1;
  }

  ImageStreamCompleter? _completer;

  /// Call this method to signal the [ImageStreamCompleter] that it can now be
  /// disposed when its last listener drops.
  ///
  /// This method must only be called once per object.
  void dispose() {
    assert(_completer != null);
    assert(_completer!._keepAliveHandles > 0);
    assert(!_completer!._disposed);

    _completer!._keepAliveHandles -= 1;
    _completer!._maybeDispose();
    _completer = null;
  }
}

455
/// Base class for those that manage the loading of [dart:ui.Image] objects for
456 457
/// [ImageStream]s.
///
458 459 460
/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
/// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible.
461
abstract class ImageStreamCompleter with Diagnosticable {
462
  final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
463 464
  ImageInfo? _currentImage;
  FlutterErrorDetails? _currentError;
465

466
  /// A string identifying the source of the underlying image.
467
  String? debugLabel;
468

469 470 471 472 473 474 475 476 477 478 479
  /// Whether any listeners are currently registered.
  ///
  /// Clients should not depend on this value for their behavior, because having
  /// one listener's logic change when another listener happens to start or stop
  /// listening will lead to extremely hard-to-track bugs. Subclasses might use
  /// this information to determine whether to do any work when there are no
  /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
  /// to determine when to iterate through frames of an animated image.
  ///
  /// Typically this is used by overriding [addListener], checking if
  /// [hasListeners] is false before calling `super.addListener()`, and if so,
480 481 482 483
  /// starting whatever work is needed to determine when to notify listeners;
  /// and similarly, by overriding [removeListener], checking if [hasListeners]
  /// is false after calling `super.removeListener()`, and if so, stopping that
  /// same work.
484
  @protected
485
  @visibleForTesting
486 487
  bool get hasListeners => _listeners.isNotEmpty;

488 489 490 491
  /// We must avoid disposing a completer if it has never had a listener, even
  /// if all [keepAlive] handles get disposed.
  bool _hadAtLeastOneListener = false;

492
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
493 494
  /// object is available or an error is reported. If a concrete image is
  /// already available, or if an error has been already reported, this object
495
  /// will notify the listener synchronously.
496
  ///
497
  /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
498
  /// this listener's [ImageStreamListener.onImage] will fire multiple times.
499
  ///
500 501
  /// {@macro flutter.painting.imageStream.addListener}
  void addListener(ImageStreamListener listener) {
502 503
    _checkDisposed();
    _hadAtLeastOneListener = true;
504
    _listeners.add(listener);
505
    if (_currentImage != null) {
506
      try {
507
        listener.onImage(_currentImage!.clone(), true);
508
      } catch (exception, stack) {
509
        reportError(
510
          context: ErrorDescription('by a synchronously-called image listener'),
511 512 513
          exception: exception,
          stack: stack,
        );
514 515
      }
    }
516
    if (_currentError != null && listener.onError != null) {
517
      try {
518
        listener.onError!(_currentError!.exception, _currentError!.stack);
519 520 521 522 523 524 525 526 527 528 529
      } catch (newException, newStack) {
        if (newException != _currentError!.exception) {
          FlutterError.reportError(
            FlutterErrorDetails(
              exception: newException,
              library: 'image resource service',
              context: ErrorDescription('by a synchronously-called image error listener'),
              stack: newStack,
            ),
          );
        }
530
      }
531
    }
532 533
  }

534 535 536 537 538 539 540 541 542 543 544 545 546 547
  int _keepAliveHandles = 0;
  /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from
  /// being disposed at least until the handle is disposed.
  ///
  /// Such handles are useful when an image cache needs to keep a completer
  /// alive but does not itself have a listener subscribed, or when a widget
  /// that displays an image needs to temporarily unsubscribe from the completer
  /// but may re-subscribe in the future, for example when the [TickerMode]
  /// changes.
  ImageStreamCompleterHandle keepAlive() {
    _checkDisposed();
    return ImageStreamCompleterHandle._(this);
  }

548
  /// Stops the specified [listener] from receiving image stream events.
549
  ///
550 551
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
552 553 554
  ///
  /// Once all listeners have been removed and all [keepAlive] handles have been
  /// disposed, this image stream is no longer usable.
555
  void removeListener(ImageStreamListener listener) {
556
    _checkDisposed();
557
    for (int i = 0; i < _listeners.length; i += 1) {
558
      if (_listeners[i] == listener) {
559
        _listeners.removeAt(i);
560
        break;
561 562
      }
    }
Dan Field's avatar
Dan Field committed
563
    if (_listeners.isEmpty) {
564 565
      final List<VoidCallback> callbacks = _onLastListenerRemovedCallbacks.toList();
      for (final VoidCallback callback in callbacks) {
Dan Field's avatar
Dan Field committed
566 567 568
        callback();
      }
      _onLastListenerRemovedCallbacks.clear();
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
      _maybeDispose();
    }
  }

  bool _disposed = false;
  void _maybeDispose() {
    if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
      return;
    }

    _currentImage?.dispose();
    _currentImage = null;
    _disposed = true;
  }

  void _checkDisposed() {
    if (_disposed) {
      throw StateError(
        'Stream has been disposed.\n'
        'An ImageStream is considered disposed once at least one listener has '
        'been added and subsequently all listeners have been removed and no '
        'handles are outstanding from the keepAlive method.\n'
        'To resolve this error, maintain at least one listener on the stream, '
        'or create an ImageStreamCompleterHandle from the keepAlive '
        'method, or create a new stream for the image.',
      );
Dan Field's avatar
Dan Field committed
595 596 597 598 599 600
    }
  }

  final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];

  /// Adds a callback to call when [removeListener] results in an empty
601
  /// list of listeners and there are no [keepAlive] handles outstanding.
Dan Field's avatar
Dan Field committed
602 603 604 605
  ///
  /// This callback will never fire if [removeListener] is never called.
  void addOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
606
    _checkDisposed();
Dan Field's avatar
Dan Field committed
607
    _onLastListenerRemovedCallbacks.add(callback);
608 609
  }

610
  /// Removes a callback previously supplied to
611 612 613
  /// [addOnLastListenerRemovedCallback].
  void removeOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
614
    _checkDisposed();
615 616 617
    _onLastListenerRemovedCallbacks.remove(callback);
  }

618 619
  /// Calls all the registered listeners to notify them of a new image.
  @protected
620
  @pragma('vm:notify-debugger-on-exception')
621
  void setImage(ImageInfo image) {
622 623
    _checkDisposed();
    _currentImage?.dispose();
624
    _currentImage = image;
625

626 627
    if (_listeners.isEmpty)
      return;
628
    // Make a copy to allow for concurrent modification.
629 630
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.from(_listeners);
631
    for (final ImageStreamListener listener in localListeners) {
632
      try {
633
        listener.onImage(image.clone(), false);
634
      } catch (exception, stack) {
635
        reportError(
636
          context: ErrorDescription('by an image listener'),
637 638 639
          exception: exception,
          stack: stack,
        );
640 641 642 643
      }
    }
  }

644 645 646
  /// Calls all the registered error listeners to notify them of an error that
  /// occurred while resolving the image.
  ///
647
  /// If no error listeners (listeners with an [ImageStreamListener.onError]
648 649 650
  /// specified) are attached, or if the handlers all rethrow the exception
  /// verbatim (with `throw exception`), a [FlutterError] will be reported using
  /// [FlutterError.reportError].
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
  ///
  /// The `context` should be a string describing where the error was caught, in
  /// a form that will make sense in English when following the word "thrown",
  /// as in "thrown while obtaining the image from the network" (for the context
  /// "while obtaining the image from the network").
  ///
  /// The `exception` is the error being reported; the `stack` is the
  /// [StackTrace] associated with the exception.
  ///
  /// The `informationCollector` is a callback (of type [InformationCollector])
  /// that is called when the exception is used by [FlutterError.reportError].
  /// It is used to obtain further details to include in the logs, which may be
  /// expensive to collect, and thus should only be collected if the error is to
  /// be logged in the first place.
  ///
  /// The `silent` argument causes the exception to not be reported to the logs
  /// in release builds, if passed to [FlutterError.reportError]. (It is still
  /// sent to error handlers.) It should be set to true if the error is one that
  /// is expected to be encountered in release builds, for example network
  /// errors. That way, logs on end-user devices will not have spurious
  /// messages, but errors during development will still be reported.
  ///
  /// See [FlutterErrorDetails] for further details on these values.
674
  @pragma('vm:notify-debugger-on-exception')
675
  void reportError({
676
    DiagnosticsNode? context,
677
    required Object exception,
678 679
    StackTrace? stack,
    InformationCollector? informationCollector,
680 681
    bool silent = false,
  }) {
682
    _currentError = FlutterErrorDetails(
683 684 685
      exception: exception,
      stack: stack,
      library: 'image resource service',
686 687 688 689 690
      context: context,
      informationCollector: informationCollector,
      silent: silent,
    );

691
    // Make a copy to allow for concurrent modification.
692
    final List<ImageErrorListener> localErrorListeners = _listeners
693 694
        .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
        .whereType<ImageErrorListener>()
695
        .toList();
696

697 698 699 700 701 702 703
    bool handled = false;
    for (final ImageErrorListener errorListener in localErrorListeners) {
      try {
        errorListener(exception, stack);
        handled = true;
      } catch (newException, newStack) {
        if (newException != exception) {
704
          FlutterError.reportError(
705
            FlutterErrorDetails(
706
              context: ErrorDescription('when reporting an error to an image listener'),
707
              library: 'image resource service',
708 709
              exception: newException,
              stack: newStack,
710 711 712 713 714
            ),
          );
        }
      }
    }
715 716 717
    if (!handled) {
      FlutterError.reportError(_currentError!);
    }
718 719
  }

720 721 722 723
  /// Calls all the registered [ImageChunkListener]s (listeners with an
  /// [ImageStreamListener.onChunk] specified) to notify them of a new
  /// [ImageChunkEvent].
  @protected
724
  void reportImageChunkEvent(ImageChunkEvent event) {
725
    _checkDisposed();
726 727 728
    if (hasListeners) {
      // Make a copy to allow for concurrent modification.
      final List<ImageChunkListener> localListeners = _listeners
729 730
          .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
          .whereType<ImageChunkListener>()
731 732 733 734 735 736 737
          .toList();
      for (final ImageChunkListener listener in localListeners) {
        listener(event);
      }
    }
  }

738 739
  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
740 741 742
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
743
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
744
    description.add(ObjectFlagProperty<List<ImageStreamListener>>(
745 746
      'listeners',
      _listeners,
747
      ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
748
    ));
749
    description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
750 751 752
  }
}

753
/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
754 755 756 757 758 759 760
/// with only one frame).
class OneFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a manager for one-frame [ImageStream]s.
  ///
  /// The image resource awaits the given [Future]. When the future resolves,
  /// it notifies the [ImageListener]s that have been registered with
  /// [addListener].
761 762 763 764 765 766 767 768 769
  ///
  /// The [InformationCollector], if provided, is invoked if the given [Future]
  /// resolves with an error, and can be used to supplement the reported error
  /// message (for example, giving the image's URL).
  ///
  /// Errors are reported using [FlutterError.reportError] with the `silent`
  /// argument on [FlutterErrorDetails] set to true, meaning that by default the
  /// message is only dumped to the console in debug mode (see [new
  /// FlutterErrorDetails]).
770
  OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector })
771
      : assert(image != null) {
772
    image.then<void>(setImage, onError: (Object error, StackTrace stack) {
773
      reportError(
774
        context: ErrorDescription('resolving a single-frame image stream'),
775 776
        exception: error,
        stack: stack,
777 778
        informationCollector: informationCollector,
        silent: true,
779
      );
780
    });
781 782
  }
}
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806

/// Manages the decoding and scheduling of image frames.
///
/// New frames will only be emitted while there are registered listeners to the
/// stream (registered with [addListener]).
///
/// This class deals with 2 types of frames:
///
///  * image frames - image frames of an animated image.
///  * app frames - frames that the flutter engine is drawing to the screen to
///    show the app GUI.
///
/// For single frame images the stream will only complete once.
///
/// For animated images, this class eagerly decodes the next image frame,
/// and notifies the listeners that a new frame is ready on the first app frame
/// that is scheduled after the image frame duration has passed.
///
/// Scheduling new timers only from scheduled app frames, makes sure we pause
/// the animation when the app is not visible (as new app frames will not be
/// scheduled).
///
/// See the following timeline example:
///
807 808 809 810 811 812 813 814
///     | Time | Event                                      | Comment                   |
///     |------|--------------------------------------------|---------------------------|
///     | t1   | App frame scheduled (image frame A posted) |                           |
///     | t2   | App frame scheduled                        |                           |
///     | t3   | App frame scheduled                        |                           |
///     | t4   | Image frame B decoded                      |                           |
///     | t5   | App frame scheduled                        | t5 - t1 < frameB_duration |
///     | t6   | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
815 816 817 818 819 820
///
class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a image stream completer.
  ///
  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
821 822 823 824 825 826
  /// The `codec` parameter is a future for an initialized [ui.Codec] that will
  /// be used to decode the image.
  ///
  /// The `scale` parameter is the linear scale factor for drawing this frames
  /// of this image at their intended size.
  ///
827 828 829
  /// The `tag` parameter is passed on to created [ImageInfo] objects to
  /// help identify the source of the image.
  ///
830 831 832 833
  /// The `chunkEvents` parameter is an optional stream of notifications about
  /// the loading progress of the image. If this stream is provided, the events
  /// produced by the stream will be delivered to registered [ImageChunkListener]s
  /// (see [addListener]).
834
  MultiFrameImageStreamCompleter({
835 836 837 838 839
    required Future<ui.Codec> codec,
    required double scale,
    String? debugLabel,
    Stream<ImageChunkEvent>? chunkEvents,
    InformationCollector? informationCollector,
840 841
  }) : assert(codec != null),
       _informationCollector = informationCollector,
842
       _scale = scale {
843
    this.debugLabel = debugLabel;
844
    codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
845
      reportError(
846
        context: ErrorDescription('resolving an image codec'),
847 848 849 850
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
851
      );
852
    });
853
    if (chunkEvents != null) {
854
      chunkEvents.listen(reportImageChunkEvent,
855
        onError: (Object error, StackTrace stack) {
856 857 858 859 860 861 862 863 864 865
          reportError(
            context: ErrorDescription('loading an image'),
            exception: error,
            stack: stack,
            informationCollector: informationCollector,
            silent: true,
          );
        },
      );
    }
866 867
  }

868
  ui.Codec? _codec;
869
  final double _scale;
870 871
  final InformationCollector? _informationCollector;
  ui.FrameInfo? _nextFrame;
872
  // When the current was first shown.
873
  late Duration _shownTimestamp;
874
  // The requested duration for the current frame;
875
  Duration? _frameDuration;
876
  // How many frames have been emitted so far.
877
  int _framesEmitted = 0;
878
  Timer? _timer;
879

880 881 882
  // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  bool _frameCallbackScheduled = false;

883
  void _handleCodecReady(ui.Codec codec) {
884
    _codec = codec;
885 886
    assert(_codec != null);

887 888 889
    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
890 891 892
  }

  void _handleAppFrame(Duration timestamp) {
893
    _frameCallbackScheduled = false;
894
    if (!hasListeners)
895
      return;
896
    assert(_nextFrame != null);
897
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
898 899 900 901 902
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
903
      _shownTimestamp = timestamp;
904
      _frameDuration = _nextFrame!.duration;
905
      _nextFrame!.image.dispose();
906
      _nextFrame = null;
907 908
      final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
      if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
909 910 911 912
        _decodeNextFrameAndSchedule();
      }
      return;
    }
913
    final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
914
    _timer = Timer(delay * timeDilation, () {
915
      _scheduleAppFrame();
916 917 918 919 920 921 922 923
    });
  }

  bool _isFirstFrame() {
    return _frameDuration == null;
  }

  bool _hasFrameDurationPassed(Duration timestamp) {
924
    return timestamp - _shownTimestamp >= _frameDuration!;
925 926
  }

927
  Future<void> _decodeNextFrameAndSchedule() async {
928 929 930 931
    // This will be null if we gave it away. If not, it's still ours and it
    // must be disposed of.
    _nextFrame?.image.dispose();
    _nextFrame = null;
932
    try {
933
      _nextFrame = await _codec!.getNextFrame();
934
    } catch (exception, stack) {
935
      reportError(
936
        context: ErrorDescription('resolving an image frame'),
937 938 939 940 941
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
942 943
      return;
    }
944
    if (_codec!.frameCount == 1) {
945 946 947 948 949 950
      // ImageStreamCompleter listeners removed while waiting for next frame to
      // be decoded.
      // There's no reason to emit the frame without active listeners.
      if (!hasListeners) {
        return;
      }
951 952
      // This is not an animated image, just return it and don't schedule more
      // frames.
953 954 955 956 957 958 959
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
      _nextFrame!.image.dispose();
      _nextFrame = null;
960 961
      return;
    }
962 963 964 965 966 967 968 969
    _scheduleAppFrame();
  }

  void _scheduleAppFrame() {
    if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
970
    SchedulerBinding.instance!.scheduleFrameCallback(_handleAppFrame);
971 972 973 974 975 976 977 978
  }

  void _emitFrame(ImageInfo imageInfo) {
    setImage(imageInfo);
    _framesEmitted += 1;
  }

  @override
979
  void addListener(ImageStreamListener listener) {
980
    if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1))
981
      _decodeNextFrameAndSchedule();
982
    super.addListener(listener);
983 984 985
  }

  @override
986
  void removeListener(ImageStreamListener listener) {
987
    super.removeListener(listener);
988
    if (!hasListeners) {
989 990 991 992 993
      _timer?.cancel();
      _timer = null;
    }
  }
}