fade_in_image.dart 22.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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import 'basic.dart';
import 'framework.dart';
import 'image.dart';
10
import 'implicit_animations.dart';
11

12
// Examples can assume:
13
// late Uint8List bytes;
14

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

  /// Creates a widget that uses a placeholder image stored in memory while
  /// loading the final image from the network.
  ///
113
  /// The `placeholder` argument contains the bytes of the in-memory image.
114
  ///
115
  /// The `image` argument is the URL of the final image.
116
  ///
117 118
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
119
  ///
120 121 122 123 124 125 126
  /// 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].
  ///
127
  /// The [placeholder], [image], [placeholderScale], [imageScale],
Ian Hickson's avatar
Ian Hickson committed
128 129 130
  /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
  /// [alignment], [repeat], and [matchTextDirection] arguments must not be
  /// null.
131 132 133
  ///
  /// See also:
  ///
134
  ///  * [Image.memory], which has more details about loading images from
135
  ///    memory.
136
  ///  * [Image.network], which has more details about loading images from
137 138
  ///    the network.
  FadeInImage.memoryNetwork({
139
    super.key,
140
    required Uint8List placeholder,
141
    this.placeholderErrorBuilder,
142
    required String image,
143
    this.imageErrorBuilder,
144 145
    double placeholderScale = 1.0,
    double imageScale = 1.0,
146 147
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
148 149 150 151
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
152 153 154
    this.width,
    this.height,
    this.fit,
155
    this.placeholderFit,
156 157
    this.filterQuality = FilterQuality.low,
    this.placeholderFilterQuality,
158 159 160
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
161 162 163 164
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
165 166 167 168 169 170 171 172
  }) : 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
173
       assert(alignment != null),
174
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
175
       assert(matchTextDirection != null),
176
       placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
177
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
178 179 180 181

  /// Creates a widget that uses a placeholder image stored in an asset bundle
  /// while loading the final image from the network.
  ///
182
  /// The `placeholder` argument is the key of the image in the asset bundle.
183
  ///
184
  /// The `image` argument is the URL of the final image.
185
  ///
186 187
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
188
  ///
189
  /// If `placeholderScale` is omitted or is null, pixel-density-aware asset
190 191 192
  /// resolution will be attempted for the [placeholder] image. Otherwise, the
  /// exact asset specified will be used.
  ///
193 194 195 196 197 198 199
  /// 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].
  ///
200
  /// The [placeholder], [image], [imageScale], [fadeOutDuration],
Ian Hickson's avatar
Ian Hickson committed
201 202
  /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
  /// and [matchTextDirection] arguments must not be null.
203 204 205
  ///
  /// See also:
  ///
206
  ///  * [Image.asset], which has more details about loading images from
207
  ///    asset bundles.
208
  ///  * [Image.network], which has more details about loading images from
209 210
  ///    the network.
  FadeInImage.assetNetwork({
211
    super.key,
212
    required String placeholder,
213
    this.placeholderErrorBuilder,
214
    required String image,
215
    this.imageErrorBuilder,
216 217
    AssetBundle? bundle,
    double? placeholderScale,
218
    double imageScale = 1.0,
219 220
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
221 222 223 224
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
225 226 227
    this.width,
    this.height,
    this.fit,
228
    this.placeholderFit,
229 230
    this.filterQuality = FilterQuality.low,
    this.placeholderFilterQuality,
231 232 233
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
234 235 236 237
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
238 239 240
  }) : assert(placeholder != null),
       assert(image != null),
       placeholder = placeholderScale != null
241 242
         ? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
         : ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
