image_stream.dart 37.6 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 Image, Codec, FrameInfo;
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 65 66 67
  ///
  /// {@tool snippet}
  ///
  /// The following sample shows how to appropriately check whether the
  /// [ImageInfo] reference refers to new image data or not.
  ///
  /// ```dart
68 69
  /// ImageInfo? _imageInfo;
  /// set imageInfo (ImageInfo? value) {
70 71 72 73 74 75 76
  ///   // 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.
77
  ///   if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
  ///     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;
  }

93 94 95 96 97 98 99
  /// 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;

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

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

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

119 120 121 122 123 124 125 126 127
  /// 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();
  }

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

  @override
132
  int get hashCode => Object.hash(image, scale, debugLabel);
133 134 135 136 137

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

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

182 183 184 185 186 187 188 189 190 191 192
  /// 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).
193
  final ImageChunkListener? onChunk;
194

195 196 197 198
  /// 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].
199 200 201 202 203 204 205 206 207
  ///
  /// 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.
208
  final ImageErrorListener? onError;
209 210

  @override
211
  int get hashCode => Object.hash(onImage, onChunk, onError);
212 213

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

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

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

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

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

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

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

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

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

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

376 377 378 379 380
  /// 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) {
381
    if (_completer != null)
382
      return _completer!.removeListener(listener);
383
    assert(_listeners != null);
384 385 386
    for (int i = 0; i < _listeners!.length; i += 1) {
      if (_listeners![i] == listener) {
        _listeners!.removeAt(i);
387
        break;
388 389
      }
    }
390 391 392 393 394
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
395 396
  /// This can be used to avoid un-registering and re-registering listeners
  /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
397 398 399 400 401 402
  /// [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.
403
  Object get key => _completer ?? this;
404

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

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

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

470 471 472 473 474 475 476 477 478 479 480
  /// 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,
481 482 483 484
  /// 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.
485
  @protected
486
  @visibleForTesting
487 488
  bool get hasListeners => _listeners.isNotEmpty;

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

493 494 495 496 497 498 499 500 501
  /// 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;

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

544 545 546 547 548 549 550 551 552 553 554 555 556 557
  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);
  }

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

  bool _disposed = false;
584 585

  @mustCallSuper
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
  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
607 608 609 610 611 612
    }
  }

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

  /// Adds a callback to call when [removeListener] results in an empty
613
  /// list of listeners and there are no [keepAlive] handles outstanding.
Dan Field's avatar
Dan Field committed
614 615 616 617
  ///
  /// This callback will never fire if [removeListener] is never called.
  void addOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
618
    _checkDisposed();
Dan Field's avatar
Dan Field committed
619
    _onLastListenerRemovedCallbacks.add(callback);
620 621
  }

622
  /// Removes a callback previously supplied to
623 624 625
  /// [addOnLastListenerRemovedCallback].
  void removeOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
626
    _checkDisposed();
627 628 629
    _onLastListenerRemovedCallbacks.remove(callback);
  }

630 631
  /// Calls all the registered listeners to notify them of a new image.
  @protected
632
  @pragma('vm:notify-debugger-on-exception')
633
  void setImage(ImageInfo image) {
634 635
    _checkDisposed();
    _currentImage?.dispose();
636
    _currentImage = image;
637

638 639
    if (_listeners.isEmpty)
      return;
640
    // Make a copy to allow for concurrent modification.
641
    final List<ImageStreamListener> localListeners =
642
        List<ImageStreamListener>.of(_listeners);
643
    for (final ImageStreamListener listener in localListeners) {
644
      try {
645
        listener.onImage(image.clone(), false);
646
      } catch (exception, stack) {
647
        reportError(
648
          context: ErrorDescription('by an image listener'),
649 650 651
          exception: exception,
          stack: stack,
        );
652 653 654 655
      }
    }
  }

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

703
    // Make a copy to allow for concurrent modification.
