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

import 'dart:typed_data';

import 'package:flutter/foundation.dart';

import 'basic.dart';
import 'framework.dart';
import 'image.dart';
12
import 'implicit_animations.dart';
13

14
// Examples can assume:
15
// late Uint8List bytes;
16

17 18 19 20 21
/// An image that shows a [placeholder] image while the target [image] is
/// loading, then fades in the new image when it loads.
///
/// Use this class to display long-loading images, such as [new NetworkImage],
/// so that the image appears on screen with a graceful animation rather than
22
/// abruptly popping onto the screen.
23
///
24 25
/// {@youtube 560 315 https://www.youtube.com/watch?v=pK738Pg9cxc}
///
26
/// If the [image] emits an [ImageInfo] synchronously, such as when the image
27
/// has been loaded and cached, the [image] is displayed immediately, and the
28 29
/// [placeholder] is never displayed.
///
30 31
/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
/// animation of the [placeholder].
32
///
33 34
/// The [fadeInDuration] and [fadeInCurve] properties control the fade-in
/// animation of the target [image].
35
///
36 37
/// Prefer a [placeholder] that's already cached so that it is displayed
/// immediately. This prevents it from popping onto the screen.
38
///
39 40
/// When [image] changes, it is resolved to a new [ImageStream]. If the new
/// [ImageStream.key] is different, this widget subscribes to the new stream and
41 42 43 44
/// replaces the displayed image with images emitted by the new stream.
///
/// When [placeholder] changes and the [image] has not yet emitted an
/// [ImageInfo], then [placeholder] is resolved to a new [ImageStream]. If the
45
/// new [ImageStream.key] is different, this widget subscribes to the new stream
46 47 48 49 50 51 52
/// and replaces the displayed image to images emitted by the new stream.
///
/// When either [placeholder] or [image] changes, this widget continues showing
/// the previously loaded image (if any) until the new image provider provides a
/// different image. This is known as "gapless playback" (see also
/// [Image.gaplessPlayback]).
///
53
/// {@tool snippet}
54 55
///
/// ```dart
56
/// FadeInImage(
57
///   // here `bytes` is a Uint8List containing the bytes for the in-memory image
58
///   placeholder: MemoryImage(bytes),
59
///   image: const NetworkImage('https://backend.example.com/image.png'),
60
/// )
61
/// ```
62
/// {@end-tool}
63
class FadeInImage extends StatefulWidget {
64 65
  /// Creates a widget that displays a [placeholder] while an [image] is loading,
  /// then fades-out the placeholder and fades-in the image.
66
  ///
67 68 69
  /// The [placeholder] and [image] may be composed in a [ResizeImage] to provide
  /// a custom decode/cache size.
  ///
70
  /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
Ian Hickson's avatar
Ian Hickson committed
71 72
  /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
  /// [matchTextDirection] arguments must not be null.
73
  ///
74
  /// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
75
  const FadeInImage({
76 77
    Key? key,
    required this.placeholder,
78
    this.placeholderErrorBuilder,
79
    required this.image,
80
    this.imageErrorBuilder,
81 82
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
83 84 85 86
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
87 88 89
    this.width,
    this.height,
    this.fit,
90 91 92
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
93 94 95 96 97 98
  }) : assert(placeholder != null),
       assert(image != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
99
       assert(alignment != null),
100
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
101
       assert(matchTextDirection != null),
102 103 104 105 106
       super(key: key);

  /// Creates a widget that uses a placeholder image stored in memory while
  /// loading the final image from the network.
  ///
107
  /// The `placeholder` argument contains the bytes of the in-memory image.
108
  ///
109
  /// The `image` argument is the URL of the final image.
110
  ///
111 112
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
113
  ///
114 115 116 117 118 119 120
  /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
  /// or [imageCacheHeight] are provided, it indicates to the
  /// engine that the respective image should be decoded at the specified size.
  /// The image will be rendered to the constraints of the layout or [width]
  /// and [height] regardless of these parameters. These parameters are primarily
  /// intended to reduce the memory usage of [ImageCache].
  ///
121
  /// The [placeholder], [image], [placeholderScale], [imageScale],
Ian Hickson's avatar
Ian Hickson committed
122 123 124
  /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
  /// [alignment], [repeat], and [matchTextDirection] arguments must not be
  /// null.
125 126 127 128 129 130 131 132
  ///
  /// See also:
  ///
  ///  * [new Image.memory], which has more details about loading images from
  ///    memory.
  ///  * [new Image.network], which has more details about loading images from
  ///    the network.
  FadeInImage.memoryNetwork({
133 134
    Key? key,
    required Uint8List placeholder,
135
    this.placeholderErrorBuilder,
136
    required String image,
137
    this.imageErrorBuilder,
138 139
    double placeholderScale = 1.0,
    double imageScale = 1.0,
140 141
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
142 143 144 145
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
146 147 148
    this.width,
    this.height,
    this.fit,
149 150 151
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
152 153 154 155
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
156 157 158 159 160 161 162 163
  }) : assert(placeholder != null),
       assert(image != null),
       assert(placeholderScale != null),
       assert(imageScale != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
164
       assert(alignment != null),
165
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
166
       assert(matchTextDirection != null),
167 168
       placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
169 170 171 172 173
       super(key: key);

  /// Creates a widget that uses a placeholder image stored in an asset bundle
  /// while loading the final image from the network.
  ///
174
  /// The `placeholder` argument is the key of the image in the asset bundle.
175
  ///
176
  /// The `image` argument is the URL of the final image.
177
  ///
178 179
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
180
  ///
181
  /// If `placeholderScale` is omitted or is null, pixel-density-aware asset
182 183 184
  /// resolution will be attempted for the [placeholder] image. Otherwise, the
  /// exact asset specified will be used.
  ///
185 186 187 188 189 190 191
  /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
  /// or [imageCacheHeight] are provided, it indicates to the
  /// engine that the respective image should be decoded at the specified size.
  /// The image will be rendered to the constraints of the layout or [width]
  /// and [height] regardless of these parameters. These parameters are primarily
  /// intended to reduce the memory usage of [ImageCache].
  ///
192
  /// The [placeholder], [image], [imageScale], [fadeOutDuration],
Ian Hickson's avatar
Ian Hickson committed
193 194
  /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
  /// and [matchTextDirection] arguments must not be null.
195 196 197 198 199 200 201 202
  ///
  /// See also:
  ///
  ///  * [new Image.asset], which has more details about loading images from
  ///    asset bundles.
  ///  * [new Image.network], which has more details about loading images from
  ///    the network.
  FadeInImage.assetNetwork({
203 204
    Key? key,
    required String placeholder,
205
    this.placeholderErrorBuilder,
206
    required String image,
207
    this.imageErrorBuilder,
208 209
    AssetBundle? bundle,
    double? placeholderScale,
210
    double imageScale = 1.0,
211 212
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
213 214 215 216
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
217 218 219
    this.width,
    this.height,
    this.fit,
220 221 222
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
223 224 225 226
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
227 228 229
  }) : assert(placeholder != null),
       assert(image != null),
       placeholder = placeholderScale != null
230 231
         ? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
         : ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
232 233 234 235 236
       assert(imageScale != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
237
       assert(alignment != null),
238
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
239
       assert(matchTextDirection != null),
240
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
241 242 243 244 245
       super(key: key);

  /// Image displayed while the target [image] is loading.
  final ImageProvider placeholder;

246 247 248 249 250 251
  /// A builder function that is called if an error occurs during placeholder
  /// image loading.
  ///
  /// If this builder is not provided, any exceptions will be reported to
  /// [FlutterError.onError]. If it is provided, the caller should either handle
  /// the exception by providing a replacement widget, or rethrow the exception.
252
  final ImageErrorWidgetBuilder? placeholderErrorBuilder;
253

254
  /// The target image that is displayed once it has loaded.
255 256
  final ImageProvider image;

257 258 259 260 261
  /// A builder function that is called if an error occurs during image loading.
  ///
  /// If this builder is not provided, any exceptions will be reported to
  /// [FlutterError.onError]. If it is provided, the caller should either handle
  /// the exception by providing a replacement widget, or rethrow the exception.
262
  final ImageErrorWidgetBuilder? imageErrorBuilder;
263

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  /// The duration of the fade-out animation for the [placeholder].
  final Duration fadeOutDuration;

  /// The curve of the fade-out animation for the [placeholder].
  final Curve fadeOutCurve;

  /// The duration of the fade-in animation for the [image].
  final Duration fadeInDuration;

  /// The curve of the fade-in animation for the [image].
  final Curve fadeInCurve;

  /// If non-null, require the image to have this width.
  ///
  /// If null, the image will pick a size that best preserves its intrinsic
  /// aspect ratio. This may result in a sudden change if the size of the
  /// placeholder image does not match that of the target image. The size is
  /// also affected by the scale factor.
282
  final double? width;
283 284 285 286 287 288 289

  /// If non-null, require the image to have this height.
  ///
  /// If null, the image will pick a size that best preserves its intrinsic
  /// aspect ratio. This may result in a sudden change if the size of the
  /// placeholder image does not match that of the target image. The size is
  /// also affected by the scale factor.
290
  final double? height;
291 292 293 294 295

  /// How to inscribe the image into the space allocated during layout.
  ///
  /// The default varies based on the other fields. See the discussion at
  /// [paintImage].
296
  final BoxFit? fit;
297 298 299

  /// How to align the image within its bounds.
  ///
Ian Hickson's avatar
Ian Hickson committed
300
  /// The alignment aligns the given position in the image to the given position
301 302
  /// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
  /// -1.0) aligns the image to the top-left corner of its layout bounds, while an
303
  /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
Ian Hickson's avatar
Ian Hickson committed
304
  /// image with the bottom right corner of its layout bounds. Similarly, an
305
  /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
Ian Hickson's avatar
Ian Hickson committed
306 307 308
  /// middle of the bottom edge of its layout bounds.
  ///
  /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
309
  /// [AlignmentDirectional]), then an ambient [Directionality] widget
Ian Hickson's avatar
Ian Hickson committed
310 311
  /// must be in scope.
  ///
312
  /// Defaults to [Alignment.center].
313 314 315 316 317 318 319
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
320
  final AlignmentGeometry alignment;
321 322 323 324

  /// How to paint any portions of the layout bounds not covered by the image.
  final ImageRepeat repeat;

Ian Hickson's avatar
Ian Hickson committed
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
  /// Whether to paint the image in the direction of the [TextDirection].
  ///
  /// If this is true, then in [TextDirection.ltr] contexts, the image will be
  /// drawn with its origin in the top left (the "normal" painting direction for
  /// images); and in [TextDirection.rtl] contexts, the image will be drawn with
  /// a scaling factor of -1 in the horizontal direction so that the origin is
  /// in the top right.
  ///
  /// This is occasionally used with images in right-to-left environments, for
  /// images that were designed for left-to-right locales. Be careful, when
  /// using this, to not flip images with integral shadows, text, or other
  /// effects that will look incorrect when flipped.
  ///
  /// If this is true, there must be an ambient [Directionality] widget in
  /// scope.
  final bool matchTextDirection;

342 343
  /// Whether to exclude this image from semantics.
  ///
344 345
  /// This is useful for images which do not contribute meaningful information
  /// to an application.
346 347
  final bool excludeFromSemantics;

348
  /// A semantic description of the [image].
349 350 351
  ///
  /// Used to provide a description of the [image] to TalkBack on Android, and
  /// VoiceOver on iOS.
352
  ///
353 354
  /// This description will be used both while the [placeholder] is shown and
  /// once the image has loaded.
355
  final String? imageSemanticLabel;
356

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  @override
  State<FadeInImage> createState() => _FadeInImageState();
}

class _FadeInImageState extends State<FadeInImage> {
  static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);

  // These ProxyAnimations are changed to the fade in animation by
  // [_AnimatedFadeOutFadeInState]. Otherwise these animations are reset to
  // their defaults by [_resetAnimations].
  final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation);
  final ProxyAnimation _placeholderAnimation = ProxyAnimation(_kOpaqueAnimation);

  void _resetAnimations() {
    _imageAnimation.parent = _kOpaqueAnimation;
    _placeholderAnimation.parent = _kOpaqueAnimation;
  }

375
  Image _image({
376 377 378
    required ImageProvider image,
    ImageErrorWidgetBuilder? errorBuilder,
    ImageFrameBuilder? frameBuilder,
379
    required Animation<double> opacity,
380 381 382 383
  }) {
    assert(image != null);
    return Image(
      image: image,
384
      errorBuilder: errorBuilder,
385
      frameBuilder: frameBuilder,
386 387 388 389 390 391 392
      opacity: opacity,
      width: widget.width,
      height: widget.height,
      fit: widget.fit,
      alignment: widget.alignment,
      repeat: widget.repeat,
      matchTextDirection: widget.matchTextDirection,
393 394 395 396
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }
397

398 399 400
  @override
  Widget build(BuildContext context) {
    Widget result = _image(
401 402 403
      image: widget.image,
      errorBuilder: widget.imageErrorBuilder,
      opacity: _imageAnimation,
404
      frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
405 406
        if (wasSynchronouslyLoaded) {
          _resetAnimations();
407
          return child;
408
        }
409 410
        return _AnimatedFadeOutFadeIn(
          target: child,
411 412 413 414 415 416 417
          targetProxyAnimation: _imageAnimation,
          placeholder: _image(
            image: widget.placeholder,
            errorBuilder: widget.placeholderErrorBuilder,
            opacity: _placeholderAnimation,
          ),
          placeholderProxyAnimation: _placeholderAnimation,
418
          isTargetLoaded: frame != null,
419 420 421 422
          fadeInDuration: widget.fadeInDuration,
          fadeOutDuration: widget.fadeOutDuration,
          fadeInCurve: widget.fadeInCurve,
          fadeOutCurve: widget.fadeOutCurve,
423 424 425
        );
      },
    );
426

427
    if (!widget.excludeFromSemantics) {
428
      result = Semantics(
429
        container: widget.imageSemanticLabel != null,
430
        image: true,
431
        label: widget.imageSemanticLabel ?? '',
432 433
        child: result,
      );
434 435
    }

436
    return result;
437 438 439
  }
}

440 441
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
442 443
    Key? key,
    required this.target,
444
    required this.targetProxyAnimation,
445
    required this.placeholder,
446
    required this.placeholderProxyAnimation,
447 448 449 450 451
    required this.isTargetLoaded,
    required this.fadeOutDuration,
    required this.fadeOutCurve,
    required this.fadeInDuration,
    required this.fadeInCurve,
452 453 454 455 456 457 458 459
  }) : assert(target != null),
       assert(placeholder != null),
       assert(isTargetLoaded != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
       super(key: key, duration: fadeInDuration + fadeOutDuration);
460

461
  final Widget target;
462
  final ProxyAnimation targetProxyAnimation;
463
  final Widget placeholder;
464
  final ProxyAnimation placeholderProxyAnimation;
465 466 467 468 469
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;
470 471

  @override
472 473
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
474

475
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
476 477 478 479
  Tween<double>? _targetOpacity;
  Tween<double>? _placeholderOpacity;
  Animation<double>? _targetOpacityAnimation;
  Animation<double>? _placeholderOpacityAnimation;
480 481

  @override
482 483 484 485
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
486
      (dynamic value) => Tween<double>(begin: value as double),
487
    ) as Tween<double>?;