243 244 245 246 247
       assert(imageScale != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
248
       assert(alignment != null),
249
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
250
       assert(matchTextDirection != null),
251
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
252 253 254 255

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

256 257 258 259 260 261
  /// 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.
262
  final ImageErrorWidgetBuilder? placeholderErrorBuilder;
263

264
  /// The target image that is displayed once it has loaded.
265 266
  final ImageProvider image;

267 268 269 270 271
  /// 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.
272
  final ImageErrorWidgetBuilder? imageErrorBuilder;
273

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
  /// 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.
292
  final double? width;
293 294 295 296 297 298 299

  /// 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.
300
  final double? height;
301 302 303 304 305

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

308 309 310 311 312
  /// How to inscribe the placeholder image into the space allocated during layout.
  ///
  /// If not value set, it will fallback to [fit].
  final BoxFit? placeholderFit;

313 314 315 316 317 318 319 320 321 322
  /// The rendering quality of the image.
  ///
  /// {@macro flutter.widgets.image.filterQuality}
  final FilterQuality filterQuality;

  /// The rendering quality of the placeholder image.
  ///
  /// {@macro flutter.widgets.image.filterQuality}
  final FilterQuality? placeholderFilterQuality;

323 324
  /// How to align the image within its bounds.
  ///
Ian Hickson's avatar
Ian Hickson committed
325
  /// The alignment aligns the given position in the image to the given position
326 327
  /// 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
328
  /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
Ian Hickson's avatar
Ian Hickson committed
329
  /// image with the bottom right corner of its layout bounds. Similarly, an
330
  /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
Ian Hickson's avatar
Ian Hickson committed
331 332 333
  /// middle of the bottom edge of its layout bounds.
  ///
  /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
334
  /// [AlignmentDirectional]), then an ambient [Directionality] widget
Ian Hickson's avatar
Ian Hickson committed
335 336
  /// must be in scope.
  ///
337
  /// Defaults to [Alignment.center].
338 339 340 341 342 343 344
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
345
  final AlignmentGeometry alignment;
346 347 348 349

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

Ian Hickson's avatar
Ian Hickson committed
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
  /// 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;

367 368
  /// Whether to exclude this image from semantics.
  ///
369 370
  /// This is useful for images which do not contribute meaningful information
  /// to an application.
371 372
  final bool excludeFromSemantics;

373
  /// A semantic description of the [image].
374 375 376
  ///
  /// Used to provide a description of the [image] to TalkBack on Android, and
  /// VoiceOver on iOS.
377
  ///
378 379
  /// This description will be used both while the [placeholder] is shown and
  /// once the image has loaded.
380
  final String? imageSemanticLabel;
381

382 383 384 385 386 387
  @override
  State<FadeInImage> createState() => _FadeInImageState();
}

class _FadeInImageState extends State<FadeInImage> {
  static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);
388
  bool targetLoaded = false;
389 390 391 392 393 394 395

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

396
  Image _image({
397 398 399
    required ImageProvider image,
    ImageErrorWidgetBuilder? errorBuilder,
    ImageFrameBuilder? frameBuilder,
400
    BoxFit? fit,
401
    required FilterQuality filterQuality,
402
    required Animation<double> opacity,
403 404 405 406
  }) {
    assert(image != null);
    return Image(
      image: image,
407
      errorBuilder: errorBuilder,
408
      frameBuilder: frameBuilder,
409 410 411
      opacity: opacity,
      width: widget.width,
      height: widget.height,
412
      fit: fit,
413
      filterQuality: filterQuality,
414 415 416
      alignment: widget.alignment,
      repeat: widget.repeat,
      matchTextDirection: widget.matchTextDirection,
417 418 419 420
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }
421

422 423 424
  @override
  Widget build(BuildContext context) {
    Widget result = _image(
425 426 427
      image: widget.image,
      errorBuilder: widget.imageErrorBuilder,
      opacity: _imageAnimation,
428
      fit: widget.fit,
429
      filterQuality: widget.filterQuality,
430
      frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
431 432
        if (wasSynchronouslyLoaded || frame != null) {
          targetLoaded = true;
433
        }
434 435
        return _AnimatedFadeOutFadeIn(
          target: child,
436 437 438 439 440
          targetProxyAnimation: _imageAnimation,
          placeholder: _image(
            image: widget.placeholder,
            errorBuilder: widget.placeholderErrorBuilder,
            opacity: _placeholderAnimation,
441
            fit: widget.placeholderFit ?? widget.fit,
442
            filterQuality: widget.placeholderFilterQuality ?? widget.filterQuality,
443 444
          ),
          placeholderProxyAnimation: _placeholderAnimation,
445 446
          isTargetLoaded: targetLoaded,
          wasSynchronouslyLoaded: wasSynchronouslyLoaded,
447 448 449 450
          fadeInDuration: widget.fadeInDuration,
          fadeOutDuration: widget.fadeOutDuration,
          fadeInCurve: widget.fadeInCurve,
          fadeOutCurve: widget.fadeOutCurve,
451 452 453
        );
      },
    );
454

455
    if (!widget.excludeFromSemantics) {
456
      result = Semantics(
457
        container: widget.imageSemanticLabel != null,
458
        image: true,
459
        label: widget.imageSemanticLabel ?? '',
460 461
        child: result,
      );
462 463
    }

464
    return result;
465 466 467
  }
}

