blob: f3451f44f5cc334151e253e021612c4a5b738e96 [file]
// 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 'package:flutter/foundation.dart';
import 'basic.dart';
import 'framework.dart';
import 'image.dart';
import 'implicit_animations.dart';
// Examples can assume:
// late Uint8List bytes;
/// Defines how a [FadeInImage] transitions from its [FadeInImage.placeholder]
/// to its [FadeInImage.image] once the image has loaded.
enum FadeInImageTransition {
/// The placeholder fades out, then the image fades in.
///
/// The fade-out of the placeholder is controlled by
/// [FadeInImage.fadeOutDuration] and [FadeInImage.fadeOutCurve], and the
/// subsequent fade-in of the image is controlled by
/// [FadeInImage.fadeInDuration] and [FadeInImage.fadeInCurve].
sequential,
/// The image fades in on top of the placeholder.
///
/// The placeholder remains at full opacity until the image has fully faded
/// in, so there is no intermediate frame where neither image is fully opaque.
///
/// The placeholder is not faded out in this mode, so
/// [FadeInImage.fadeOutDuration] and [FadeInImage.fadeOutCurve] must be
/// omitted or null. Only [FadeInImage.fadeInDuration] and
/// [FadeInImage.fadeInCurve] are used.
fadeInOver,
}
/// 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 [NetworkImage.new],
/// 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 [transition] property controls how the widget animates from the
/// [placeholder] to the [image]. By default
/// ([FadeInImageTransition.sequential]), the [placeholder] fades out before the
/// [image] fades in. Set it to [FadeInImageTransition.fadeInOver] to instead
/// have the [image] fade in on top of the [placeholder], keeping it visible
/// until the image is fully loaded.
///
/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
/// animation of the [placeholder]. They have no effect when [transition] is
/// [FadeInImageTransition.fadeInOver].
///
/// 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: const NetworkImage('https://backend.example.com/image.png'),
/// )
/// ```
/// {@end-tool}
class FadeInImage extends StatefulWidget {
/// Creates a widget that displays a [placeholder] while an [image] is loading,
/// then transitions to the image according to [transition].
///
/// By default ([FadeInImageTransition.sequential]), the placeholder fades out
/// and then the image fades in. Set [transition] to
/// [FadeInImageTransition.fadeInOver] to instead fade the image in on top of
/// the placeholder; in that case [fadeOutDuration] and [fadeOutCurve] must be
/// omitted or null.
///
/// The [placeholder] and [image] may be composed in a [ResizeImage] to provide
/// a custom decode/cache size.
///
/// The [placeholder] and [image] may have their own BoxFit settings via [fit]
/// and [placeholderFit].
///
/// The [placeholder] and [image] may have their own FilterQuality settings via [filterQuality]
/// and [placeholderFilterQuality].
///
/// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
const FadeInImage({
super.key,
required this.placeholder,
this.placeholderErrorBuilder,
required this.image,
this.imageErrorBuilder,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
Duration? fadeOutDuration,
Curve? fadeOutCurve,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.color,
this.colorBlendMode,
this.placeholderColor,
this.placeholderColorBlendMode,
this.width,
this.height,
this.fit,
this.placeholderFit,
this.filterQuality = FilterQuality.medium,
this.placeholderFilterQuality,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
this.transition = FadeInImageTransition.sequential,
}) : assert(
transition != FadeInImageTransition.fadeInOver ||
(fadeOutDuration == null && fadeOutCurve == null),
'fadeOutDuration and fadeOutCurve must be omitted or null when '
'transition is FadeInImageTransition.fadeInOver, since the placeholder '
'is not faded out in that mode.',
),
fadeOutDuration = fadeOutDuration ?? _defaultFadeOutDuration,
fadeOutCurve = fadeOutCurve ?? _defaultFadeOutCurve;
/// 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].
///
/// If [useLogicalCacheSize] is true, the cache dimensions are interpreted
/// as logical pixels instead of physical pixels. Set this to true when
/// the cache dimensions describe the image's on-screen size in logical
/// pixels. The flag applies to both the placeholder and the image.
///
/// The [transition] argument controls how the widget animates from the
/// [placeholder] to the [image]; see [FadeInImageTransition].
///
/// See also:
///
/// * [Image.memory], which has more details about loading images from
/// memory.
/// * [Image.network], which has more details about loading images from
/// the network.
FadeInImage.memoryNetwork({
super.key,
required Uint8List placeholder,
this.placeholderErrorBuilder,
required String image,
this.imageErrorBuilder,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
Duration? fadeOutDuration,
Curve? fadeOutCurve,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.color,
this.colorBlendMode,
this.placeholderColor,
this.placeholderColorBlendMode,
this.placeholderFit,
this.filterQuality = FilterQuality.medium,
this.placeholderFilterQuality,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
this.transition = FadeInImageTransition.sequential,
int? placeholderCacheWidth,
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
bool useLogicalCacheSize = false,
}) : assert(
transition != FadeInImageTransition.fadeInOver ||
(fadeOutDuration == null && fadeOutCurve == null),
'fadeOutDuration and fadeOutCurve must be omitted or null when '
'transition is FadeInImageTransition.fadeInOver, since the placeholder '
'is not faded out in that mode.',
),
fadeOutDuration = fadeOutDuration ?? _defaultFadeOutDuration,
fadeOutCurve = fadeOutCurve ?? _defaultFadeOutCurve,
placeholder = ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
MemoryImage(placeholder, scale: placeholderScale),
useLogicalSize: useLogicalCacheSize,
),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
NetworkImage(image, scale: imageScale),
useLogicalSize: useLogicalCacheSize,
);
/// 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].
///
/// If [useLogicalCacheSize] is true, the cache dimensions are interpreted
/// as logical pixels instead of physical pixels. Set this to true when
/// the cache dimensions describe the image's on-screen size in logical
/// pixels. The flag applies to both the placeholder and the image.
///
/// The [transition] argument controls how the widget animates from the
/// [placeholder] to the [image]; see [FadeInImageTransition].
///
/// See also:
///
/// * [Image.asset], which has more details about loading images from
/// asset bundles.
/// * [Image.network], which has more details about loading images from
/// the network.
FadeInImage.assetNetwork({
super.key,
required String placeholder,
this.placeholderErrorBuilder,
required String image,
this.imageErrorBuilder,
AssetBundle? bundle,
double? placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
Duration? fadeOutDuration,
Curve? fadeOutCurve,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.color,
this.colorBlendMode,
this.placeholderColor,
this.placeholderColorBlendMode,
this.placeholderFit,
this.filterQuality = FilterQuality.medium,
this.placeholderFilterQuality,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
this.transition = FadeInImageTransition.sequential,
int? placeholderCacheWidth,
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
bool useLogicalCacheSize = false,
}) : assert(
transition != FadeInImageTransition.fadeInOver ||
(fadeOutDuration == null && fadeOutCurve == null),
'fadeOutDuration and fadeOutCurve must be omitted or null when '
'transition is FadeInImageTransition.fadeInOver, since the placeholder '
'is not faded out in that mode.',
),
fadeOutDuration = fadeOutDuration ?? _defaultFadeOutDuration,
fadeOutCurve = fadeOutCurve ?? _defaultFadeOutCurve,
placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale),
useLogicalSize: useLogicalCacheSize,
)
: ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
AssetImage(placeholder, bundle: bundle),
useLogicalSize: useLogicalCacheSize,
),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
NetworkImage(image, scale: imageScale),
useLogicalSize: useLogicalCacheSize,
);
/// 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;
// Defaults for the fade-out animation, used when the corresponding
// constructor argument is null. The constructors assert that those arguments
// are null when [transition] is [FadeInImageTransition.fadeInOver], since the
// placeholder is not faded out in that mode.
static const Duration _defaultFadeOutDuration = Duration(milliseconds: 300);
static const Curve _defaultFadeOutCurve = Curves.easeOut;
/// The duration of the fade-out animation for the [placeholder].
///
/// Defaults to 300 milliseconds.
///
/// Must be omitted or null when [transition] is
/// [FadeInImageTransition.fadeInOver], since the [placeholder] is not faded
/// out in that mode.
final Duration fadeOutDuration;
/// The curve of the fade-out animation for the [placeholder].
///
/// Defaults to [Curves.easeOut].
///
/// Must be omitted or null when [transition] is
/// [FadeInImageTransition.fadeInOver], since the [placeholder] is not faded
/// out in that mode.
final Curve fadeOutCurve;
/// The duration of the fade-in animation for the [image].
///
/// Defaults to 700 milliseconds.
final Duration fadeInDuration;
/// The curve of the fade-in animation for the [image].
///
/// Defaults to [Curves.easeIn].
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, this color is blended with each image pixel using [colorBlendMode].
///
/// Color applies to the [image].
///
/// See Also:
///
/// * [placeholderColor], the color which applies to the [placeholder].
final Color? color;
/// Used to combine [color] with this [image].
///
/// The default is [BlendMode.srcIn]. In terms of the blend mode, [color] is
/// the source and this image is the destination.
///
/// See also:
///
/// * [BlendMode], which includes an illustration of the effect of each blend mode.
/// * [placeholderColorBlendMode], the color blend mode which applies to the [placeholder].
final BlendMode? colorBlendMode;
/// If non-null, this color is blended with each placeholder image pixel using [placeholderColorBlendMode].
///
/// Color applies to the [placeholder].
///
/// See Also:
///
/// * [color], the color which applies to the [image].
final Color? placeholderColor;
/// Used to combine [placeholderColor] with the [placeholder] image.
///
/// The default is [BlendMode.srcIn]. In terms of the blend mode, [placeholderColor] is
/// the source and this placeholder is the destination.
///
/// See also:
///
/// * [BlendMode], which includes an illustration of the effect of each blend mode.
/// * [colorBlendMode], the color blend mode which applies to the [image].
final BlendMode? placeholderColorBlendMode;
/// 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 inscribe the placeholder image into the space allocated during layout.
///
/// If not value set, it will fallback to [fit].
final BoxFit? placeholderFit;
/// 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;
/// 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;
/// How the widget transitions from the [placeholder] to the [image] once it
/// has loaded.
///
/// [FadeInImageTransition.sequential] fades the [placeholder] out before
/// fading the [image] in, while [FadeInImageTransition.fadeInOver] fades the
/// [image] in on top of the still-visible [placeholder].
///
/// Defaults to [FadeInImageTransition.sequential].
///
/// {@tool dartpad}
/// This example switches between [FadeInImageTransition.sequential] and
/// [FadeInImageTransition.fadeInOver] to compare how each one animates from
/// the [placeholder] to the [image].
///
/// ** See code in examples/api/lib/widgets/fade_in_image/fade_in_image.transition.0.dart **
/// {@end-tool}
final FadeInImageTransition transition;
@override
State<FadeInImage> createState() => _FadeInImageState();
}
class _FadeInImageState extends State<FadeInImage> {
static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);
bool targetLoaded = false;
// 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);
Image _image({
required ImageProvider image,
ImageErrorWidgetBuilder? errorBuilder,
ImageFrameBuilder? frameBuilder,
BoxFit? fit,
Color? color,
BlendMode? colorBlendMode,
required FilterQuality filterQuality,
required Animation<double> opacity,
}) {
return Image(
image: image,
errorBuilder: errorBuilder,
frameBuilder: frameBuilder,
opacity: opacity,
width: widget.width,
height: widget.height,
fit: fit,
color: color,
colorBlendMode: colorBlendMode,
filterQuality: filterQuality,
alignment: widget.alignment,
repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
gaplessPlayback: true,
excludeFromSemantics: true,
);
}
@override
Widget build(BuildContext context) {
Widget result = _image(
image: widget.image,
errorBuilder: widget.imageErrorBuilder,
opacity: _imageAnimation,
fit: widget.fit,
color: widget.color,
colorBlendMode: widget.colorBlendMode,
filterQuality: widget.filterQuality,
frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
targetLoaded = true;
}
return _AnimatedFadeOutFadeIn(
target: child,
targetProxyAnimation: _imageAnimation,
placeholder: _image(
image: widget.placeholder,
errorBuilder: widget.placeholderErrorBuilder,
opacity: _placeholderAnimation,
color: widget.placeholderColor,
colorBlendMode: widget.placeholderColorBlendMode,
fit: widget.placeholderFit ?? widget.fit,
filterQuality: widget.placeholderFilterQuality ?? widget.filterQuality,
),
placeholderProxyAnimation: _placeholderAnimation,
isTargetLoaded: targetLoaded,
wasSynchronouslyLoaded: wasSynchronouslyLoaded,
fadeInDuration: widget.fadeInDuration,
fadeOutDuration: widget.fadeOutDuration,
fadeInCurve: widget.fadeInCurve,
fadeOutCurve: widget.fadeOutCurve,
transition: widget.transition,
);
},
);
if (!widget.excludeFromSemantics) {
result = Semantics(
container: widget.imageSemanticLabel != null,
image: true,
label: widget.imageSemanticLabel ?? '',
child: result,
);
}
return result;
}
}
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
const _AnimatedFadeOutFadeIn({
required this.target,
required this.targetProxyAnimation,
required this.placeholder,
required this.placeholderProxyAnimation,
required this.isTargetLoaded,
required this.fadeOutDuration,
required this.fadeOutCurve,
required this.fadeInDuration,
required this.fadeInCurve,
required this.wasSynchronouslyLoaded,
required this.transition,
}) : assert(!wasSynchronouslyLoaded || isTargetLoaded),
super(
duration: transition == FadeInImageTransition.fadeInOver
? fadeInDuration
: fadeInDuration + fadeOutDuration,
);
final Widget target;
final ProxyAnimation targetProxyAnimation;
final Widget placeholder;
final ProxyAnimation placeholderProxyAnimation;
final bool isTargetLoaded;
final Duration fadeInDuration;
final Duration fadeOutDuration;
final Curve fadeInCurve;
final Curve fadeOutCurve;
final bool wasSynchronouslyLoaded;
final FadeInImageTransition transition;
@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,
// fadeInOver: placeholder stays opaque; it's removed from the tree
// once the image animation completes rather than being faded out.
(widget.isTargetLoaded && widget.transition != FadeInImageTransition.fadeInOver)
? 0.0
: 1.0,
(dynamic value) => Tween<double>(begin: value as double),
)
as Tween<double>?;
}
@override
void initState() {
super.initState();
// Both opacity animations are driven by the controller, and they are
// recreated whenever the tweens change, so the listener is registered on
// the controller itself for the lifetime of this state.
controller.addStatusListener(_handleAnimationStatusChanged);
}
@override
void dispose() {
controller.removeStatusListener(_handleAnimationStatusChanged);
super.dispose();
}
void _handleAnimationStatusChanged(AnimationStatus status) {
if (status.isCompleted) {
// Need to rebuild to remove placeholder now that it is invisible.
setState(() {});
}
}
TweenSequenceItem<double> _fadeInItem(double weight) {
return TweenSequenceItem<double>(
tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
weight: weight,
);
}
// Drives [items] from the animation, or holds [valueWithoutAnimation] when
// every phase was dropped for having a zero duration.
Animation<double> _driveOpacity(
List<TweenSequenceItem<double>> items,
double valueWithoutAnimation,
) {
return animation.drive(
items.isEmpty ? ConstantTween<double>(valueWithoutAnimation) : TweenSequence<double>(items),
);
}
@override
void didUpdateTweens() {
if (widget.wasSynchronouslyLoaded) {
// Opacity animations should not be reset if image was synchronously loaded.
return;
}
// A TweenSequenceItem must have a positive weight, so a phase whose
// duration is zero is left out of the sequence rather than given a weight
// of zero. If that leaves no phase at all, the opacity jumps straight to
// its final value instead of being animated.
final double fadeOutWeight = widget.fadeOutDuration.inMilliseconds.toDouble();
final double fadeInWeight = widget.fadeInDuration.inMilliseconds.toDouble();
final bool hasFadeOut = fadeOutWeight > 0;
final bool hasFadeIn = fadeInWeight > 0;
switch (widget.transition) {
case FadeInImageTransition.fadeInOver:
// The image fades in on top of the placeholder, which stays fully
// opaque until it is removed from the tree once the fade-in completes.
_placeholderOpacityAnimation = animation.drive(ConstantTween<double>(1.0));
_targetOpacityAnimation = _driveOpacity(<TweenSequenceItem<double>>[
if (hasFadeIn) _fadeInItem(fadeInWeight),
], _targetOpacity!.end!);
case FadeInImageTransition.sequential:
// The placeholder fades out, and only then does the image fade in.
_placeholderOpacityAnimation = _driveOpacity(<TweenSequenceItem<double>>[
if (hasFadeOut)
TweenSequenceItem<double>(
tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
weight: fadeOutWeight,
),
if (hasFadeIn)
TweenSequenceItem<double>(
tween: ConstantTween<double>(_placeholderOpacity!.end!),
weight: fadeInWeight,
),
], _placeholderOpacity!.end!);
_targetOpacityAnimation = _driveOpacity(<TweenSequenceItem<double>>[
// The image is only held back while the placeholder fades out if it
// is going to fade in afterwards.
if (hasFadeOut && hasFadeIn)
TweenSequenceItem<double>(tween: ConstantTween<double>(0), weight: fadeOutWeight),
if (hasFadeIn) _fadeInItem(fadeInWeight),
], _targetOpacity!.end!);
}
widget.targetProxyAnimation.parent = _targetOpacityAnimation;
widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
}
@override
Widget build(BuildContext context) {
if (widget.wasSynchronouslyLoaded || (_placeholderOpacityAnimation?.isCompleted ?? true)) {
return widget.target;
}
// In fadeInOver mode the target image is painted on top of the placeholder
// so it can fade in over it. In sequential mode the placeholder is on top
// so it can fade out while the target is revealed beneath it.
final children = widget.transition == FadeInImageTransition.fadeInOver
? <Widget>[widget.placeholder, widget.target]
: <Widget>[widget.target, widget.placeholder];
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: children,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation),
);
properties.add(
DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation),
);
}
}