blob: 40eaaf390d11814c370fe8d767b2ec3e1a0a2417 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'framework.dart';
import 'image.dart';
import 'implicit_animations.dart';
import 'transitions.dart';
// Examples can assume:
// Uint8List bytes;
/// 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
/// abruptly popping onto the screen.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=pK738Pg9cxc}
///
/// If the [image] emits an [ImageInfo] synchronously, such as when the image
/// has been loaded and cached, the [image] is displayed immediately, and the
/// [placeholder] is never displayed.
///
/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
/// animation of the [placeholder].
///
/// The [fadeInDuration] and [fadeInCurve] properties control the fade-in
/// animation of the target [image].
///
/// Prefer a [placeholder] that's already cached so that it is displayed
/// immediately. This prevents it from popping onto the screen.
///
/// 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
/// 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
/// new [ImageStream.key] is different, this widget subscribes to the new stream
/// 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]).
///
/// {@tool snippet}
///
/// ```dart
/// FadeInImage(
/// // here `bytes` is a Uint8List containing the bytes for the in-memory image
/// placeholder: MemoryImage(bytes),
/// image: NetworkImage('https://backend.example.com/image.png'),
/// )
/// ```
/// {@end-tool}
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.
///
/// The [placeholder] and [image] may be composed in a [ResizeImage] to provide
/// a custom decode/cache size.
///
/// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
/// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
/// [matchTextDirection] arguments must not be null.
///
/// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
const FadeInImage({
Key key,
@required this.placeholder,
this.placeholderErrorBuilder,
@required this.image,
this.imageErrorBuilder,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
}) : assert(placeholder != null),
assert(image != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
/// Creates a widget that uses a placeholder image stored in memory while
/// loading the final image from the network.
///
/// The `placeholder` argument contains the bytes of the in-memory image.
///
/// The `image` argument is the URL of the final image.
///
/// The `placeholderScale` and `imageScale` arguments are passed to their
/// respective [ImageProvider]s (see also [ImageInfo.scale]).
///
/// 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].
///
/// The [placeholder], [image], [placeholderScale], [imageScale],
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
/// [alignment], [repeat], and [matchTextDirection] arguments must not be
/// null.
///
/// 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,
this.placeholderErrorBuilder,
@required String image,
this.imageErrorBuilder,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int placeholderCacheWidth,
int placeholderCacheHeight,
int imageCacheWidth,
int imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
assert(placeholderScale != null),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
super(key: key);
/// Creates a widget that uses a placeholder image stored in an asset bundle
/// while loading the final image from the network.
///
/// The `placeholder` argument is the key of the image in the asset bundle.
///
/// The `image` argument is the URL of the final image.
///
/// The `placeholderScale` and `imageScale` arguments are passed to their
/// respective [ImageProvider]s (see also [ImageInfo.scale]).
///
/// If `placeholderScale` is omitted or is null, pixel-density-aware asset
/// resolution will be attempted for the [placeholder] image. Otherwise, the
/// exact asset specified will be used.
///
/// 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].
///
/// The [placeholder], [image], [imageScale], [fadeOutDuration],
/// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
/// and [matchTextDirection] arguments must not be null.
///
/// 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,
this.placeholderErrorBuilder,
@required String image,
this.imageErrorBuilder,
AssetBundle bundle,
double placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int placeholderCacheWidth,
int placeholderCacheHeight,
int imageCacheWidth,
int imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
: ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
super(key: key);
/// Image displayed while the target [image] is loading.
final ImageProvider placeholder;
/// 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.
final ImageErrorWidgetBuilder placeholderErrorBuilder;
/// The target image that is displayed once it has loaded.
final ImageProvider image;
/// 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.
final ImageErrorWidgetBuilder imageErrorBuilder;
/// 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.
///
/// The alignment aligns the given position in the image to the given position
/// 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
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// image with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
/// middle of the bottom edge of its layout bounds.
///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
/// [AlignmentDirectional]), then an ambient [Directionality] widget
/// must be in scope.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
/// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat;
/// 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;
/// Whether to exclude this image from semantics.
///
/// This is useful for images which do not contribute meaningful information
/// to an application.
final bool excludeFromSemantics;
/// A semantic description of the [image].
///
/// Used to provide a description of the [image] to TalkBack on Android, and
/// VoiceOver on iOS.
///
/// This description will be used both while the [placeholder] is shown and
/// once the image has loaded.
final String imageSemanticLabel;
Image _image({
@required ImageProvider image,
ImageErrorWidgetBuilder errorBuilder,
ImageFrameBuilder frameBuilder,
}) {
assert(image != null);
return Image(
image: image,
errorBuilder: errorBuilder,
frameBuilder: frameBuilder,
width: width,
height: height,
fit: fit,
alignment: alignment,
repeat: repeat,
matchTextDirection: matchTextDirection,
gaplessPlayback: true,
excludeFromSemantics: true,
);
}
@override
Widget build(BuildContext context) {
Widget result = _image(
image: image,
errorBuilder: imageErrorBuilder,
frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded)
return child;
return _AnimatedFadeOutFadeIn(
target: child,
placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
isTargetLoaded: frame != null,
fadeInDuration: fadeInDuration,
fadeOutDuration: fadeOutDuration,
fadeInCurve: fadeInCurve,
fadeOutCurve: fadeOutCurve,
);
},
);
if (!excludeFromSemantics) {
result = Semantics(
container: imageSemanticLabel != null,
image: true,
label: imageSemanticLabel ?? '',
child: result,
);
}
return result;
}
}
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);
final Widget target;
final Widget placeholder;
final bool isTargetLoaded;
final Duration fadeInDuration;
final Duration fadeOutDuration;
final Curve fadeInCurve;
final Curve fadeOutCurve;
@override
_AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
Tween<double> _targetOpacity;
Tween<double> _placeholderOpacity;
Animation<double> _targetOpacityAnimation;
Animation<double> _placeholderOpacityAnimation;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_targetOpacity = visitor(
_targetOpacity,
widget.isTargetLoaded ? 1.0 : 0.0,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>;
_placeholderOpacity = visitor(
_placeholderOpacity,
widget.isTargetLoaded ? 0.0 : 1.0,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>;
}
@override
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(),
),
]))..addStatusListener((AnimationStatus status) {
if (_placeholderOpacityAnimation.isCompleted) {
// Need to rebuild to remove placeholder now that it is invisibile.
setState(() {});
}
});
_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;
}
}
bool _isValid(Tween<double> tween) {
return tween.begin != null && tween.end != null;
}
@override
Widget build(BuildContext context) {
final Widget target = FadeTransition(
opacity: _targetOpacityAnimation,
child: widget.target,
);
if (_placeholderOpacityAnimation.isCompleted) {
return target;
}
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>[
target,
FadeTransition(
opacity: _placeholderOpacityAnimation,
child: widget.placeholder,
),
],
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
}
}