488 489 490
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
491
      (dynamic value) => Tween<double>(begin: value as double),
492
    ) as Tween<double>?;
493 494 495
  }

  @override
496
  void didUpdateTweens() {
497
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
498
      TweenSequenceItem<double>(
499
        tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
500 501 502 503 504 505
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
506
    ]))..addStatusListener((AnimationStatus status) {
507
      if (_placeholderOpacityAnimation!.isCompleted) {
508
        // Need to rebuild to remove placeholder now that it is invisible.
509 510 511 512
        setState(() {});
      }
    });

513
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
514 515 516 517 518
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
519
        tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
520 521 522
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
523
    if (!widget.isTargetLoaded && _isValid(_placeholderOpacity!) && _isValid(_targetOpacity!)) {
524 525
      // Jump (don't fade) back to the placeholder image, so as to be ready
      // for the full animation when the new target image becomes ready.
526
      controller.value = controller.upperBound;
527
    }
528 529 530

    widget.targetProxyAnimation.parent = _targetOpacityAnimation;
    widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
531 532
  }

533 534
  bool _isValid(Tween<double> tween) {
    return tween.begin != null && tween.end != null;
535 536
  }

537 538
  @override
  Widget build(BuildContext context) {
539
    if (_placeholderOpacityAnimation!.isCompleted) {
540
      return widget.target;
541 542
    }

543 544 545 546 547 548 549
    return Stack(
      fit: StackFit.passthrough,
      alignment: AlignmentDirectional.center,
      // Text direction is irrelevant here since we're using center alignment,
      // but it allows the Stack to avoid a call to Directionality.of()
      textDirection: TextDirection.ltr,
      children: <Widget>[
550 551
        widget.target,
        widget.placeholder,
552
      ],
553
    );
554 555 556
  }

  @override
557 558 559 560
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
561 562
  }
}