fade_in_image.dart 18.6 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 17 18
// Examples can assume:
// Uint8List bytes;

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 61
///   placeholder: MemoryImage(bytes),
///   image: 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 78 79 80
  const FadeInImage({
    Key key,
    @required this.placeholder,
    @required this.image,
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 133 134 135
  ///
  /// 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({
    Key key,
    @required Uint8List placeholder,
    @required String image,
136 137
    double placeholderScale = 1.0,
    double imageScale = 1.0,
138 139
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
140 141 142 143
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
144 145 146
    this.width,
    this.height,
    this.fit,
147 148 149
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
150 151 152 153
    int placeholderCacheWidth,
    int placeholderCacheHeight,
    int imageCacheWidth,
    int imageCacheHeight,
154 155 156 157 158 159 160 161
  }) : 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
162
       assert(alignment != null),
163
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
164
       assert(matchTextDirection != null),
165 166
       placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
167 168 169 170 171
       super(key: key);

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

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

242
  /// The target image that is displayed once it has loaded.
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
  final ImageProvider image;

  /// 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.
  final double width;

  /// 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.
  final double height;

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

  /// How to align the image within its bounds.
  ///
Ian Hickson's avatar
Ian Hickson committed
281
  /// The alignment aligns the given position in the image to the given position
282 283
  /// 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
284
  /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
Ian Hickson's avatar
Ian Hickson committed
285
  /// image with the bottom right corner of its layout bounds. Similarly, an
286
  /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
Ian Hickson's avatar
Ian Hickson committed
287 288 289
  /// middle of the bottom edge of its layout bounds.
  ///
  /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
290
  /// [AlignmentDirectional]), then an ambient [Directionality] widget
Ian Hickson's avatar
Ian Hickson committed
291 292
  /// must be in scope.
  ///
293
  /// Defaults to [Alignment.center].
294 295 296 297 298 299 300
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
301
  final AlignmentGeometry alignment;
302 303 304 305

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

Ian Hickson's avatar
Ian Hickson committed
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
  /// 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;

323 324
  /// Whether to exclude this image from semantics.
  ///
325 326
  /// This is useful for images which do not contribute meaningful information
  /// to an application.
327 328
  final bool excludeFromSemantics;

329
  /// A semantic description of the [image].
330 331 332
  ///
  /// Used to provide a description of the [image] to TalkBack on Android, and
  /// VoiceOver on iOS.
333
  ///
334 335 336
  /// This description will be used both while the [placeholder] is shown and
  /// once the image has loaded.
  final String imageSemanticLabel;
337

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
  Image _image({
    @required ImageProvider image,
    ImageFrameBuilder frameBuilder,
  }) {
    assert(image != null);
    return Image(
      image: image,
      frameBuilder: frameBuilder,
      width: width,
      height: height,
      fit: fit,
      alignment: alignment,
      repeat: repeat,
      matchTextDirection: matchTextDirection,
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }
356

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  @override
  Widget build(BuildContext context) {
    Widget result = _image(
      image: image,
      frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
        if (wasSynchronouslyLoaded)
          return child;
        return _AnimatedFadeOutFadeIn(
          target: child,
          placeholder: _image(image: placeholder),
          isTargetLoaded: frame != null,
          fadeInDuration: fadeInDuration,
          fadeOutDuration: fadeOutDuration,
          fadeInCurve: fadeInCurve,
          fadeOutCurve: fadeOutCurve,
        );
      },
    );
375

376 377 378 379 380 381 382
    if (!excludeFromSemantics) {
      result = Semantics(
        container: imageSemanticLabel != null,
        image: true,
        label: imageSemanticLabel ?? '',
        child: result,
      );
383 384
    }

385
    return result;
386 387 388
  }
}

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
    Key key,
    @required this.target,
    @required this.placeholder,
    @required this.isTargetLoaded,
    @required this.fadeOutDuration,
    @required this.fadeOutCurve,
    @required this.fadeInDuration,
    @required this.fadeInCurve,
  }) : 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);
407

408 409 410 411 412 413 414
  final Widget target;
  final Widget placeholder;
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;
415 416

  @override
417 418
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
419

420 421 422 423 424
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
  Tween<double> _targetOpacity;
  Tween<double> _placeholderOpacity;
  Animation<double> _targetOpacityAnimation;
  Animation<double> _placeholderOpacityAnimation;
425 426

  @override
427 428 429 430
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
431 432
      (dynamic value) => Tween<double>(begin: value as double),
    ) as Tween<double>;
433 434 435
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
436 437
      (dynamic value) => Tween<double>(begin: value as double),
    ) as Tween<double>;
438 439 440
  }

  @override
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
  void didUpdateTweens() {
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: _placeholderOpacity.chain(CurveTween(curve: widget.fadeOutCurve)),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: _targetOpacity.chain(CurveTween(curve: widget.fadeInCurve)),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
    if (!widget.isTargetLoaded && _isValid(_placeholderOpacity) && _isValid(_targetOpacity)) {
      // 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.
      controller.value = controller.upperBound;
466 467 468
    }
  }

469 470
  bool _isValid(Tween<double> tween) {
    return tween.begin != null && tween.end != null;
471 472
  }

473 474
  @override
  Widget build(BuildContext context) {
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
    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>[
        FadeTransition(
          opacity: _targetOpacityAnimation,
          child: widget.target,
        ),
        FadeTransition(
          opacity: _placeholderOpacityAnimation,
          child: widget.placeholder,
        ),
      ],
491
    );
492 493 494
  }

  @override
495 496 497 498
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
499 500
  }
}