Unverified Commit 56940b54 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Update FadeInImage to use new Image APIs (#33370)

This updates FadeInImage to use the new Image.frameBuilder
API (added in #33369), to greatly simplify the implementation
of FadeInImage.

This also removes the FadeInImage.placeholderSemanticLabel property.
This property was added in #28799 for the sake of completeness (the bug
it fixed was the lack of any semantic label support in FadeInImage), but a
placeholder is a transient visual artifact, not something that affects the
underlying semantic meaning of the image.
parent 0f2254a5
......@@ -10,7 +10,8 @@ import 'package:flutter/services.dart';
import 'basic.dart';
import 'framework.dart';
import 'image.dart';
import 'ticker_provider.dart';
import 'implicit_animations.dart';
import 'transitions.dart';
// Examples can assume:
// Uint8List bytes;
......@@ -20,30 +21,30 @@ import 'ticker_provider.dart';
///
/// 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 pops onto the screen.
/// 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
/// has been loaded and cached, the [image] is displayed immediately, and the
/// [placeholder] is never displayed.
///
/// [fadeOutDuration] and [fadeOutCurve] control the fade-out animation of the
/// placeholder.
/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
/// animation of the [placeholder].
///
/// [fadeInDuration] and [fadeInCurve] control the fade-in animation of the
/// target [image].
/// 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 in one
/// frame. This prevents it from popping onto the screen.
/// 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
/// 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
/// 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
......@@ -61,28 +62,21 @@ import 'ticker_provider.dart';
/// )
/// ```
/// {@end-tool}
class FadeInImage extends StatefulWidget {
/// Creates a widget that displays a [placeholder] while an [image] is loading
/// then cross-fades to display the [image].
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], [image], [fadeOutDuration], [fadeOutCurve],
/// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
/// [matchTextDirection] arguments must not be null.
///
/// There are two different semantic label for the class.
/// [placeholderSemanticLabel] is used for defining a semantics label for
/// [placeholder]. [imageSemanticLabel] is used for defining a semantics label
/// for [image]
///
/// If [excludeFromSemantics] is true, then [placeholderSemanticLabel] and
/// [imageSemanticLabel] will be ignored.
/// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
const FadeInImage({
Key key,
@required this.placeholder,
@required this.image,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -107,12 +101,12 @@ class FadeInImage extends StatefulWidget {
/// Creates a widget that uses a placeholder image stored in memory while
/// loading the final image from the network.
///
/// [placeholder] contains the bytes of the in-memory image.
/// The `placeholder` argument contains the bytes of the in-memory image.
///
/// [image] is the URL of the final image.
/// The `image` argument is the URL of the final image.
///
/// [placeholderScale] and [imageScale] are passed to their respective
/// [ImageProvider]s (see also [ImageInfo.scale]).
/// The `placeholderScale` and `imageScale` arguments are passed to their
/// respective [ImageProvider]s (see also [ImageInfo.scale]).
///
/// The [placeholder], [image], [placeholderScale], [imageScale],
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
......@@ -133,7 +127,6 @@ class FadeInImage extends StatefulWidget {
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -162,14 +155,14 @@ class FadeInImage extends StatefulWidget {
/// Creates a widget that uses a placeholder image stored in an asset bundle
/// while loading the final image from the network.
///
/// [placeholder] is the key of the image in the asset bundle.
/// The `placeholder` argument is the key of the image in the asset bundle.
///
/// [image] is the URL of the final image.
/// The `image` argument is the URL of the final image.
///
/// [placeholderScale] and [imageScale] are passed to their respective
/// [ImageProvider]s (see also [ImageInfo.scale]).
/// The `placeholderScale` and `imageScale` arguments are passed to their
/// respective [ImageProvider]s (see also [ImageInfo.scale]).
///
/// If [placeholderScale] is omitted or is null, the pixel-density-aware asset
/// 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.
///
......@@ -192,7 +185,6 @@ class FadeInImage extends StatefulWidget {
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.placeholderSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
......@@ -222,7 +214,7 @@ class FadeInImage extends StatefulWidget {
/// Image displayed while the target [image] is loading.
final ImageProvider placeholder;
/// The target image that is displayed.
/// The target image that is displayed once it has loaded.
final ImageProvider image;
/// The duration of the fade-out animation for the [placeholder].
......@@ -305,271 +297,179 @@ class FadeInImage extends StatefulWidget {
/// Whether to exclude this image from semantics.
///
/// Useful for images which do not contribute meaningful information to an
/// application.
/// This is useful for images which do not contribute meaningful information
/// to an application.
final bool excludeFromSemantics;
/// A Semantic description of the [placeholder].
///
/// Used to provide a description of the [placeholder] to TalkBack on Android, and
/// VoiceOver on iOS.
final String placeholderSemanticLabel;
/// A Semantic description of the [image].
/// A semantic description of the [image].
///
/// Used to provide a description of the [image] to TalkBack on Android, and
/// VoiceOver on iOS.
final String imageSemanticLabel;
@override
State<StatefulWidget> createState() => _FadeInImageState();
}
/// The phases a [FadeInImage] goes through.
@visibleForTesting
enum FadeInImagePhase {
/// The initial state.
///
/// We do not yet know whether the target image is ready and therefore no
/// animation is necessary, or whether we need to use the placeholder and
/// wait for the image to load.
start,
/// Waiting for the target image to load.
waiting,
/// Fading out previous image.
fadeOut,
/// Fading in new image.
fadeIn,
/// Fade-in complete.
completed,
}
typedef _ImageProviderResolverListener = void Function();
class _ImageProviderResolver {
_ImageProviderResolver({
@required this.state,
@required this.listener,
});
final _FadeInImageState state;
final _ImageProviderResolverListener listener;
FadeInImage get widget => state.widget;
ImageStream _imageStream;
ImageInfo _imageInfo;
void resolve(ImageProvider provider) {
final ImageStream oldImageStream = _imageStream;
_imageStream = provider.resolve(createLocalImageConfiguration(
state.context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
assert(_imageStream != null);
if (_imageStream.key != oldImageStream?.key) {
final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
_imageInfo = imageInfo;
listener();
}
/// This description will be used both while the [placeholder] is shown and
/// once the image has loaded.
final String imageSemanticLabel;
void stopListening() {
_imageStream?.removeListener(ImageStreamListener(_handleImageChanged));
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,
);
}
}
class _FadeInImageState extends State<FadeInImage> with TickerProviderStateMixin {
_ImageProviderResolver _imageResolver;
_ImageProviderResolver _placeholderResolver;
AnimationController _controller;
Animation<double> _animation;
FadeInImagePhase _phase = FadeInImagePhase.start;
FadeInImagePhase get phase => _phase;
@override
void initState() {
_imageResolver = _ImageProviderResolver(state: this, listener: _updatePhase);
_placeholderResolver = _ImageProviderResolver(state: this, listener: () {
setState(() {
// Trigger rebuild to display the placeholder image
});
});
_controller = AnimationController(
value: 1.0,
vsync: this,
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,
);
},
);
_controller.addListener(() {
setState(() {
// Trigger rebuild to update opacity value.
});
});
_controller.addStatusListener((AnimationStatus status) {
_updatePhase();
});
super.initState();
}
@override
void didChangeDependencies() {
_resolveImage();
super.didChangeDependencies();
if (!excludeFromSemantics) {
result = Semantics(
container: imageSemanticLabel != null,
image: true,
label: imageSemanticLabel ?? '',
child: result,
);
}
@override
void didUpdateWidget(FadeInImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.image != oldWidget.image || widget.placeholder != oldWidget.placeholder)
_resolveImage();
return result;
}
}
@override
void reassemble() {
_resolveImage(); // in case the image cache was flushed
super.reassemble();
}
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);
void _resolveImage() {
_imageResolver.resolve(widget.image);
final Widget target;
final Widget placeholder;
final bool isTargetLoaded;
final Duration fadeInDuration;
final Duration fadeOutDuration;
final Curve fadeInCurve;
final Curve fadeOutCurve;
// No need to resolve the placeholder if we are past the placeholder stage.
if (_isShowingPlaceholder)
_placeholderResolver.resolve(widget.placeholder);
@override
_AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
if (_phase == FadeInImagePhase.start)
_updatePhase();
}
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
Tween<double> _targetOpacity;
Tween<double> _placeholderOpacity;
Animation<double> _targetOpacityAnimation;
Animation<double> _placeholderOpacityAnimation;
void _updatePhase() {
setState(() {
switch (_phase) {
case FadeInImagePhase.start:
if (_imageResolver._imageInfo != null)
_phase = FadeInImagePhase.completed;
else
_phase = FadeInImagePhase.waiting;
break;
case FadeInImagePhase.waiting:
if (_imageResolver._imageInfo != null) {
// Received image data. Begin placeholder fade-out.
_controller.duration = widget.fadeOutDuration;
_animation = CurvedAnimation(
parent: _controller,
curve: widget.fadeOutCurve,
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_targetOpacity = visitor(
_targetOpacity,
widget.isTargetLoaded ? 1.0 : 0.0,
(dynamic value) => Tween<double>(begin: value),
);
_phase = FadeInImagePhase.fadeOut;
_controller.reverse(from: 1.0);
}
break;
case FadeInImagePhase.fadeOut:
if (_controller.status == AnimationStatus.dismissed) {
// Done fading out placeholder. Begin target image fade-in.
_controller.duration = widget.fadeInDuration;
_animation = CurvedAnimation(
parent: _controller,
curve: widget.fadeInCurve,
_placeholderOpacity = visitor(
_placeholderOpacity,
widget.isTargetLoaded ? 0.0 : 1.0,
(dynamic value) => Tween<double>(begin: value),
);
_phase = FadeInImagePhase.fadeIn;
_placeholderResolver.stopListening();
_controller.forward(from: 0.0);
}
break;
case FadeInImagePhase.fadeIn:
if (_controller.status == AnimationStatus.completed) {
// Done finding in new image.
_phase = FadeInImagePhase.completed;
}
break;
case FadeInImagePhase.completed:
// Nothing to do.
break;
}
});
}
@override
void dispose() {
_imageResolver.stopListening();
_placeholderResolver.stopListening();
_controller.dispose();
super.dispose();
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;
}
bool get _isShowingPlaceholder {
assert(_phase != null);
switch (_phase) {
case FadeInImagePhase.start:
case FadeInImagePhase.waiting:
case FadeInImagePhase.fadeOut:
return true;
case FadeInImagePhase.fadeIn:
case FadeInImagePhase.completed:
return false;
}
return null;
}
ImageInfo get _imageInfo {
return _isShowingPlaceholder
? _placeholderResolver._imageInfo
: _imageResolver._imageInfo;
}
String get _semanticLabel {
return _isShowingPlaceholder
? widget.placeholderSemanticLabel
: widget.imageSemanticLabel;
bool _isValid(Tween<double> tween) {
return tween.begin != null && tween.end != null;
}
@override
Widget build(BuildContext context) {
assert(_phase != FadeInImagePhase.start);
final ImageInfo imageInfo = _imageInfo;
final RawImage image = RawImage(
image: imageInfo?.image,
width: widget.width,
height: widget.height,
scale: imageInfo?.scale ?? 1.0,
color: Color.fromRGBO(255, 255, 255, _animation?.value ?? 1.0),
colorBlendMode: BlendMode.modulate,
fit: widget.fit,
alignment: widget.alignment,
repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
);
if (widget.excludeFromSemantics) {
return image;
}
return Semantics(
container: _semanticLabel != null,
image: true,
label: _semanticLabel ?? '',
child: image,
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,
),
],
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(EnumProperty<FadeInImagePhase>('phase', _phase));
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
description.add(DiagnosticsProperty<ImageStream>('image stream', _imageResolver._imageStream));
description.add(DiagnosticsProperty<ImageStream>('placeholder stream', _placeholderResolver._imageStream));
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
}
}
......@@ -49,3 +49,8 @@ Future<ui.Image> createTestImage() {
ui.decodeImageFromList(Uint8List.fromList(kTransparentImage), uiImage.complete);
return uiImage.future;
}
class FakeImageConfiguration implements ImageConfiguration {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
......@@ -9,183 +9,318 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../painting/image_test_utils.dart';
const Duration animationDuration = Duration(milliseconds: 50);
class FadeInImageParts {
const FadeInImageParts(this.fadeInImageElement, this.placeholder, this.target)
: assert(fadeInImageElement != null),
assert(target != null);
final ComponentElement fadeInImageElement;
final FadeInImageElements placeholder;
final FadeInImageElements target;
State get state {
StatefulElement animatedFadeOutFadeInElement;
fadeInImageElement.visitChildren((Element child) {
expect(animatedFadeOutFadeInElement, isNull);
animatedFadeOutFadeInElement = child;
});
expect(animatedFadeOutFadeInElement, isNotNull);
return animatedFadeOutFadeInElement.state;
}
Element get semanticsElement {
Element result;
fadeInImageElement.visitChildren((Element child) {
if (child.widget is Semantics)
result = child;
});
return result;
}
}
class FadeInImageElements {
const FadeInImageElements(this.rawImageElement, this.fadeTransitionElement);
final Element rawImageElement;
final Element fadeTransitionElement;
RawImage get rawImage => rawImageElement.widget;
FadeTransition get fadeTransition => fadeTransitionElement?.widget;
double get opacity => fadeTransition == null ? 1 : fadeTransition.opacity.value;
}
FadeInImageParts findFadeInImage(WidgetTester tester) {
final List<FadeInImageElements> elements = <FadeInImageElements>[];
final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
ComponentElement fadeInImageElement;
for (Element rawImageElement in rawImageElements) {
Element fadeTransitionElement;
rawImageElement.visitAncestorElements((Element ancestor) {
if (ancestor.widget is FadeTransition) {
fadeTransitionElement = ancestor;
} else if (ancestor.widget is FadeInImage) {
if (fadeInImageElement == null) {
fadeInImageElement = ancestor;
} else {
expect(fadeInImageElement, same(ancestor));
}
return false;
}
return true;
});
expect(fadeInImageElement, isNotNull);
elements.add(FadeInImageElements(rawImageElement, fadeTransitionElement));
}
if (elements.length == 2) {
return FadeInImageParts(fadeInImageElement, elements.last, elements.first);
} else {
expect(elements, hasLength(1));
return FadeInImageParts(fadeInImageElement, null, elements.first);
}
}
Future<void> main() async {
// These must run outside test zone to complete
final ui.Image targetImage = await createTestImage();
final ui.Image placeholderImage = await createTestImage();
final ui.Image secondPlaceholderImage = await createTestImage();
final ui.Image replacementImage = await createTestImage();
group('FadeInImage', () {
testWidgets('animates uncached image and shows cached image immediately', (WidgetTester tester) async {
// State type is private, hence using dynamic.
dynamic state() => tester.state(find.byType(FadeInImage));
RawImage displayedImage() => tester.widget(find.byType(RawImage));
// The placeholder is expected to be already loaded
testWidgets('animates an uncached image', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// Test case: long loading image
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: const Duration(milliseconds: 50),
fadeInDuration: const Duration(milliseconds: 50),
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
fadeOutCurve: Curves.linear,
fadeInCurve: Curves.linear,
excludeFromSemantics: true,
));
expect(displayedImage().image, null); // image providers haven't completed yet
expect(findFadeInImage(tester).placeholder.rawImage.image, null);
expect(findFadeInImage(tester).target.rawImage.image, null);
placeholderProvider.complete();
await tester.pump();
expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
expect(findFadeInImage(tester).target.rawImage.image, null);
expect(displayedImage().image, same(placeholderImage)); // placeholder completed
expect(state().phase, FadeInImagePhase.waiting);
imageProvider.complete(); // load the image
expect(state().phase, FadeInImagePhase.fadeOut); // fade out placeholder
for (int i = 0; i < 7; i += 1) {
expect(displayedImage().image, same(placeholderImage));
imageProvider.complete();
await tester.pump();
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
expect(parts.placeholder.rawImage.image, same(placeholderImage));
expect(parts.target.rawImage.image, same(targetImage));
expect(parts.placeholder.opacity, moreOrLessEquals(1 - i / 5));
expect(parts.target.opacity, 0);
await tester.pump(const Duration(milliseconds: 10));
}
expect(displayedImage().image, same(targetImage));
expect(state().phase, FadeInImagePhase.fadeIn); // fade in image
for (int i = 0; i < 6; i += 1) {
expect(displayedImage().image, same(targetImage));
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
expect(parts.placeholder.rawImage.image, same(placeholderImage));
expect(parts.target.rawImage.image, same(targetImage));
expect(parts.placeholder.opacity, 0);
expect(parts.target.opacity, moreOrLessEquals(i / 5));
await tester.pump(const Duration(milliseconds: 10));
}
expect(state().phase, FadeInImagePhase.completed); // done
expect(displayedImage().image, same(targetImage));
// Test case: re-use state object (didUpdateWidget)
final dynamic stateBeforeDidUpdateWidget = state();
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
final dynamic stateAfterDidUpdateWidget = state();
expect(stateAfterDidUpdateWidget, same(stateBeforeDidUpdateWidget));
expect(stateAfterDidUpdateWidget.phase, FadeInImagePhase.completed); // completes immediately
expect(displayedImage().image, same(targetImage));
// Test case: new state object but cached image
final dynamic stateBeforeRecreate = state();
await tester.pumpWidget(Container()); // clear widget tree to prevent state reuse
expect(findFadeInImage(tester).target.rawImage.image, same(targetImage));
expect(findFadeInImage(tester).target.opacity, 1);
});
testWidgets('shows a cached image immediately when skipFadeOnSynchronousLoad=true', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
imageProvider.resolve(FakeImageConfiguration());
imageProvider.complete();
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
expect(displayedImage().image, same(targetImage));
final dynamic stateAfterRecreate = state();
expect(stateAfterRecreate, isNot(same(stateBeforeRecreate)));
expect(stateAfterRecreate.phase, FadeInImagePhase.completed); // completes immediately
expect(displayedImage().image, same(targetImage));
});
testWidgets('handles a updating the placeholder image', (WidgetTester tester) async {
RawImage displayedImage() => tester.widget(find.byType(RawImage));
expect(findFadeInImage(tester).target.rawImage.image, same(targetImage));
expect(findFadeInImage(tester).placeholder, isNull);
expect(findFadeInImage(tester).target.opacity, 1);
});
// The placeholder is expected to be already loaded
testWidgets('handles updating the placeholder image', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider secondPlaceholderProvider = TestImageProvider(secondPlaceholderImage);
// Test case: long loading image
final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: const Duration(milliseconds: 50),
fadeInDuration: const Duration(milliseconds: 50),
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
final State state = findFadeInImage(tester).state;
placeholderProvider.complete();
await tester.pump();
expect(displayedImage().image, same(placeholderImage)); // placeholder completed
expect(displayedImage().image, isNot(same(secondPlaceholderImage)));
expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
await tester.pumpWidget(FadeInImage(
placeholder: secondPlaceholderProvider,
image: imageProvider,
fadeOutDuration: const Duration(milliseconds: 50),
fadeInDuration: const Duration(milliseconds: 50),
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
secondPlaceholderProvider.complete();
await tester.pump();
expect(displayedImage().image, isNot(same(placeholderImage))); // placeholder replaced
expect(displayedImage().image, same(secondPlaceholderImage));
expect(findFadeInImage(tester).placeholder.rawImage.image, same(replacementImage));
expect(findFadeInImage(tester).state, same(state));
});
group('semanticLabel', () {
testWidgets('re-fades in the image when the target image is updated', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
final TestImageProvider secondImageProvider = TestImageProvider(replacementImage);
const String placeholderSemanticText = 'Test placeholder semantic label';
const String imageSemanticText = 'Test image semantic label';
const Duration animationDuration = Duration(milliseconds: 50);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
testWidgets('assigned correctly according to placeholder or image', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
final State state = findFadeInImage(tester).state;
placeholderProvider.complete();
imageProvider.complete();
await tester.pump();
await tester.pump(animationDuration * 2);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: secondImageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
secondImageProvider.complete();
await tester.pump();
expect(findFadeInImage(tester).target.rawImage.image, same(replacementImage));
expect(findFadeInImage(tester).state, same(state));
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(1));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
await tester.pump(animationDuration);
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
await tester.pump(animationDuration);
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
});
testWidgets('doesn\'t interrupt in-progress animation when animation values are updated', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: Image and Placeholder semantic texts are provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
imageSemanticLabel: imageSemanticText,
placeholderSemanticLabel: placeholderSemanticText,
excludeFromSemantics: true,
));
placeholderProvider.complete(); // load the placeholder
final State state = findFadeInImage(tester).state;
placeholderProvider.complete();
imageProvider.complete();
await tester.pump();
expect(displayedWidget().properties.label, same(placeholderSemanticText));
await tester.pump(animationDuration);
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(imageSemanticText));
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration * 2,
fadeInDuration: animationDuration * 2,
excludeFromSemantics: true,
));
expect(findFadeInImage(tester).state, same(state));
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
await tester.pump(animationDuration);
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
});
testWidgets('assigned correctly with only one semantics text', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
group('semantics', () {
testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: Placeholder semantic text provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
expect(find.byType(Semantics), findsOneWidget);
});
testWidgets('is excluded if excludeFromSemantics is true', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
excludeFromSemantics: true,
));
expect(find.byType(Semantics), findsNothing);
});
group('label', () {
const String imageSemanticText = 'Test image semantic label';
testWidgets('defaults to image label if placeholder label is unspecified', (WidgetTester tester) async {
Semantics semanticsWidget() => tester.widget(find.byType(Semantics));
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
placeholderSemanticLabel: placeholderSemanticText,
imageSemanticLabel: imageSemanticText,
));
placeholderProvider.complete(); // load the placeholder
placeholderProvider.complete();
await tester.pump();
expect(displayedWidget().properties.label, same(placeholderSemanticText));
expect(semanticsWidget().properties.label, imageSemanticText);
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(''));
imageProvider.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 51));
expect(semanticsWidget().properties.label, imageSemanticText);
});
testWidgets('assigned correctly without any semantics text', (WidgetTester tester) async {
// The semantics widget that is created
Semantics displayedWidget() => tester.widget(find.byType(Semantics));
// The placeholder is expected to be already loaded
testWidgets('is empty without any specified semantics labels', (WidgetTester tester) async {
Semantics semanticsWidget() => tester.widget(find.byType(Semantics));
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
// The image which takes long to load
final TestImageProvider imageProvider = TestImageProvider(targetImage);
// Test case: No semantic text provided.
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
......@@ -193,15 +328,15 @@ Future<void> main() async {
fadeInDuration: animationDuration,
));
placeholderProvider.complete(); // load the placeholder
placeholderProvider.complete();
await tester.pump();
expect(displayedWidget().properties.label, same(''));
expect(semanticsWidget().properties.label, isEmpty);
imageProvider.complete(); // load the image
for (int i = 0; i < 10; i += 1) {
await tester.pump(const Duration(milliseconds: 10)); // do the fadeout and fade in
}
expect(displayedWidget().properties.label, same(''));
imageProvider.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 51));
expect(semanticsWidget().properties.label, isEmpty);
});
});
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment