image_stream.dart 19.2 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

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

12
/// A [dart:ui.Image] object with its corresponding scale.
13 14 15
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
16
@immutable
17 18 19 20
class ImageInfo {
  /// Creates an [ImageInfo] object for the given image and scale.
  ///
  /// Both the image and the scale must not be null.
21
  const ImageInfo({ @required this.image, this.scale = 1.0 })
22 23
      : assert(image != null),
        assert(scale != null);
24 25 26 27 28 29 30 31 32 33 34 35 36 37

  /// 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;

  /// The linear scale factor for drawing this image at its intended size.
  ///
  /// The scale factor applies to the width and the height.
  ///
  /// For example, if this is 2.0 it means that there are four image pixels for
  /// every one logical pixel, and the image's actual width and height (as given
38
  /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are double the
39 40 41 42 43 44
  /// height and width that should be used when painting the image (e.g. in the
  /// arguments given to [Canvas.drawImage]).
  final double scale;

  @override
  String toString() => '$image @ ${scale}x';
45 46 47 48 49 50 51 52 53 54 55 56

  @override
  int get hashCode => hashValues(image, scale);

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ImageInfo typedOther = other;
    return typedOther.image == image
        && typedOther.scale == scale;
  }
57 58 59 60 61
}

/// Signature for callbacks reporting that an image is available.
///
/// Used by [ImageStream].
62 63 64 65 66 67 68
///
/// The `synchronousCall` argument is true if the listener is being invoked
/// during the call to addListener. This can be useful if, for example,
/// [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]).
69
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
70

71 72 73
/// Signature for reporting errors when resolving images.
///
/// Used by [ImageStream] and [precacheImage] to report errors.
74
typedef ImageErrorListener = void Function(dynamic exception, StackTrace stackTrace);
75 76 77 78 79 80 81

class _ImageListenerPair {
  _ImageListenerPair(this.listener, this.errorListener);
  final ImageListener listener;
  final ImageErrorListener errorListener;
}

82 83
/// A handle to an image resource.
///
84
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
85 86 87 88 89 90 91 92
/// (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.
93 94 95 96 97
///
/// See also:
///
///  * [ImageProvider], which has an example that includes the use of an
///    [ImageStream] in a [Widget].
98
class ImageStream extends Diagnosticable {
99 100 101 102 103 104 105 106 107 108 109
  /// 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.
  ImageStreamCompleter get completer => _completer;
  ImageStreamCompleter _completer;

110
  List<_ImageListenerPair> _listeners;
111 112 113 114 115

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  ///
  /// This is usually done automatically by the [ImageProvider] that created the
  /// [ImageStream].
116 117 118 119
  ///
  /// 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.
120 121 122 123
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
124
      final List<_ImageListenerPair> initialListeners = _listeners;
125
      _listeners = null;
126 127 128 129 130 131
      for (_ImageListenerPair listenerPair in initialListeners) {
        _completer.addListener(
          listenerPair.listener,
          onError: listenerPair.errorListener,
        );
      }
132 133 134
    }
  }

135
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
136 137
  /// object is available. If a concrete image is already available, this object
  /// will call the listener synchronously.
138 139 140 141 142 143 144 145
  ///
  /// If the assigned [completer] completes multiple images over its lifetime,
  /// this listener will fire multiple times.
  ///
  /// 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.
146 147 148 149 150 151 152 153 154
  ///
  /// An [ImageErrorListener] can also optionally be added along with the
  /// `listener`. If an error occurred, `onError` will be called instead of
  /// `listener`.
  ///
  /// Many `listener`s can have the same `onError` and one `listener` can also
  /// have multiple `onError` by invoking [addListener] multiple times with
  /// a different `onError` each time.
  void addListener(ImageListener listener, { ImageErrorListener onError }) {
155
    if (_completer != null)
156 157
      return _completer.addListener(listener, onError: onError);
    _listeners ??= <_ImageListenerPair>[];
158
    _listeners.add(_ImageListenerPair(listener, onError));
159 160
  }

161 162
  /// Stop listening for new concrete [ImageInfo] objects and errors from
  /// the `listener`'s associated [ImageErrorListener].
163 164 165 166
  void removeListener(ImageListener listener) {
    if (_completer != null)
      return _completer.removeListener(listener);
    assert(_listeners != null);
167 168 169 170 171 172
    for (int i = 0; i < _listeners.length; ++i) {
      if (_listeners[i].listener == listener) {
        _listeners.removeAt(i);
        continue;
      }
    }
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
  /// This can be used to avoid unregistering and reregistering listeners after
  /// calling [ImageProvider.resolve] on a new, but possibly equivalent,
  /// [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.
  Object get key => _completer != null ? _completer : this;

188 189 190
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
191
    properties.add(ObjectFlagProperty<ImageStreamCompleter>(
192 193 194 195 196
      'completer',
      _completer,
      ifPresent: _completer?.toStringShort(),
      ifNull: 'unresolved',
    ));
197
    properties.add(ObjectFlagProperty<List<_ImageListenerPair>>(
198 199 200 201
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
      ifNull: 'no listeners',
202
      level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
203 204
    ));
    _completer?.debugFillProperties(properties);
205 206 207
  }
}

208
/// Base class for those that manage the loading of [dart:ui.Image] objects for
209 210
/// [ImageStream]s.
///
211 212 213 214
/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
/// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible.
abstract class ImageStreamCompleter extends Diagnosticable {
215 216 217
  final List<_ImageListenerPair> _listeners = <_ImageListenerPair>[];
  ImageInfo _currentImage;
  FlutterErrorDetails _currentError;
218

219
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
220 221 222
  /// 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
  /// will call the listener or error listener synchronously.
223
  ///
224
  /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
225 226
  /// this listener will fire multiple times.
  ///
227 228
  /// The listener will be passed a flag indicating whether a synchronous call
  /// occurred. If the listener is added within a render object paint function,
229 230
  /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
  /// a paint.
231
  void addListener(ImageListener listener, { ImageErrorListener onError }) {
232
    _listeners.add(_ImageListenerPair(listener, onError));
233
    if (_currentImage != null) {
234
      try {
235
        listener(_currentImage, true);
236
      } catch (exception, stack) {
237 238 239 240 241
        reportError(
          context: 'by a synchronously-called image listener',
          exception: exception,
          stack: stack,
        );
242 243
      }
    }
244 245
    if (_currentError != null && onError != null) {
      try {
246 247 248
        onError(_currentError.exception, _currentError.stack);
      } catch (exception, stack) {
        FlutterError.reportError(
249
          FlutterErrorDetails(
250 251 252 253 254 255 256
            exception: exception,
            library: 'image resource service',
            context: 'by a synchronously-called image error listener',
            stack: stack,
          ),
        );
      }
257
    }
258 259
  }

260 261
  /// Stop listening for new concrete [ImageInfo] objects and errors from
  /// its associated [ImageErrorListener].
262
  void removeListener(ImageListener listener) {
263 264 265 266 267 268
    for (int i = 0; i < _listeners.length; ++i) {
      if (_listeners[i].listener == listener) {
        _listeners.removeAt(i);
        continue;
      }
    }
269 270 271 272 273
  }

  /// Calls all the registered listeners to notify them of a new image.
  @protected
  void setImage(ImageInfo image) {
274
    _currentImage = image;
275 276
    if (_listeners.isEmpty)
      return;
277 278 279
    final List<ImageListener> localListeners = _listeners.map<ImageListener>(
      (_ImageListenerPair listenerPair) => listenerPair.listener
    ).toList();
280 281
    for (ImageListener listener in localListeners) {
      try {
282
        listener(image, false);
283
      } catch (exception, stack) {
284 285 286 287 288
        reportError(
          context: 'by an image listener',
          exception: exception,
          stack: stack,
        );
289 290 291 292
      }
    }
  }

293 294 295 296 297 298 299 300 301 302 303 304 305
  /// Calls all the registered error listeners to notify them of an error that
  /// occurred while resolving the image.
  ///
  /// If no error listeners are attached, a [FlutterError] will be reported
  /// instead.
  @protected
  void reportError({
    String context,
    dynamic exception,
    StackTrace stack,
    InformationCollector informationCollector,
    bool silent = false,
  }) {
306
    _currentError = FlutterErrorDetails(
307 308 309
      exception: exception,
      stack: stack,
      library: 'image resource service',
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
      context: context,
      informationCollector: informationCollector,
      silent: silent,
    );

    final List<ImageErrorListener> localErrorListeners =
        _listeners.map<ImageErrorListener>(
          (_ImageListenerPair listenerPair) => listenerPair.errorListener
        ).where(
          (ImageErrorListener errorListener) => errorListener != null
        ).toList();

    if (localErrorListeners.isEmpty) {
      FlutterError.reportError(_currentError);
    } else {
      for (ImageErrorListener errorListener in localErrorListeners) {
        try {
          errorListener(exception, stack);
        } catch (exception, stack) {
          FlutterError.reportError(
330
            FlutterErrorDetails(
331 332 333 334 335 336 337 338 339
              context: 'by an image error listener',
              library: 'image resource service',
              exception: exception,
              stack: stack,
            ),
          );
        }
      }
    }
340 341 342 343
  }

  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
344 345 346
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
347 348
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
    description.add(ObjectFlagProperty<List<_ImageListenerPair>>(
349 350 351 352
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
    ));
353 354 355
  }
}

356
/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
357 358 359 360 361 362 363
/// 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].
364 365 366 367 368 369 370 371 372
  ///
  /// The [InformationCollector], if provided, is invoked if the given [Future]
  /// resolves with an error, and can be used to supplement the reported error
  /// message (for example, giving the image's URL).
  ///
  /// Errors are reported using [FlutterError.reportError] with the `silent`
  /// argument on [FlutterErrorDetails] set to true, meaning that by default the
  /// message is only dumped to the console in debug mode (see [new
  /// FlutterErrorDetails]).
373 374
  OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector informationCollector })
    : assert(image != null) {
375
    image.then<void>(setImage, onError: (dynamic error, StackTrace stack) {
376 377
      reportError(
        context: 'resolving a single-frame image stream',
378 379
        exception: error,
        stack: stack,
380 381
        informationCollector: informationCollector,
        silent: true,
382
      );
383
    });
384 385
  }
}
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409

/// 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:
///
410 411 412 413 414 415 416 417
///     | 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 |
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
///
class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a image stream completer.
  ///
  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
  /// [codec] is a future for an initialized [ui.Codec] that will be used to
  /// decode the image.
  /// [scale] is the linear scale factor for drawing this frames of this image
  /// at their intended size.
  MultiFrameImageStreamCompleter({
    @required Future<ui.Codec> codec,
    @required double scale,
    InformationCollector informationCollector
  }) : assert(codec != null),
       _informationCollector = informationCollector,
       _scale = scale,
       _framesEmitted = 0,
       _timer = null {
437
    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
438 439
      reportError(
        context: 'resolving an image codec',
440 441 442 443
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
444
      );
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
    });
  }

  ui.Codec _codec;
  final double _scale;
  final InformationCollector _informationCollector;
  ui.FrameInfo _nextFrame;
  // When the current was first shown.
  Duration _shownTimestamp;
  // The requested duration for the current frame;
  Duration _frameDuration;
  // How many frames have been emitted so far.
  int _framesEmitted;
  Timer _timer;

460
  void _handleCodecReady(ui.Codec codec) {
461
    _codec = codec;
462 463
    assert(_codec != null);

464 465 466 467 468 469 470
    _decodeNextFrameAndSchedule();
  }

  void _handleAppFrame(Duration timestamp) {
    if (!_hasActiveListeners)
      return;
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
471
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
472 473 474 475 476 477 478 479 480 481
      _shownTimestamp = timestamp;
      _frameDuration = _nextFrame.duration;
      _nextFrame = null;
      final int completedCycles = _framesEmitted ~/ _codec.frameCount;
      if (_codec.repetitionCount == -1 || completedCycles <= _codec.repetitionCount) {
        _decodeNextFrameAndSchedule();
      }
      return;
    }
    final Duration delay = _frameDuration - (timestamp - _shownTimestamp);
482
    _timer = Timer(delay * timeDilation, () {
483 484 485 486 487 488 489 490 491 492 493 494 495
      SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
    });
  }

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

  bool _hasFrameDurationPassed(Duration timestamp) {
    assert(_shownTimestamp != null);
    return timestamp - _shownTimestamp >= _frameDuration;
  }

496
  Future<void> _decodeNextFrameAndSchedule() async {
497 498 499
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
500 501 502 503 504 505 506
      reportError(
        context: 'resolving an image frame',
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
507 508 509 510 511
      return;
    }
    if (_codec.frameCount == 1) {
      // This is not an animated image, just return it and don't schedule more
      // frames.
512
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
513 514 515 516 517 518 519 520 521 522 523 524 525
      return;
    }
    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
  }

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

  bool get _hasActiveListeners => _listeners.isNotEmpty;

  @override
526
  void addListener(ImageListener listener, { ImageErrorListener onError }) {
527 528 529
    if (!_hasActiveListeners && _codec != null) {
      _decodeNextFrameAndSchedule();
    }
530
    super.addListener(listener, onError: onError);
531 532 533 534 535
  }

  @override
  void removeListener(ImageListener listener) {
    super.removeListener(listener);
536
    if (!_hasActiveListeners) {
537 538 539 540 541
      _timer?.cancel();
      _timer = null;
    }
  }
}