image_stream.dart 37.8 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
import 'dart:ui' as ui show Codec, FrameInfo, Image;
7 8

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

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

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
  /// 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
60
  /// logic is required when receiving a new image reference.
61 62 63 64
  ///
  /// {@tool snippet}
  ///
  /// The following sample shows how to appropriately check whether the
65 66
  /// [ImageInfo] reference refers to new image data or not (in this case in a
  /// setter).
67 68
  ///
  /// ```dart
69
  /// ImageInfo? get imageInfo => _imageInfo;
70 71
  /// ImageInfo? _imageInfo;
  /// set imageInfo (ImageInfo? value) {
72 73 74 75 76 77 78
  ///   // 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.
79
  ///   if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
80 81 82
  ///     value.dispose();
  ///     return;
  ///   }
83 84
  ///   // It is a new image. Dispose of the old one and take a reference
  ///   // to the new one.
85 86
  ///   _imageInfo?.dispose();
  ///   _imageInfo = value;
87 88
  ///   // Perform work to determine size, paint the image, etc.
  ///   // ...
89 90 91 92 93 94 95 96 97
  /// }
  /// ```
  /// {@end-tool}
  bool isCloneOf(ImageInfo other) {
    return other.image.isCloneOf(image)
        && scale == scale
        && other.debugLabel == debugLabel;
  }

98 99 100 101 102 103 104
  /// 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;

105 106 107
  /// The size of raw image pixels in bytes.
  int get sizeBytes => image.height * image.width * 4;

108 109 110 111
  /// The linear scale factor for drawing this image at its intended size.
  ///
  /// The scale factor applies to the width and the height.
  ///
112 113
  /// {@template flutter.painting.imageInfo.scale}
  /// For example, if this is 2.0, it means that there are four image pixels for
114
  /// every one logical pixel, and the image's actual width and height (as given
Dan Field's avatar
Dan Field committed
115 116 117
  /// 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]).
118
  /// {@endtemplate}
119 120
  final double scale;

121
  /// A string used for debugging purposes to identify the source of this image.
122
  final String? debugLabel;
123

124 125 126 127 128 129 130 131 132
  /// 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();
  }

133
  @override
134
  String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
135 136

  @override
137
  int get hashCode => Object.hash(image, scale, debugLabel);
138 139 140

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