704
    final List<ImageErrorListener> localErrorListeners = _listeners
705 706
        .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
        .whereType<ImageErrorListener>()
707
        .toList();
708

709 710 711 712 713 714 715
    bool handled = false;
    for (final ImageErrorListener errorListener in localErrorListeners) {
      try {
        errorListener(exception, stack);
        handled = true;
      } catch (newException, newStack) {
        if (newException != exception) {
716
          FlutterError.reportError(
717
            FlutterErrorDetails(
718
              context: ErrorDescription('when reporting an error to an image listener'),
719
              library: 'image resource service',
720 721
              exception: newException,
              stack: newStack,
722 723 724 725 726
            ),
          );
        }
      }
    }
727 728 729
    if (!handled) {
      FlutterError.reportError(_currentError!);
    }
730 731
  }

732 733 734 735
  /// Calls all the registered [ImageChunkListener]s (listeners with an
  /// [ImageStreamListener.onChunk] specified) to notify them of a new
  /// [ImageChunkEvent].
  @protected
736
  void reportImageChunkEvent(ImageChunkEvent event) {
737
    _checkDisposed();
738 739 740
    if (hasListeners) {
      // Make a copy to allow for concurrent modification.
      final List<ImageChunkListener> localListeners = _listeners
741 742
          .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
          .whereType<ImageChunkListener>()
743 744 745 746 747 748 749
          .toList();
      for (final ImageChunkListener listener in localListeners) {
        listener(event);
      }
    }
  }

750 751
  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
752 753 754
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
755
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
756
    description.add(ObjectFlagProperty<List<ImageStreamListener>>(
757 758
      'listeners',
      _listeners,
759
      ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
760
    ));
761
    description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
762 763 764
  }
}

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

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

880
  StreamSubscription<ImageChunkEvent>? _chunkSubscription;
881
  ui.Codec? _codec;
882
  final double _scale;
883 884
  final InformationCollector? _informationCollector;
  ui.FrameInfo? _nextFrame;
885
  // When the current was first shown.
886
  late Duration _shownTimestamp;
887
  // The requested duration for the current frame;
888
  Duration? _frameDuration;
889
  // How many frames have been emitted so far.
890
  int _framesEmitted = 0;
891
  Timer? _timer;
892

893 894 895
  // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  bool _frameCallbackScheduled = false;

896
  void _handleCodecReady(ui.Codec codec) {
897
    _codec = codec;
898 899
    assert(_codec != null);

900 901 902
    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
903 904 905
  }

  void _handleAppFrame(Duration timestamp) {
906
    _frameCallbackScheduled = false;
907
    if (!hasListeners)
908
      return;
909
    assert(_nextFrame != null);
910
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
911 912 913 914 915
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
916
      _shownTimestamp = timestamp;
917
      _frameDuration = _nextFrame!.duration;
918
      _nextFrame!.image.dispose();
919
      _nextFrame = null;
920 921
      final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
      if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
922 923 924 925
        _decodeNextFrameAndSchedule();
      }
      return;
    }
926
    final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
927
    _timer = Timer(delay * timeDilation, () {
928
      _scheduleAppFrame();
929 930 931 932 933 934 935 936
    });
  }

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

  bool _hasFrameDurationPassed(Duration timestamp) {
937
    return timestamp - _shownTimestamp >= _frameDuration!;
938 939
  }

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

  void _scheduleAppFrame() {
    if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
983
    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
984 985 986 987 988 989 990 991
  }

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

  @override
992
  void addListener(ImageStreamListener listener) {
993
    if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1))
994
      _decodeNextFrameAndSchedule();
995
    super.addListener(listener);
996 997 998
  }

  @override
999
  void removeListener(ImageStreamListener listener) {
1000
    super.removeListener(listener);
1001
    if (!hasListeners) {
1002 1003 1004 1005
      _timer?.cancel();
      _timer = null;
    }
  }
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

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