468 469
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
470
    required this.target,
471
    required this.targetProxyAnimation,
472
    required this.placeholder,
473
    required this.placeholderProxyAnimation,
474 475 476 477 478
    required this.isTargetLoaded,
    required this.fadeOutDuration,
    required this.fadeOutCurve,
    required this.fadeInDuration,
    required this.fadeInCurve,
479
    required this.wasSynchronouslyLoaded,
480 481 482 483 484 485 486
  }) : assert(target != null),
       assert(placeholder != null),
       assert(isTargetLoaded != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
487
       assert(!wasSynchronouslyLoaded || isTargetLoaded),
488
       super(duration: fadeInDuration + fadeOutDuration);
489

490
  final Widget target;
491
  final ProxyAnimation targetProxyAnimation;
492
  final Widget placeholder;
493
  final ProxyAnimation placeholderProxyAnimation;
494 495 496 497 498
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;
499
  final bool wasSynchronouslyLoaded;
500 501

  @override
502 503
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
504

505
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
506 507 508 509
  Tween<double>? _targetOpacity;
  Tween<double>? _placeholderOpacity;
  Animation<double>? _targetOpacityAnimation;
  Animation<double>? _placeholderOpacityAnimation;
510 511

  @override
512 513 514 515
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
516
      (dynamic value) => Tween<double>(begin: value as double),
517
    ) as Tween<double>?;
518 519 520
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
521
      (dynamic value) => Tween<double>(begin: value as double),
522
    ) as Tween<double>?;
523 524 525
  }

  @override
526
  void didUpdateTweens() {
527 528 529 530 531
    if (widget.wasSynchronouslyLoaded) {
      // Opacity animations should not be reset if image was synchronously loaded.
      return;
    }

532
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
533
      TweenSequenceItem<double>(
534
        tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
535 536 537 538 539 540
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
541
    ]))..addStatusListener((AnimationStatus status) {
542
      if (_placeholderOpacityAnimation!.isCompleted) {
543
        // Need to rebuild to remove placeholder now that it is invisible.
544 545 546 547
        setState(() {});
      }
    });

548
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
549 550 551 552 553
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
554
        tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
555 556 557
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
558 559 560

    widget.targetProxyAnimation.parent = _targetOpacityAnimation;
    widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
561 562 563 564
  }

  @override
  Widget build(BuildContext context) {
565 566
    if (widget.wasSynchronouslyLoaded ||
        (_placeholderOpacityAnimation?.isCompleted ?? true)) {
567
      return widget.target;
568 569
    }

570 571 572 573 574 575 576
    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>[
577 578
        widget.target,
        widget.placeholder,
579
      ],
580
    );
581 582 583
  }

  @override
584 585 586 587
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
588 589
  }
}