151 152
/// Interface for receiving notifications about the loading of an image.
///
Dan Field's avatar
Dan Field committed
153
/// This class overrides [operator ==] and [hashCode] to compare the individual
154 155 156
/// 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
157
/// listener will be properly removed as long as all associated callbacks are
158 159 160 161 162 163 164 165 166 167
/// equal.
///
/// Used by [ImageStream] and [ImageStreamCompleter].
@immutable
class ImageStreamListener {
  /// Creates a new [ImageStreamListener].
  ///
  /// The [onImage] parameter must not be null.
  const ImageStreamListener(
    this.onImage, {
168
    this.onChunk,
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    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;

188 189 190 191 192 193 194 195 196 197 198
  /// 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).
199
  final ImageChunkListener? onChunk;
200

201 202 203 204
  /// 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].
205 206 207 208 209 210 211 212 213
  ///
  /// 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.
214
  final ImageErrorListener? onError;
215 216

  @override
217
  int get hashCode => Object.hash(onImage, onChunk, onError);
218 219

  @override
220
  bool operator ==(Object other) {
221
    if (other.runtimeType != runtimeType) {
222
      return false;
223
    }
224 225 226 227
    return other is ImageStreamListener
        && other.onImage == onImage
        && other.onChunk == onChunk
        && other.onError == onError;
228 229 230
  }
}

231 232
/// Signature for callbacks reporting that an image is available.
///
233
/// Used in [ImageStreamListener].
234
///
235 236 237 238
/// 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.
///
239
/// The `synchronousCall` argument is true if the listener is being invoked
240
/// during the call to `addListener`. This can be useful if, for example,
241 242 243 244
/// [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]).
245
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
246

247 248 249 250 251
/// Signature for listening to [ImageChunkEvent] events.
///
/// Used in [ImageStreamListener].
typedef ImageChunkListener = void Function(ImageChunkEvent event);

252 253
/// Signature for reporting errors when resolving images.
///
254 255
/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
/// [precacheImage], to report errors.
256
typedef ImageErrorListener = void Function(Object exception, StackTrace? stackTrace);
257

258 259 260 261 262 263 264 265 266 267
/// 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
268
class ImageChunkEvent with Diagnosticable {
269 270
  /// Creates a new chunk event.
  const ImageChunkEvent({
271 272
    required this.cumulativeBytesLoaded,
    required this.expectedTotalBytes,
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
  }) : 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.
290
  final int? expectedTotalBytes;
291 292 293 294 295 296 297 298 299

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

300 301
/// A handle to an image resource.
///
302
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
303 304 305 306 307 308 309 310
/// (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.
311
///
Dan Field's avatar
Dan Field committed
312 313
/// The [ImageCache] will consider an image to be live until the listener count
/// drops to zero after adding at least one listener. The
314 315
/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
/// tracking this information.
Dan Field's avatar
Dan Field committed
316
///
317 318 319 320
/// See also:
///
///  * [ImageProvider], which has an example that includes the use of an
///    [ImageStream] in a [Widget].
321
class ImageStream with Diagnosticable {
322 323 324 325 326 327 328 329
  /// 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.
330 331
  ImageStreamCompleter? get completer => _completer;
  ImageStreamCompleter? _completer;
332

333
  List<ImageStreamListener>? _listeners;
334 335 336 337 338

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  ///
  /// This is usually done automatically by the [ImageProvider] that created the
  /// [ImageStream].
339 340 341 342
  ///
  /// 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.
343 344 345 346
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
347
      final List<ImageStreamListener> initialListeners = _listeners!;
348
      _listeners = null;
349
      _completer!._addingInitialListeners = true;
350
      initialListeners.forEach(_completer!.addListener);
351
      _completer!._addingInitialListeners = false;
352 353 354
    }
  }

355
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
356 357
  /// object is available. If a concrete image is already available, this object
  /// will call the listener synchronously.
358 359 360 361
  ///
  /// If the assigned [completer] completes multiple images over its lifetime,
  /// this listener will fire multiple times.
  ///
362
  /// {@template flutter.painting.imageStream.addListener}
363 364 365 366
  /// 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.
367
  ///
368 369 370 371
  /// 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.
372 373 374
  ///
  /// When a `listener` receives an [ImageInfo] object, the `listener` is
  /// responsible for disposing of the [ImageInfo.image].
375 376
  /// {@endtemplate}
  void addListener(ImageStreamListener listener) {
377
    if (_completer != null) {
378
      return _completer!.addListener(listener);
379
    }
380
    _listeners ??= <ImageStreamListener>[];
381
    _listeners!.add(listener);
382 383
  }

384 385 386 387 388
  /// 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) {
389
    if (_completer != null) {
390
      return _completer!.removeListener(listener);
391
    }
392
    assert(_listeners != null);
393 394 395
    for (int i = 0; i < _listeners!.length; i += 1) {
      if (_listeners![i] == listener) {
        _listeners!.removeAt(i);
396
        break;
397 398
      }
    }
399 400 401 402 403
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
404 405
  /// This can be used to avoid un-registering and re-registering listeners
  /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
406 407 408 409 410 411
  /// [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.
412
  Object get key => _completer ?? this;
413

414 415 416
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
417
    properties.add(ObjectFlagProperty<ImageStreamCompleter>(
418 419 420 421 422
      'completer',
      _completer,
      ifPresent: _completer?.toStringShort(),
      ifNull: 'unresolved',
    ));
423
    properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
424 425 426 427
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
      ifNull: 'no listeners',
428
      level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
429 430
    ));
    _completer?.debugFillProperties(properties);
431 432 433
  }
}

434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
/// 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;
  }
}

