fade_in_image.dart 20 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 12
// 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 'package:flutter/services.dart';

import 'basic.dart';
import 'framework.dart';
import 'image.dart';
13 14
import 'implicit_animations.dart';
import 'transitions.dart';
15

16
// Examples can assume:
17
// late Uint8List bytes;
18

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

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

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

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

248 249 250 251 252 253
  /// 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.
254
  final ImageErrorWidgetBuilder? placeholderErrorBuilder;
255

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

259 260 261 262 263
  /// 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.
264
  final ImageErrorWidgetBuilder? imageErrorBuilder;
265

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  /// 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.
284
  final double? width;
285 286 287 288 289 290 291

  /// 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.
292
  final double? height;
293 294 295 296 297

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

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

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

Ian Hickson's avatar
Ian Hickson committed
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
  /// 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;

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

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

359
  Image _image({
360 361 362
    required ImageProvider image,
    ImageErrorWidgetBuilder? errorBuilder,
    ImageFrameBuilder? frameBuilder,
363 364 365 366
  }) {
    assert(image != null);
    return Image(
      image: image,
367
      errorBuilder: errorBuilder,
368 369 370 371 372 373 374 375 376 377 378
      frameBuilder: frameBuilder,
      width: width,
      height: height,
      fit: fit,
      alignment: alignment,
      repeat: repeat,
      matchTextDirection: matchTextDirection,
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }
379

380 381 382 383
  @override
  Widget build(BuildContext context) {
    Widget result = _image(
      image: image,
384
      errorBuilder: imageErrorBuilder,
385
      frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
386 387 388 389
        if (wasSynchronouslyLoaded)
          return child;
        return _AnimatedFadeOutFadeIn(
          target: child,
390
          placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
391 392 393 394 395 396 397 398
          isTargetLoaded: frame != null,
          fadeInDuration: fadeInDuration,
          fadeOutDuration: fadeOutDuration,
          fadeInCurve: fadeInCurve,
          fadeOutCurve: fadeOutCurve,
        );
      },
    );
399

400 401 402 403 404 405 406
    if (!excludeFromSemantics) {
      result = Semantics(
        container: imageSemanticLabel != null,
        image: true,
        label: imageSemanticLabel ?? '',
        child: result,
      );
407 408
    }

409
    return result;
410 411 412
  }
}

413 414
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
415 416 417 418 419 420 421 422
    Key? key,
    required this.target,
    required this.placeholder,
    required this.isTargetLoaded,
    required this.fadeOutDuration,
    required this.fadeOutCurve,
    required this.fadeInDuration,
    required this.fadeInCurve,
423 424 425 426 427 428 429 430
  }) : 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);
431

432 433 434 435 436 437 438
  final Widget target;
  final Widget placeholder;
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;
439 440

  @override
441 442
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
443

444
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
445 446 447 448
  Tween<double>? _targetOpacity;
  Tween<double>? _placeholderOpacity;
  Animation<double>? _targetOpacityAnimation;
  Animation<double>? _placeholderOpacityAnimation;
449 450

  @override
451 452 453 454
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
455
      (dynamic value) => Tween<double>(begin: value as double),
456
    ) as Tween<double>?;
457 458 459
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
460
      (dynamic value) => Tween<double>(begin: value as double),
461
    ) as Tween<double>?;
462 463 464
  }

  @override
465
  void didUpdateTweens() {
466
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
467
      TweenSequenceItem<double>(
468
        tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
469 470 471 472 473 474
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
475
    ]))..addStatusListener((AnimationStatus status) {
476
      if (_placeholderOpacityAnimation!.isCompleted) {
477
        // Need to rebuild to remove placeholder now that it is invisible.
478 479 480 481
        setState(() {});
      }
    });

482
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
483 484 485 486 487
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
488
        tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
489 490 491
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
492
    if (!widget.isTargetLoaded && _isValid(_placeholderOpacity!) && _isValid(_targetOpacity!)) {
493 494
      // 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.
495
      controller.value = controller.upperBound;
496 497 498
    }
  }

499 500
  bool _isValid(Tween<double> tween) {
    return tween.begin != null && tween.end != null;
501 502
  }

503 504
  @override
  Widget build(BuildContext context) {
505
    final Widget target = FadeTransition(
506
      opacity: _targetOpacityAnimation!,
507 508 509
      child: widget.target,
    );

510
    if (_placeholderOpacityAnimation!.isCompleted) {
511 512 513
      return target;
    }

514 515 516 517 518 519 520
    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>[
521
        target,
522
        FadeTransition(
523
          opacity: _placeholderOpacityAnimation!,
524 525 526
          child: widget.placeholder,
        ),
      ],
527
    );
528 529 530
  }

  @override
531 532 533 534
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
535 536
  }
}