FadeInImage: shows a placeholder while loading then fades in (#11371)
* FadeInImage: shows a placeholder while loading then fades in * fix dartdoc quotes * license headers; imports * use ImageProvider; docs; constructors * _resolveImage when placeholder changes * address comments * docs re ImageProvider changes; unsubscribe from placeholder * rebase * address comments
This commit is contained in:
parent
c72381aa2d
commit
89d06450e1
@ -14,7 +14,7 @@ export 'package:flutter/painting.dart' show
|
||||
/// An image in the render tree.
|
||||
///
|
||||
/// The render image attempts to find a size for itself that fits in the given
|
||||
/// constraints and preserves the image's intrinisc aspect ratio.
|
||||
/// constraints and preserves the image's intrinsic aspect ratio.
|
||||
///
|
||||
/// The image is painted using [paintImage], which describes the meanings of the
|
||||
/// various fields on this class in more detail.
|
||||
|
470
packages/flutter/lib/src/widgets/fade_in_image.dart
Normal file
470
packages/flutter/lib/src/widgets/fade_in_image.dart
Normal file
@ -0,0 +1,470 @@
|
||||
// Copyright 2017 The Chromium 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 'ticker_provider.dart';
|
||||
|
||||
/// 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 pops onto the screen.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// [fadeOutDuration] and [fadeOutCurve] control the fade-out animation of the
|
||||
/// placeholder.
|
||||
///
|
||||
/// [fadeInDuration] and [fadeInCurve] 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.
|
||||
///
|
||||
/// 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]).
|
||||
///
|
||||
/// ## Sample code:
|
||||
///
|
||||
/// ```dart
|
||||
/// return new FadeInImage(
|
||||
/// // here `bytes` is a Uint8List containing the bytes for the in-memory image
|
||||
/// placeholder: new MemoryImage(bytes),
|
||||
/// image: new NetworkImage('https://backend.example.com/image.png'),
|
||||
/// );
|
||||
/// ```
|
||||
class FadeInImage extends StatefulWidget {
|
||||
/// Creates a widget that displays a [placeholder] while an [image] is loading
|
||||
/// then cross-fades to display the [image].
|
||||
///
|
||||
/// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
|
||||
/// [fadeInDuration], [fadeInCurve] and [repeat] arguments must not be null.
|
||||
const FadeInImage({
|
||||
Key key,
|
||||
@required this.placeholder,
|
||||
@required this.image,
|
||||
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,
|
||||
this.repeat: ImageRepeat.noRepeat,
|
||||
}) : assert(placeholder != null),
|
||||
assert(image != null),
|
||||
assert(fadeOutDuration != null),
|
||||
assert(fadeOutCurve != null),
|
||||
assert(fadeInDuration != null),
|
||||
assert(fadeInCurve != null),
|
||||
assert(repeat != null),
|
||||
super(key: key);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// [image] is the URL of the final image.
|
||||
///
|
||||
/// [placeholderScale] and [imageScale] are passed to their respective
|
||||
/// [ImageProvider]s (see also [ImageInfo.scale]).
|
||||
///
|
||||
/// The [placeholder], [image], [placeholderScale], [imageScale],
|
||||
/// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve] and
|
||||
/// [repeat] 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,
|
||||
@required String image,
|
||||
double placeholderScale: 1.0,
|
||||
double imageScale: 1.0,
|
||||
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,
|
||||
this.repeat: ImageRepeat.noRepeat,
|
||||
}) : 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(repeat != null),
|
||||
placeholder = new MemoryImage(placeholder, scale: placeholderScale),
|
||||
image = new 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.
|
||||
///
|
||||
/// [placeholder] is the key of the image in the asset bundle.
|
||||
///
|
||||
/// [image] is the URL of the final image.
|
||||
///
|
||||
/// [placeholderScale] and [imageScale] are passed to their respective
|
||||
/// [ImageProvider]s (see also [ImageInfo.scale]).
|
||||
///
|
||||
/// If [placeholderScale] is omitted or is null, the pixel-density-aware asset
|
||||
/// resolution will be attempted for the [placeholder] image. Otherwise, the
|
||||
/// exact asset specified will be used.
|
||||
///
|
||||
/// The [placeholder], [image], [imageScale], [fadeOutDuration],
|
||||
/// [fadeOutCurve], [fadeInDuration], [fadeInCurve] and [repeat] 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,
|
||||
@required String image,
|
||||
AssetBundle bundle,
|
||||
double placeholderScale,
|
||||
double imageScale: 1.0,
|
||||
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,
|
||||
this.repeat: ImageRepeat.noRepeat,
|
||||
}) : assert(placeholder != null),
|
||||
assert(image != null),
|
||||
placeholder = placeholderScale != null
|
||||
? new ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale)
|
||||
: new AssetImage(placeholder, bundle: bundle),
|
||||
assert(imageScale != null),
|
||||
assert(fadeOutDuration != null),
|
||||
assert(fadeOutCurve != null),
|
||||
assert(fadeInDuration != null),
|
||||
assert(fadeInCurve != null),
|
||||
assert(repeat != null),
|
||||
image = new NetworkImage(image, scale: imageScale),
|
||||
super(key: key);
|
||||
|
||||
/// Image displayed while the target [image] is loading.
|
||||
final ImageProvider placeholder;
|
||||
|
||||
/// The target image that is displayed.
|
||||
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.
|
||||
///
|
||||
/// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its
|
||||
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle
|
||||
/// of the right edge of its layout bounds.
|
||||
final FractionalOffset alignment;
|
||||
|
||||
/// How to paint any portions of the layout bounds not covered by the image.
|
||||
final ImageRepeat repeat;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => new _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 void _ImageProviderResolverListener();
|
||||
|
||||
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 ? new Size(widget.width, widget.height) : null
|
||||
));
|
||||
assert(_imageStream != null);
|
||||
|
||||
if (_imageStream.key != oldImageStream?.key) {
|
||||
oldImageStream?.removeListener(_handleImageChanged);
|
||||
_imageStream.addListener(_handleImageChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
|
||||
_imageInfo = imageInfo;
|
||||
listener();
|
||||
}
|
||||
|
||||
void stopListening() {
|
||||
_imageStream?.removeListener(_handleImageChanged);
|
||||
}
|
||||
}
|
||||
|
||||
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 = new _ImageProviderResolver(state: this, listener: _updatePhase);
|
||||
_placeholderResolver = new _ImageProviderResolver(state: this, listener: () {
|
||||
setState(() {
|
||||
// Trigger rebuild to display the placeholder image
|
||||
});
|
||||
});
|
||||
_controller = new AnimationController(
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
);
|
||||
_controller.addListener(() {
|
||||
setState(() {
|
||||
// Trigger rebuild to update opacity value.
|
||||
});
|
||||
});
|
||||
_controller.addStatusListener((AnimationStatus status) {
|
||||
_updatePhase();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_resolveImage();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FadeInImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.image != oldWidget.image || widget.placeholder != widget.placeholder)
|
||||
_resolveImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
_resolveImage(); // in case the image cache was flushed
|
||||
super.reassemble();
|
||||
}
|
||||
|
||||
void _resolveImage() {
|
||||
_imageResolver.resolve(widget.image);
|
||||
|
||||
// No need to resolve the placeholder if we are past the placeholder stage.
|
||||
if (_isShowingPlaceholder)
|
||||
_placeholderResolver.resolve(widget.placeholder);
|
||||
|
||||
if (_phase == FadeInImagePhase.start)
|
||||
_updatePhase();
|
||||
}
|
||||
|
||||
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 = new CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: widget.fadeOutCurve,
|
||||
);
|
||||
_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 = new CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: widget.fadeInCurve,
|
||||
);
|
||||
_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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(_phase != FadeInImagePhase.start);
|
||||
final ImageInfo imageInfo = _imageInfo;
|
||||
return new RawImage(
|
||||
image: imageInfo?.image,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
scale: imageInfo?.scale ?? 1.0,
|
||||
color: new Color.fromRGBO(255, 255, 255, _animation?.value ?? 1.0),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
fit: widget.fit,
|
||||
alignment: widget.alignment,
|
||||
repeat: widget.repeat,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(List<DiagnosticsNode> description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(new EnumProperty<FadeInImagePhase>('phase', _phase));
|
||||
description.add(new DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
||||
description.add(new DiagnosticsProperty<ImageStream>('image stream', _imageResolver._imageStream));
|
||||
description.add(new DiagnosticsProperty<ImageStream>('placeholder stream', _placeholderResolver._imageStream));
|
||||
}
|
||||
}
|
@ -162,10 +162,11 @@ class Image extends StatefulWidget {
|
||||
/// If the `bundle` argument is omitted or null, then the
|
||||
/// [DefaultAssetBundle] will be used.
|
||||
///
|
||||
/// By default, the exact asset specified will be used. In addition:
|
||||
/// By default, the pixel-density-aware asset resolution will be attempted. In
|
||||
/// addition:
|
||||
///
|
||||
/// * If the `scale` argument is omitted or null, then pixel-density-aware
|
||||
/// asset resolution will be attempted.
|
||||
/// * If the `scale` argument is provided and is not null, then the exact
|
||||
/// asset specified will be used.
|
||||
//
|
||||
// TODO(ianh): Implement the following (see ../services/image_resolution.dart):
|
||||
// ///
|
||||
|
@ -29,6 +29,7 @@ export 'src/widgets/debug.dart';
|
||||
export 'src/widgets/dismissible.dart';
|
||||
export 'src/widgets/drag_target.dart';
|
||||
export 'src/widgets/editable_text.dart';
|
||||
export 'src/widgets/fade_in_image.dart';
|
||||
export 'src/widgets/focus_manager.dart';
|
||||
export 'src/widgets/focus_scope.dart';
|
||||
export 'src/widgets/form.dart';
|
||||
|
51
packages/flutter/test/services/image_test_utils.dart
Normal file
51
packages/flutter/test/services/image_test_utils.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2017 The Chromium 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:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'image_data.dart';
|
||||
|
||||
class TestImageProvider extends ImageProvider<TestImageProvider> {
|
||||
TestImageProvider(this.testImage);
|
||||
|
||||
final ui.Image testImage;
|
||||
|
||||
final Completer<ImageInfo> _completer = new Completer<ImageInfo>.sync();
|
||||
ImageConfiguration configuration;
|
||||
|
||||
@override
|
||||
Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return new SynchronousFuture<TestImageProvider>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStream resolve(ImageConfiguration config) {
|
||||
configuration = config;
|
||||
return super.resolve(configuration);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(TestImageProvider key) =>
|
||||
new OneFrameImageStreamCompleter(_completer.future);
|
||||
|
||||
ImageInfo complete() {
|
||||
final ImageInfo imageInfo = new ImageInfo(image: testImage);
|
||||
_completer.complete(imageInfo);
|
||||
return imageInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '${describeIdentity(this)}()';
|
||||
}
|
||||
|
||||
Future<ui.Image> createTestImage() {
|
||||
final Completer<ui.Image> uiImage = new Completer<ui.Image>();
|
||||
ui.decodeImageFromList(new Uint8List.fromList(kTransparentImage), uiImage.complete);
|
||||
return uiImage.future;
|
||||
}
|
84
packages/flutter/test/widgets/fade_in_image_test.dart
Normal file
84
packages/flutter/test/widgets/fade_in_image_test.dart
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2017 The Chromium 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:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../services/image_test_utils.dart';
|
||||
|
||||
Future<Null> main() async {
|
||||
// These must run outside test zone to complete
|
||||
final ui.Image targetImage = await createTestImage();
|
||||
final ui.Image placeholderImage = 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
|
||||
final TestImageProvider placeholderProvider = new TestImageProvider(placeholderImage);
|
||||
|
||||
// Test case: long loading image
|
||||
final TestImageProvider imageProvider = new TestImageProvider(targetImage);
|
||||
|
||||
await tester.pumpWidget(new FadeInImage(
|
||||
placeholder: placeholderProvider,
|
||||
image: imageProvider,
|
||||
fadeOutDuration: const Duration(milliseconds: 50),
|
||||
fadeInDuration: const Duration(milliseconds: 50),
|
||||
));
|
||||
|
||||
expect(displayedImage().image, null); // image providers haven't completed yet
|
||||
placeholderProvider.complete();
|
||||
await tester.pump();
|
||||
|
||||
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));
|
||||
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));
|
||||
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(new 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(new Container()); // clear widget tree to prevent state reuse
|
||||
await tester.pumpWidget(new 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));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user