465
/// Base class for those that manage the loading of [dart:ui.Image] objects for
466 467
/// [ImageStream]s.
///
468 469 470
/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
/// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible.
471
abstract class ImageStreamCompleter with Diagnosticable {
472
  final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
473 474
  ImageInfo? _currentImage;
  FlutterErrorDetails? _currentError;
475

476
  /// A string identifying the source of the underlying image.
477
  String? debugLabel;
478

479 480 481 482 483 484 485 486 487 488 489
  /// 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,
490 491 492 493
  /// 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.
494
  @protected
495
  @visibleForTesting
496 497
  bool get hasListeners => _listeners.isNotEmpty;

498 499 500 501
  /// We must avoid disposing a completer if it has never had a listener, even
  /// if all [keepAlive] handles get disposed.
  bool _hadAtLeastOneListener = false;

502 503 504 505 506 507 508 509 510
  /// Whether the future listeners added to this completer are initial listeners.
  ///
  /// This can be set to true when an [ImageStream] adds its initial listeners to
  /// this completer. This ultimately controls the synchronousCall parameter for
  /// the listener callbacks. When adding cached listeners to a completer,
  /// [_addingInitialListeners] can be set to false to indicate to the listeners
  /// that they are being called asynchronously.
  bool _addingInitialListeners = false;

511
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
512 513
  /// 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
514
  /// will notify the listener synchronously.
515
  ///
516
  /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
517
  /// this listener's [ImageStreamListener.onImage] will fire multiple times.
518
  ///
519 520
  /// {@macro flutter.painting.imageStream.addListener}
  void addListener(ImageStreamListener listener) {
521 522
    _checkDisposed();
    _hadAtLeastOneListener = true;
523
    _listeners.add(listener);
524
    if (_currentImage != null) {
525
      try {
526
        listener.onImage(_currentImage!.clone(), !_addingInitialListeners);
527
      } catch (exception, stack) {
528
        reportError(
529
          context: ErrorDescription('by a synchronously-called image listener'),
530 531 532
          exception: exception,
          stack: stack,
        );
533 534
      }
    }
535
    if (_currentError != null && listener.onError != null) {
536
      try {
537
        listener.onError!(_currentError!.exception, _currentError!.stack);
538 539 540 541 542 543 544 545 546 547 548
      } 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,
            ),
          );
        }
549
      }
550
    }
551 552
  }

553 554 555 556 557 558 559 560 561 562 563 564 565 566
  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);
  }

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

  bool _disposed = false;
593 594

  @mustCallSuper
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
  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
616 617 618 619 620 621
    }
  }

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

  /// Adds a callback to call when [removeListener] results in an empty
622
  /// list of listeners and there are no [keepAlive] handles outstanding.
Dan Field's avatar
Dan Field committed
623 624 625 626
  ///
  /// This callback will never fire if [removeListener] is never called.
  void addOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
627
    _checkDisposed();
Dan Field's avatar
Dan Field committed
628
    _onLastListenerRemovedCallbacks.add(callback);
629 630
  }

631
  /// Removes a callback previously supplied to
632 633 634
  /// [addOnLastListenerRemovedCallback].
  void removeOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
635
    _checkDisposed();
636 637 638
    _onLastListenerRemovedCallbacks.remove(callback);
  }

639 640
  /// Calls all the registered listeners to notify them of a new image.
  @protected
641
  @pragma('vm:notify-debugger-on-exception')
642
  void setImage(ImageInfo image) {
643 644
    _checkDisposed();
    _currentImage?.dispose();
645
    _currentImage = image;
646

647
    if (_listeners.isEmpty) {
648
      return;
649
    }
650
    // Make a copy to allow for concurrent modification.
651
    final List<ImageStreamListener> localListeners =
652
        List<ImageStreamListener>.of(_listeners);
653
    for (final ImageStreamListener listener in localListeners) {
654
      try {
655
        listener.onImage(image.clone(), false);
656
      } catch (exception, stack) {
657
        reportError(
658
          context: ErrorDescription('by an image listener'),
659 660 661
          exception: exception,
          stack: stack,
        );
662 663 664 665
      }
    }
  }

