"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter/lib/src/painting/image_stream.dart" (13 Nov 2020, 29402 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 
    6 import 'dart:async';
    7 import 'dart:ui' as ui show Image, Codec, FrameInfo;
    8 import 'dart:ui' show hashValues;
    9 
   10 import 'package:flutter/foundation.dart';
   11 import 'package:flutter/scheduler.dart';
   12 
   13 /// A [dart:ui.Image] object with its corresponding scale.
   14 ///
   15 /// ImageInfo objects are used by [ImageStream] objects to represent the
   16 /// actual data of the image once it has been obtained.
   17 @immutable
   18 class ImageInfo {
   19   /// Creates an [ImageInfo] object for the given [image] and [scale].
   20   ///
   21   /// Both the image and the scale must not be null.
   22   ///
   23   /// The tag may be used to identify the source of this image.
   24   const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
   25     : assert(image != null),
   26       assert(scale != null);
   27 
   28   /// The raw image pixels.
   29   ///
   30   /// This is the object to pass to the [Canvas.drawImage],
   31   /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
   32   /// the image.
   33   final ui.Image image;
   34 
   35   /// The linear scale factor for drawing this image at its intended size.
   36   ///
   37   /// The scale factor applies to the width and the height.
   38   ///
   39   /// For example, if this is 2.0 it means that there are four image pixels for
   40   /// every one logical pixel, and the image's actual width and height (as given
   41   /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are
   42   /// double the height and width that should be used when painting the image
   43   /// (e.g. in the arguments given to [Canvas.drawImage]).
   44   final double scale;
   45 
   46   /// A string used for debugging purpopses to identify the source of this image.
   47   final String? debugLabel;
   48 
   49   @override
   50   String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
   51 
   52   @override
   53   int get hashCode => hashValues(image, scale, debugLabel);
   54 
   55   @override
   56   bool operator ==(Object other) {
   57     if (other.runtimeType != runtimeType)
   58       return false;
   59     return other is ImageInfo
   60         && other.image == image
   61         && other.scale == scale
   62         && other.debugLabel == debugLabel;
   63   }
   64 }
   65 
   66 /// Interface for receiving notifications about the loading of an image.
   67 ///
   68 /// This class overrides [operator ==] and [hashCode] to compare the individual
   69 /// callbacks in the listener, meaning that if you add an instance of this class
   70 /// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
   71 /// _different_ instance of this class when you remove the listener, and the
   72 /// listener will be properly removed as long as all associated callbacks are
   73 /// equal.
   74 ///
   75 /// Used by [ImageStream] and [ImageStreamCompleter].
   76 @immutable
   77 class ImageStreamListener {
   78   /// Creates a new [ImageStreamListener].
   79   ///
   80   /// The [onImage] parameter must not be null.
   81   const ImageStreamListener(
   82     this.onImage, {
   83     this.onChunk,
   84     this.onError,
   85   }) : assert(onImage != null);
   86 
   87   /// Callback for getting notified that an image is available.
   88   ///
   89   /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
   90   /// that drives the notifications fires multiple times). An example of such a
   91   /// case would be an image with multiple frames within it (such as an animated
   92   /// GIF).
   93   ///
   94   /// For more information on how to interpret the parameters to the callback,
   95   /// see the documentation on [ImageListener].
   96   ///
   97   /// See also:
   98   ///
   99   ///  * [onError], which will be called instead of [onImage] if an error occurs
  100   ///    during loading.
  101   final ImageListener onImage;
  102 
  103   /// Callback for getting notified when a chunk of bytes has been received
  104   /// during the loading of the image.
  105   ///
  106   /// This callback may fire many times (e.g. when used with a [NetworkImage],
  107   /// where the image bytes are loaded incrementally over the wire) or not at
  108   /// all (e.g. when used with a [MemoryImage], where the image bytes are
  109   /// already available in memory).
  110   ///
  111   /// This callback may also continue to fire after the [onImage] callback has
  112   /// fired (e.g. for multi-frame images that continue to load after the first
  113   /// frame is available).
  114   final ImageChunkListener? onChunk;
  115 
  116   /// Callback for getting notified when an error occurs while loading an image.
  117   ///
  118   /// If an error occurs during loading, [onError] will be called instead of
  119   /// [onImage].
  120   final ImageErrorListener? onError;
  121 
  122   @override
  123   int get hashCode => hashValues(onImage, onChunk, onError);
  124 
  125   @override
  126   bool operator ==(Object other) {
  127     if (other.runtimeType != runtimeType)
  128       return false;
  129     return other is ImageStreamListener
  130         && other.onImage == onImage
  131         && other.onChunk == onChunk
  132         && other.onError == onError;
  133   }
  134 }
  135 
  136 /// Signature for callbacks reporting that an image is available.
  137 ///
  138 /// Used in [ImageStreamListener].
  139 ///
  140 /// The `synchronousCall` argument is true if the listener is being invoked
  141 /// during the call to `addListener`. This can be useful if, for example,
  142 /// [ImageStream.addListener] is invoked during a frame, so that a new rendering
  143 /// frame is requested if the call was asynchronous (after the current frame)
  144 /// and no rendering frame is requested if the call was synchronous (within the
  145 /// same stack frame as the call to [ImageStream.addListener]).
  146 typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
  147 
  148 /// Signature for listening to [ImageChunkEvent] events.
  149 ///
  150 /// Used in [ImageStreamListener].
  151 typedef ImageChunkListener = void Function(ImageChunkEvent event);
  152 
  153 /// Signature for reporting errors when resolving images.
  154 ///
  155 /// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
  156 /// [precacheImage], to report errors.
  157 typedef ImageErrorListener = void Function(dynamic exception, StackTrace? stackTrace);
  158 
  159 /// An immutable notification of image bytes that have been incrementally loaded.
  160 ///
  161 /// Chunk events represent progress notifications while an image is being
  162 /// loaded (e.g. from disk or over the network).
  163 ///
  164 /// See also:
  165 ///
  166 ///  * [ImageChunkListener], the means by which callers get notified of
  167 ///    these events.
  168 @immutable
  169 class ImageChunkEvent with Diagnosticable {
  170   /// Creates a new chunk event.
  171   const ImageChunkEvent({
  172     required this.cumulativeBytesLoaded,
  173     required this.expectedTotalBytes,
  174   }) : assert(cumulativeBytesLoaded >= 0),
  175        assert(expectedTotalBytes == null || expectedTotalBytes >= 0);
  176 
  177   /// The number of bytes that have been received across the wire thus far.
  178   final int cumulativeBytesLoaded;
  179 
  180   /// The expected number of bytes that need to be received to finish loading
  181   /// the image.
  182   ///
  183   /// This value is not necessarily equal to the expected _size_ of the image
  184   /// in bytes, as the bytes required to load the image may be compressed.
  185   ///
  186   /// This value will be null if the number is not known in advance.
  187   ///
  188   /// When this value is null, the chunk event may still be useful as an
  189   /// indication that data is loading (and how much), but it cannot represent a
  190   /// loading completion percentage.
  191   final int? expectedTotalBytes;
  192 
  193   @override
  194   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  195     super.debugFillProperties(properties);
  196     properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
  197     properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
  198   }
  199 }
  200 
  201 /// A handle to an image resource.
  202 ///
  203 /// ImageStream represents a handle to a [dart:ui.Image] object and its scale
  204 /// (together represented by an [ImageInfo] object). The underlying image object
  205 /// might change over time, either because the image is animating or because the
  206 /// underlying image resource was mutated.
  207 ///
  208 /// ImageStream objects can also represent an image that hasn't finished
  209 /// loading.
  210 ///
  211 /// ImageStream objects are backed by [ImageStreamCompleter] objects.
  212 ///
  213 /// The [ImageCache] will consider an image to be live until the listener count
  214 /// drops to zero after adding at least one listener. The
  215 /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
  216 /// tracking this information.
  217 ///
  218 /// See also:
  219 ///
  220 ///  * [ImageProvider], which has an example that includes the use of an
  221 ///    [ImageStream] in a [Widget].
  222 class ImageStream with Diagnosticable {
  223   /// Create an initially unbound image stream.
  224   ///
  225   /// Once an [ImageStreamCompleter] is available, call [setCompleter].
  226   ImageStream();
  227 
  228   /// The completer that has been assigned to this image stream.
  229   ///
  230   /// Generally there is no need to deal with the completer directly.
  231   ImageStreamCompleter? get completer => _completer;
  232   ImageStreamCompleter? _completer;
  233 
  234   List<ImageStreamListener>? _listeners;
  235 
  236   /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  237   ///
  238   /// This is usually done automatically by the [ImageProvider] that created the
  239   /// [ImageStream].
  240   ///
  241   /// This method can only be called once per stream. To have an [ImageStream]
  242   /// represent multiple images over time, assign it a completer that
  243   /// completes several images in succession.
  244   void setCompleter(ImageStreamCompleter value) {
  245     assert(_completer == null);
  246     _completer = value;
  247     if (_listeners != null) {
  248       final List<ImageStreamListener> initialListeners = _listeners!;
  249       _listeners = null;
  250       initialListeners.forEach(_completer!.addListener);
  251     }
  252   }
  253 
  254   /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
  255   /// object is available. If a concrete image is already available, this object
  256   /// will call the listener synchronously.
  257   ///
  258   /// If the assigned [completer] completes multiple images over its lifetime,
  259   /// this listener will fire multiple times.
  260   ///
  261   /// {@template flutter.painting.imageStream.addListener}
  262   /// The listener will be passed a flag indicating whether a synchronous call
  263   /// occurred. If the listener is added within a render object paint function,
  264   /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
  265   /// a paint.
  266   ///
  267   /// If a duplicate `listener` is registered N times, then it will be called N
  268   /// times when the image stream completes (whether because a new image is
  269   /// available or because an error occurs). Likewise, to remove all instances
  270   /// of the listener, [removeListener] would need to called N times as well.
  271   /// {@endtemplate}
  272   void addListener(ImageStreamListener listener) {
  273     if (_completer != null)
  274       return _completer!.addListener(listener);
  275     _listeners ??= <ImageStreamListener>[];
  276     _listeners!.add(listener);
  277   }
  278 
  279   /// Stops listening for events from this stream's [ImageStreamCompleter].
  280   ///
  281   /// If [listener] has been added multiple times, this removes the _first_
  282   /// instance of the listener.
  283   void removeListener(ImageStreamListener listener) {
  284     if (_completer != null)
  285       return _completer!.removeListener(listener);
  286     assert(_listeners != null);
  287     for (int i = 0; i < _listeners!.length; i += 1) {
  288       if (_listeners![i] == listener) {
  289         _listeners!.removeAt(i);
  290         break;
  291       }
  292     }
  293   }
  294 
  295   /// Returns an object which can be used with `==` to determine if this
  296   /// [ImageStream] shares the same listeners list as another [ImageStream].
  297   ///
  298   /// This can be used to avoid un-registering and re-registering listeners
  299   /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
  300   /// [ImageProvider].
  301   ///
  302   /// The key may change once in the lifetime of the object. When it changes, it
  303   /// will go from being different than other [ImageStream]'s keys to
  304   /// potentially being the same as others'. No notification is sent when this
  305   /// happens.
  306   Object get key => _completer ?? this;
  307 
  308   @override
  309   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  310     super.debugFillProperties(properties);
  311     properties.add(ObjectFlagProperty<ImageStreamCompleter>(
  312       'completer',
  313       _completer,
  314       ifPresent: _completer?.toStringShort(),
  315       ifNull: 'unresolved',
  316     ));
  317     properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
  318       'listeners',
  319       _listeners,
  320       ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
  321       ifNull: 'no listeners',
  322       level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
  323     ));
  324     _completer?.debugFillProperties(properties);
  325   }
  326 }
  327 
  328 /// Base class for those that manage the loading of [dart:ui.Image] objects for
  329 /// [ImageStream]s.
  330 ///
  331 /// [ImageStreamListener] objects are rarely constructed directly. Generally, an
  332 /// [ImageProvider] subclass will return an [ImageStream] and automatically
  333 /// configure it with the right [ImageStreamCompleter] when possible.
  334 abstract class ImageStreamCompleter with Diagnosticable {
  335   final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
  336   ImageInfo? _currentImage;
  337   FlutterErrorDetails? _currentError;
  338 
  339   /// A string identifying the source of the underlying image.
  340   String? debugLabel;
  341 
  342   /// Whether any listeners are currently registered.
  343   ///
  344   /// Clients should not depend on this value for their behavior, because having
  345   /// one listener's logic change when another listener happens to start or stop
  346   /// listening will lead to extremely hard-to-track bugs. Subclasses might use
  347   /// this information to determine whether to do any work when there are no
  348   /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
  349   /// to determine when to iterate through frames of an animated image.
  350   ///
  351   /// Typically this is used by overriding [addListener], checking if
  352   /// [hasListeners] is false before calling `super.addListener()`, and if so,
  353   /// starting whatever work is needed to determine when to notify listeners;
  354   /// and similarly, by overriding [removeListener], checking if [hasListeners]
  355   /// is false after calling `super.removeListener()`, and if so, stopping that
  356   /// same work.
  357   @protected
  358   @visibleForTesting
  359   bool get hasListeners => _listeners.isNotEmpty;
  360 
  361   /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
  362   /// object is available or an error is reported. If a concrete image is
  363   /// already available, or if an error has been already reported, this object
  364   /// will notify the listener synchronously.
  365   ///
  366   /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
  367   /// this listener's [ImageStreamListener.onImage] will fire multiple times.
  368   ///
  369   /// {@macro flutter.painting.imageStream.addListener}
  370   void addListener(ImageStreamListener listener) {
  371     _listeners.add(listener);
  372     if (_currentImage != null) {
  373       try {
  374         listener.onImage(_currentImage!, true);
  375       } catch (exception, stack) {
  376         reportError(
  377           context: ErrorDescription('by a synchronously-called image listener'),
  378           exception: exception,
  379           stack: stack,
  380         );
  381       }
  382     }
  383     if (_currentError != null && listener.onError != null) {
  384       try {
  385         listener.onError!(_currentError!.exception, _currentError!.stack);
  386       } catch (exception, stack) {
  387         FlutterError.reportError(
  388           FlutterErrorDetails(
  389             exception: exception,
  390             library: 'image resource service',
  391             context: ErrorDescription('by a synchronously-called image error listener'),
  392             stack: stack,
  393           ),
  394         );
  395       }
  396     }
  397   }
  398 
  399   /// Stops the specified [listener] from receiving image stream events.
  400   ///
  401   /// If [listener] has been added multiple times, this removes the _first_
  402   /// instance of the listener.
  403   void removeListener(ImageStreamListener listener) {
  404     for (int i = 0; i < _listeners.length; i += 1) {
  405       if (_listeners[i] == listener) {
  406         _listeners.removeAt(i);
  407         break;
  408       }
  409     }
  410     if (_listeners.isEmpty) {
  411       for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
  412         callback();
  413       }
  414       _onLastListenerRemovedCallbacks.clear();
  415     }
  416   }
  417 
  418   final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];
  419 
  420   /// Adds a callback to call when [removeListener] results in an empty
  421   /// list of listeners.
  422   ///
  423   /// This callback will never fire if [removeListener] is never called.
  424   void addOnLastListenerRemovedCallback(VoidCallback callback) {
  425     assert(callback != null);
  426     _onLastListenerRemovedCallbacks.add(callback);
  427   }
  428 
  429   /// Removes a callback previously supplied to
  430   /// [addOnLastListenerRemovedCallback].
  431   void removeOnLastListenerRemovedCallback(VoidCallback callback) {
  432     assert(callback != null);
  433     _onLastListenerRemovedCallbacks.remove(callback);
  434   }
  435 
  436   /// Calls all the registered listeners to notify them of a new image.
  437   @protected
  438   void setImage(ImageInfo image) {
  439     _currentImage = image;
  440     if (_listeners.isEmpty)
  441       return;
  442     // Make a copy to allow for concurrent modification.
  443     final List<ImageStreamListener> localListeners =
  444         List<ImageStreamListener>.from(_listeners);
  445     for (final ImageStreamListener listener in localListeners) {
  446       try {
  447         listener.onImage(image, false);
  448       } catch (exception, stack) {
  449         reportError(
  450           context: ErrorDescription('by an image listener'),
  451           exception: exception,
  452           stack: stack,
  453         );
  454       }
  455     }
  456   }
  457 
  458   /// Calls all the registered error listeners to notify them of an error that
  459   /// occurred while resolving the image.
  460   ///
  461   /// If no error listeners (listeners with an [ImageStreamListener.onError]
  462   /// specified) are attached, a [FlutterError] will be reported instead.
  463   ///
  464   /// The `context` should be a string describing where the error was caught, in
  465   /// a form that will make sense in English when following the word "thrown",
  466   /// as in "thrown while obtaining the image from the network" (for the context
  467   /// "while obtaining the image from the network").
  468   ///
  469   /// The `exception` is the error being reported; the `stack` is the
  470   /// [StackTrace] associated with the exception.
  471   ///
  472   /// The `informationCollector` is a callback (of type [InformationCollector])
  473   /// that is called when the exception is used by [FlutterError.reportError].
  474   /// It is used to obtain further details to include in the logs, which may be
  475   /// expensive to collect, and thus should only be collected if the error is to
  476   /// be logged in the first place.
  477   ///
  478   /// The `silent` argument causes the exception to not be reported to the logs
  479   /// in release builds, if passed to [FlutterError.reportError]. (It is still
  480   /// sent to error handlers.) It should be set to true if the error is one that
  481   /// is expected to be encountered in release builds, for example network
  482   /// errors. That way, logs on end-user devices will not have spurious
  483   /// messages, but errors during development will still be reported.
  484   ///
  485   /// See [FlutterErrorDetails] for further details on these values.
  486   @protected
  487   void reportError({
  488     DiagnosticsNode? context,
  489     dynamic exception,
  490     StackTrace? stack,
  491     InformationCollector? informationCollector,
  492     bool silent = false,
  493   }) {
  494     _currentError = FlutterErrorDetails(
  495       exception: exception,
  496       stack: stack,
  497       library: 'image resource service',
  498       context: context,
  499       informationCollector: informationCollector,
  500       silent: silent,
  501     );
  502 
  503     // Make a copy to allow for concurrent modification.
  504     final List<ImageErrorListener> localErrorListeners = _listeners
  505         .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
  506         .whereType<ImageErrorListener>()
  507         .toList();
  508 
  509     if (localErrorListeners.isEmpty) {
  510       FlutterError.reportError(_currentError!);
  511     } else {
  512       for (final ImageErrorListener errorListener in localErrorListeners) {
  513         try {
  514           errorListener(exception, stack);
  515         } catch (exception, stack) {
  516           FlutterError.reportError(
  517             FlutterErrorDetails(
  518               context: ErrorDescription('when reporting an error to an image listener'),
  519               library: 'image resource service',
  520               exception: exception,
  521               stack: stack,
  522             ),
  523           );
  524         }
  525       }
  526     }
  527   }
  528 
  529   /// Calls all the registered [ImageChunkListener]s (listeners with an
  530   /// [ImageStreamListener.onChunk] specified) to notify them of a new
  531   /// [ImageChunkEvent].
  532   @protected
  533   void reportImageChunkEvent(ImageChunkEvent event){
  534     if (hasListeners) {
  535       // Make a copy to allow for concurrent modification.
  536       final List<ImageChunkListener> localListeners = _listeners
  537           .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
  538           .whereType<ImageChunkListener>()
  539           .toList();
  540       for (final ImageChunkListener listener in localListeners) {
  541         listener(event);
  542       }
  543     }
  544   }
  545 
  546   /// Accumulates a list of strings describing the object's state. Subclasses
  547   /// should override this to have their information included in [toString].
  548   @override
  549   void debugFillProperties(DiagnosticPropertiesBuilder description) {
  550     super.debugFillProperties(description);
  551     description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
  552     description.add(ObjectFlagProperty<List<ImageStreamListener>>(
  553       'listeners',
  554       _listeners,
  555       ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
  556     ));
  557   }
  558 }
  559 
  560 /// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
  561 /// with only one frame).
  562 class OneFrameImageStreamCompleter extends ImageStreamCompleter {
  563   /// Creates a manager for one-frame [ImageStream]s.
  564   ///
  565   /// The image resource awaits the given [Future]. When the future resolves,
  566   /// it notifies the [ImageListener]s that have been registered with
  567   /// [addListener].
  568   ///
  569   /// The [InformationCollector], if provided, is invoked if the given [Future]
  570   /// resolves with an error, and can be used to supplement the reported error
  571   /// message (for example, giving the image's URL).
  572   ///
  573   /// Errors are reported using [FlutterError.reportError] with the `silent`
  574   /// argument on [FlutterErrorDetails] set to true, meaning that by default the
  575   /// message is only dumped to the console in debug mode (see [new
  576   /// FlutterErrorDetails]).
  577   OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector })
  578       : assert(image != null) {
  579     image.then<void>(setImage, onError: (dynamic error, StackTrace stack) {
  580       reportError(
  581         context: ErrorDescription('resolving a single-frame image stream'),
  582         exception: error,
  583         stack: stack,
  584         informationCollector: informationCollector,
  585         silent: true,
  586       );
  587     });
  588   }
  589 }
  590 
  591 /// Manages the decoding and scheduling of image frames.
  592 ///
  593 /// New frames will only be emitted while there are registered listeners to the
  594 /// stream (registered with [addListener]).
  595 ///
  596 /// This class deals with 2 types of frames:
  597 ///
  598 ///  * image frames - image frames of an animated image.
  599 ///  * app frames - frames that the flutter engine is drawing to the screen to
  600 ///    show the app GUI.
  601 ///
  602 /// For single frame images the stream will only complete once.
  603 ///
  604 /// For animated images, this class eagerly decodes the next image frame,
  605 /// and notifies the listeners that a new frame is ready on the first app frame
  606 /// that is scheduled after the image frame duration has passed.
  607 ///
  608 /// Scheduling new timers only from scheduled app frames, makes sure we pause
  609 /// the animation when the app is not visible (as new app frames will not be
  610 /// scheduled).
  611 ///
  612 /// See the following timeline example:
  613 ///
  614 ///     | Time | Event                                      | Comment                   |
  615 ///     |------|--------------------------------------------|---------------------------|
  616 ///     | t1   | App frame scheduled (image frame A posted) |                           |
  617 ///     | t2   | App frame scheduled                        |                           |
  618 ///     | t3   | App frame scheduled                        |                           |
  619 ///     | t4   | Image frame B decoded                      |                           |
  620 ///     | t5   | App frame scheduled                        | t5 - t1 < frameB_duration |
  621 ///     | t6   | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
  622 ///
  623 class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  624   /// Creates a image stream completer.
  625   ///
  626   /// Immediately starts decoding the first image frame when the codec is ready.
  627   ///
  628   /// The `codec` parameter is a future for an initialized [ui.Codec] that will
  629   /// be used to decode the image.
  630   ///
  631   /// The `scale` parameter is the linear scale factor for drawing this frames
  632   /// of this image at their intended size.
  633   ///
  634   /// The `tag` parameter is passed on to created [ImageInfo] objects to
  635   /// help identify the source of the image.
  636   ///
  637   /// The `chunkEvents` parameter is an optional stream of notifications about
  638   /// the loading progress of the image. If this stream is provided, the events
  639   /// produced by the stream will be delivered to registered [ImageChunkListener]s
  640   /// (see [addListener]).
  641   MultiFrameImageStreamCompleter({
  642     required Future<ui.Codec> codec,
  643     required double scale,
  644     String? debugLabel,
  645     Stream<ImageChunkEvent>? chunkEvents,
  646     InformationCollector? informationCollector,
  647   }) : assert(codec != null),
  648        _informationCollector = informationCollector,
  649        _scale = scale {
  650     this.debugLabel = debugLabel;
  651     codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
  652       reportError(
  653         context: ErrorDescription('resolving an image codec'),
  654         exception: error,
  655         stack: stack,
  656         informationCollector: informationCollector,
  657         silent: true,
  658       );
  659     });
  660     if (chunkEvents != null) {
  661       chunkEvents.listen(reportImageChunkEvent,
  662         onError: (dynamic error, StackTrace stack) {
  663           reportError(
  664             context: ErrorDescription('loading an image'),
  665             exception: error,
  666             stack: stack,
  667             informationCollector: informationCollector,
  668             silent: true,
  669           );
  670         },
  671       );
  672     }
  673   }
  674 
  675   ui.Codec? _codec;
  676   final double _scale;
  677   final InformationCollector? _informationCollector;
  678   ui.FrameInfo? _nextFrame;
  679   // When the current was first shown.
  680   late Duration _shownTimestamp;
  681   // The requested duration for the current frame;
  682   Duration? _frameDuration;
  683   // How many frames have been emitted so far.
  684   int _framesEmitted = 0;
  685   Timer? _timer;
  686 
  687   // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  688   bool _frameCallbackScheduled = false;
  689 
  690   void _handleCodecReady(ui.Codec codec) {
  691     _codec = codec;
  692     assert(_codec != null);
  693 
  694     if (hasListeners) {
  695       _decodeNextFrameAndSchedule();
  696     }
  697   }
  698 
  699   void _handleAppFrame(Duration timestamp) {
  700     _frameCallbackScheduled = false;
  701     if (!hasListeners)
  702       return;
  703     if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
  704       _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
  705       _shownTimestamp = timestamp;
  706       _frameDuration = _nextFrame!.duration;
  707       _nextFrame = null;
  708       final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
  709       if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
  710         _decodeNextFrameAndSchedule();
  711       }
  712       return;
  713     }
  714     final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
  715     _timer = Timer(delay * timeDilation, () {
  716       _scheduleAppFrame();
  717     });
  718   }
  719 
  720   bool _isFirstFrame() {
  721     return _frameDuration == null;
  722   }
  723 
  724   bool _hasFrameDurationPassed(Duration timestamp) {
  725     return timestamp - _shownTimestamp >= _frameDuration!;
  726   }
  727 
  728   Future<void> _decodeNextFrameAndSchedule() async {
  729     try {
  730       _nextFrame = await _codec!.getNextFrame();
  731     } catch (exception, stack) {
  732       reportError(
  733         context: ErrorDescription('resolving an image frame'),
  734         exception: exception,
  735         stack: stack,
  736         informationCollector: _informationCollector,
  737         silent: true,
  738       );
  739       return;
  740     }
  741     if (_codec!.frameCount == 1) {
  742       // This is not an animated image, just return it and don't schedule more
  743       // frames.
  744       _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
  745       return;
  746     }
  747     _scheduleAppFrame();
  748   }
  749 
  750   void _scheduleAppFrame() {
  751     if (_frameCallbackScheduled) {
  752       return;
  753     }
  754     _frameCallbackScheduled = true;
  755     SchedulerBinding.instance!.scheduleFrameCallback(_handleAppFrame);
  756   }
  757 
  758   void _emitFrame(ImageInfo imageInfo) {
  759     setImage(imageInfo);
  760     _framesEmitted += 1;
  761   }
  762 
  763   @override
  764   void addListener(ImageStreamListener listener) {
  765     if (!hasListeners && _codec != null)
  766       _decodeNextFrameAndSchedule();
  767     super.addListener(listener);
  768   }
  769 
  770   @override
  771   void removeListener(ImageStreamListener listener) {
  772     super.removeListener(listener);
  773     if (!hasListeners) {
  774       _timer?.cancel();
  775       _timer = null;
  776     }
  777   }
  778 }