666 667 668
  /// Calls all the registered error listeners to notify them of an error that
  /// occurred while resolving the image.
  ///
669
  /// If no error listeners (listeners with an [ImageStreamListener.onError]
670 671 672
  /// specified) are attached, or if the handlers all rethrow the exception
  /// verbatim (with `throw exception`), a [FlutterError] will be reported using
  /// [FlutterError.reportError].
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
  ///
  /// 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.
696
  @pragma('vm:notify-debugger-on-exception')
697
  void reportError({
698
    DiagnosticsNode? context,
699
    required Object exception,
700 701
    StackTrace? stack,
    InformationCollector? informationCollector,
702 703
    bool silent = false,
  }) {
704
    _currentError = FlutterErrorDetails(
705 706 707
      exception: exception,
      stack: stack,
      library: 'image resource service',
708 709 710 711 712
      context: context,
      informationCollector: informationCollector,
      silent: silent,
    );

713
    // Make a copy to allow for concurrent modification.
714
    final List<ImageErrorListener> localErrorListeners = _listeners
715 716
        .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
        .whereType<ImageErrorListener>()
717
        .toList();
718

719 720 721 722 723 724 725
    bool handled = false;
    for (final ImageErrorListener errorListener in localErrorListeners) {
      try {
        errorListener(exception, stack);
        handled = true;
      } catch (newException, newStack) {
        if (newException != exception) {
726
          FlutterError.reportError(
727
            FlutterErrorDetails(
728
              context: ErrorDescription('when reporting an error to an image listener'),
729
              library: 'image resource service',
730 731
              exception: newException,
              stack: newStack,
732 733 734 735 736
            ),
          );
        }
      }
    }
737 738 739
    if (!handled) {
      FlutterError.reportError(_currentError!);
    }
740 741
  }

742 743 744 745
  /// Calls all the registered [ImageChunkListener]s (listeners with an
  /// [ImageStreamListener.onChunk] specified) to notify them of a new
  /// [ImageChunkEvent].
  @protected
746
  void reportImageChunkEvent(ImageChunkEvent event) {
747
    _checkDisposed();
748 749 750
    if (hasListeners) {
      // Make a copy to allow for concurrent modification.
      final List<ImageChunkListener> localListeners = _listeners
751 752
          .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
          .whereType<ImageChunkListener>()
753 754 755 756 757 758 759
          .toList();
      for (final ImageChunkListener listener in localListeners) {
        listener(event);
      }
    }
  }

760 761
  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
762 763 764
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
765
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
766
    description.add(ObjectFlagProperty<List<ImageStreamListener>>(
767 768
      'listeners',
      _listeners,
769
      ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
770
    ));
771
    description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
772 773 774
  }
}

775
/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
776 777 778 779 780 781 782
/// 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].
783 784 785 786 787 788 789
  ///
  /// 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
790
  /// message is only dumped to the console in debug mode (see [
791
  /// FlutterErrorDetails]).
792
  OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector })
793
      : assert(image != null) {
794
    image.then<void>(setImage, onError: (Object error, StackTrace stack) {
795
      reportError(
796
        context: ErrorDescription('resolving a single-frame image stream'),
797 798
        exception: error,
        stack: stack,
799 800
        informationCollector: informationCollector,
        silent: true,
801
      );
802
    });
803 804
  }
}
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828

/// 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:
///
829 830 831 832 833 834 835 836
///     | 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 |
837 838 839 840 841 842
///
class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a image stream completer.
  ///
  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
843 844 845 846 847 848
  /// 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.
  ///
849 850 851
  /// The `tag` parameter is passed on to created [ImageInfo] objects to
  /// help identify the source of the image.
  ///
852 853 854 855
  /// 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]).
856
  MultiFrameImageStreamCompleter({
857 858 859 860 861
    required Future<ui.Codec> codec,
    required double scale,
    String? debugLabel,
    Stream<ImageChunkEvent>? chunkEvents,
    InformationCollector? informationCollector,
862 863
  }) : assert(codec != null),
       _informationCollector = informationCollector,
864
       _scale = scale {
865
    this.debugLabel = debugLabel;
866
    codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
867
      reportError(
868
        context: ErrorDescription('resolving an image codec'),
869 870 871 872
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
873
      );
874
    });
875
    if (chunkEvents != null) {
876
      _chunkSubscription = chunkEvents.listen(reportImageChunkEvent,
877
        onError: (Object error, StackTrace stack) {
878 879 880 881 882 883 884 885 886 887
          reportError(
            context: ErrorDescription('loading an image'),
            exception: error,
            stack: stack,
            informationCollector: informationCollector,
            silent: true,
          );
        },
      );
    }
888 889
  }

890
  StreamSubscription<ImageChunkEvent>? _chunkSubscription;
891
  ui.Codec? _codec;
892
  final double _scale;
893 894
  final InformationCollector? _informationCollector;
  ui.FrameInfo? _nextFrame;
895
  // When the current was first shown.
896
  late Duration _shownTimestamp;
897
  // The requested duration for the current frame;
898
  Duration? _frameDuration;
899
  // How many frames have been emitted so far.
900
  int _framesEmitted = 0;
901
  Timer? _timer;
902

903 904 905
  // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  bool _frameCallbackScheduled = false;

906
  void _handleCodecReady(ui.Codec codec) {
907
    _codec = codec;
908 909
    assert(_codec != null);

910 911 912
    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
913 914 915
  }

  void _handleAppFrame(Duration timestamp) {
916
    _frameCallbackScheduled = false;
917
    if (!hasListeners) {
918
      return;
919
    }
920
    assert(_nextFrame != null);
921
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
922 923 924 925 926
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
927
      _shownTimestamp = timestamp;
928
      _frameDuration = _nextFrame!.duration;
929
      _nextFrame!.image.dispose();
930
      _nextFrame = null;
931 932
      final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
      if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
933 934 935 936
        _decodeNextFrameAndSchedule();
      }
      return;
    }
937
    final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
938
    _timer = Timer(delay * timeDilation, () {
939
      _scheduleAppFrame();
940 941 942 943 944 945 946 947
    });
  }

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

  bool _hasFrameDurationPassed(Duration timestamp) {
948
    return timestamp - _shownTimestamp >= _frameDuration!;
949 950
  }

951
  Future<void> _decodeNextFrameAndSchedule() async {
952 953 954 955
    // 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;
956
    try {
957
      _nextFrame = await _codec!.getNextFrame();
958
    } catch (exception, stack) {
959
      reportError(
960
        context: ErrorDescription('resolving an image frame'),
961 962 963 964 965
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
966 967
      return;
    }
968
    if (_codec!.frameCount == 1) {
969 970 971 972 973 974
      // 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;
      }
975 976
      // This is not an animated image, just return it and don't schedule more
      // frames.
977 978 979 980 981 982 983
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
      _nextFrame!.image.dispose();
      _nextFrame = null;
984 985
      return;
    }
986 987 988 989 990 991 992 993
    _scheduleAppFrame();
  }

  void _scheduleAppFrame() {
    if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
994
    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
995 996 997 998 999 1000 1001 1002
  }

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

  @override
1003
  void addListener(ImageStreamListener listener) {
1004
    if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) {
1005
      _decodeNextFrameAndSchedule();
1006
    }
1007
    super.addListener(listener);
1008 1009 1010
  }

  @override
1011
  void removeListener(ImageStreamListener listener) {
1012
    super.removeListener(listener);
1013
    if (!hasListeners) {
1014 1015 1016 1017
      _timer?.cancel();
      _timer = null;
    }
  }
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027

  @override
  void _maybeDispose() {
    super._maybeDispose();
    if (_disposed) {
      _chunkSubscription?.onData(null);
      _chunkSubscription?.cancel();
      _chunkSubscription = null;
    }
  }
1028
}