1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6
7import 'basic.dart';
8import 'framework.dart';
9import 'image.dart';
10import 'implicit_animations.dart';
11
12// Examples can assume:
13// late Uint8List bytes;
14
15/// An image that shows a [placeholder] image while the target [image] is
16/// loading, then fades in the new image when it loads.
17///
18/// Use this class to display long-loading images, such as [NetworkImage.new],
19/// so that the image appears on screen with a graceful animation rather than
20/// abruptly popping onto the screen.
21///
22/// {@youtube 560 315 https://www.youtube.com/watch?v=pK738Pg9cxc}
23///
24/// If the [image] emits an [ImageInfo] synchronously, such as when the image
25/// has been loaded and cached, the [image] is displayed immediately, and the
26/// [placeholder] is never displayed.
27///
28/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
29/// animation of the [placeholder].
30///
31/// The [fadeInDuration] and [fadeInCurve] properties control the fade-in
32/// animation of the target [image].
33///
34/// Prefer a [placeholder] that's already cached so that it is displayed
35/// immediately. This prevents it from popping onto the screen.
36///
37/// When [image] changes, it is resolved to a new [ImageStream]. If the new
38/// [ImageStream.key] is different, this widget subscribes to the new stream and
39/// replaces the displayed image with images emitted by the new stream.
40///
41/// When [placeholder] changes and the [image] has not yet emitted an
42/// [ImageInfo], then [placeholder] is resolved to a new [ImageStream]. If the
43/// new [ImageStream.key] is different, this widget subscribes to the new stream
44/// and replaces the displayed image to images emitted by the new stream.
45///
46/// When either [placeholder] or [image] changes, this widget continues showing
47/// the previously loaded image (if any) until the new image provider provides a
48/// different image. This is known as "gapless playback" (see also
49/// [Image.gaplessPlayback]).
50///
51/// {@tool snippet}
52///
53/// ```dart
54/// FadeInImage(
55/// // here `bytes` is a Uint8List containing the bytes for the in-memory image
56/// placeholder: MemoryImage(bytes),
57/// image: const NetworkImage('https://backend.example.com/image.png'),
58/// )
59/// ```
60/// {@end-tool}
61class FadeInImage extends StatefulWidget {
62 /// Creates a widget that displays a [placeholder] while an [image] is loading,
63 /// then fades-out the placeholder and fades-in the image.
64 ///
65 /// The [placeholder] and [image] may be composed in a [ResizeImage] to provide
66 /// a custom decode/cache size.
67 ///
68 /// The [placeholder] and [image] may have their own BoxFit settings via [fit]
69 /// and [placeholderFit].
70 ///
71 /// The [placeholder] and [image] may have their own FilterQuality settings via [filterQuality]
72 /// and [placeholderFilterQuality].
73 ///
74 /// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
75 const FadeInImage({
76 super.key,
77 required this.placeholder,
78 this.placeholderErrorBuilder,
79 required this.image,
80 this.imageErrorBuilder,
81 this.excludeFromSemantics = false,
82 this.imageSemanticLabel,
83 this.fadeOutDuration = const Duration(milliseconds: 300),
84 this.fadeOutCurve = Curves.easeOut,
85 this.fadeInDuration = const Duration(milliseconds: 700),
86 this.fadeInCurve = Curves.easeIn,
87 this.color,
88 this.colorBlendMode,
89 this.placeholderColor,
90 this.placeholderColorBlendMode,
91 this.width,
92 this.height,
93 this.fit,
94 this.placeholderFit,
95 this.filterQuality = FilterQuality.medium,
96 this.placeholderFilterQuality,
97 this.alignment = Alignment.center,
98 this.repeat = ImageRepeat.noRepeat,
99 this.matchTextDirection = false,
100 });
101
102 /// Creates a widget that uses a placeholder image stored in memory while
103 /// loading the final image from the network.
104 ///
105 /// The `placeholder` argument contains the bytes of the in-memory image.
106 ///
107 /// The `image` argument is the URL of the final image.
108 ///
109 /// The `placeholderScale` and `imageScale` arguments are passed to their
110 /// respective [ImageProvider]s (see also [ImageInfo.scale]).
111 ///
112 /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
113 /// or [imageCacheHeight] are provided, it indicates to the
114 /// engine that the respective image should be decoded at the specified size.
115 /// The image will be rendered to the constraints of the layout or [width]
116 /// and [height] regardless of these parameters. These parameters are primarily
117 /// intended to reduce the memory usage of [ImageCache].
118 ///
119 /// The [placeholder], [image], [placeholderScale], [imageScale],
120 /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
121 /// [alignment], [repeat], and [matchTextDirection] arguments must not be
122 /// null.
123 ///
124 /// See also:
125 ///
126 /// * [Image.memory], which has more details about loading images from
127 /// memory.
128 /// * [Image.network], which has more details about loading images from
129 /// the network.
130 FadeInImage.memoryNetwork({
131 super.key,
132 required Uint8List placeholder,
133 this.placeholderErrorBuilder,
134 required String image,
135 this.imageErrorBuilder,
136 double placeholderScale = 1.0,
137 double imageScale = 1.0,
138 this.excludeFromSemantics = false,
139 this.imageSemanticLabel,
140 this.fadeOutDuration = const Duration(milliseconds: 300),
141 this.fadeOutCurve = Curves.easeOut,
142 this.fadeInDuration = const Duration(milliseconds: 700),
143 this.fadeInCurve = Curves.easeIn,
144 this.width,
145 this.height,
146 this.fit,
147 this.color,
148 this.colorBlendMode,
149 this.placeholderColor,
150 this.placeholderColorBlendMode,
151 this.placeholderFit,
152 this.filterQuality = FilterQuality.medium,
153 this.placeholderFilterQuality,
154 this.alignment = Alignment.center,
155 this.repeat = ImageRepeat.noRepeat,
156 this.matchTextDirection = false,
157 int? placeholderCacheWidth,
158 int? placeholderCacheHeight,
159 int? imageCacheWidth,
160 int? imageCacheHeight,
161 }) : placeholder = ResizeImage.resizeIfNeeded(
162 placeholderCacheWidth,
163 placeholderCacheHeight,
164 MemoryImage(placeholder, scale: placeholderScale),
165 ),
166 image = ResizeImage.resizeIfNeeded(
167 imageCacheWidth,
168 imageCacheHeight,
169 NetworkImage(image, scale: imageScale),
170 );
171
172 /// Creates a widget that uses a placeholder image stored in an asset bundle
173 /// while loading the final image from the network.
174 ///
175 /// The `placeholder` argument is the key of the image in the asset bundle.
176 ///
177 /// The `image` argument is the URL of the final image.
178 ///
179 /// The `placeholderScale` and `imageScale` arguments are passed to their
180 /// respective [ImageProvider]s (see also [ImageInfo.scale]).
181 ///
182 /// If `placeholderScale` is omitted or is null, pixel-density-aware asset
183 /// resolution will be attempted for the [placeholder] image. Otherwise, the
184 /// exact asset specified will be used.
185 ///
186 /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
187 /// or [imageCacheHeight] are provided, it indicates to the
188 /// engine that the respective image should be decoded at the specified size.
189 /// The image will be rendered to the constraints of the layout or [width]
190 /// and [height] regardless of these parameters. These parameters are primarily
191 /// intended to reduce the memory usage of [ImageCache].
192 ///
193 /// See also:
194 ///
195 /// * [Image.asset], which has more details about loading images from
196 /// asset bundles.
197 /// * [Image.network], which has more details about loading images from
198 /// the network.
199 FadeInImage.assetNetwork({
200 super.key,
201 required String placeholder,
202 this.placeholderErrorBuilder,
203 required String image,
204 this.imageErrorBuilder,
205 AssetBundle? bundle,
206 double? placeholderScale,
207 double imageScale = 1.0,
208 this.excludeFromSemantics = false,
209 this.imageSemanticLabel,
210 this.fadeOutDuration = const Duration(milliseconds: 300),
211 this.fadeOutCurve = Curves.easeOut,
212 this.fadeInDuration = const Duration(milliseconds: 700),
213 this.fadeInCurve = Curves.easeIn,
214 this.width,
215 this.height,
216 this.fit,
217 this.color,
218 this.colorBlendMode,
219 this.placeholderColor,
220 this.placeholderColorBlendMode,
221 this.placeholderFit,
222 this.filterQuality = FilterQuality.medium,
223 this.placeholderFilterQuality,
224 this.alignment = Alignment.center,
225 this.repeat = ImageRepeat.noRepeat,
226 this.matchTextDirection = false,
227 int? placeholderCacheWidth,
228 int? placeholderCacheHeight,
229 int? imageCacheWidth,
230 int? imageCacheHeight,
231 }) : placeholder =
232 placeholderScale != null
233 ? ResizeImage.resizeIfNeeded(
234 placeholderCacheWidth,
235 placeholderCacheHeight,
236 ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale),
237 )
238 : ResizeImage.resizeIfNeeded(
239 placeholderCacheWidth,
240 placeholderCacheHeight,
241 AssetImage(placeholder, bundle: bundle),
242 ),
243 image = ResizeImage.resizeIfNeeded(
244 imageCacheWidth,
245 imageCacheHeight,
246 NetworkImage(image, scale: imageScale),
247 );
248
249 /// Image displayed while the target [image] is loading.
250 final ImageProvider placeholder;
251
252 /// A builder function that is called if an error occurs during placeholder
253 /// image loading.
254 ///
255 /// If this builder is not provided, any exceptions will be reported to
256 /// [FlutterError.onError]. If it is provided, the caller should either handle
257 /// the exception by providing a replacement widget, or rethrow the exception.
258 final ImageErrorWidgetBuilder? placeholderErrorBuilder;
259
260 /// The target image that is displayed once it has loaded.
261 final ImageProvider image;
262
263 /// A builder function that is called if an error occurs during image loading.
264 ///
265 /// If this builder is not provided, any exceptions will be reported to
266 /// [FlutterError.onError]. If it is provided, the caller should either handle
267 /// the exception by providing a replacement widget, or rethrow the exception.
268 final ImageErrorWidgetBuilder? imageErrorBuilder;
269
270 /// The duration of the fade-out animation for the [placeholder].
271 final Duration fadeOutDuration;
272
273 /// The curve of the fade-out animation for the [placeholder].
274 final Curve fadeOutCurve;
275
276 /// The duration of the fade-in animation for the [image].
277 final Duration fadeInDuration;
278
279 /// The curve of the fade-in animation for the [image].
280 final Curve fadeInCurve;
281
282 /// If non-null, require the image to have this width.
283 ///
284 /// If null, the image will pick a size that best preserves its intrinsic
285 /// aspect ratio. This may result in a sudden change if the size of the
286 /// placeholder image does not match that of the target image. The size is
287 /// also affected by the scale factor.
288 final double? width;
289
290 /// If non-null, this color is blended with each image pixel using [colorBlendMode].
291 ///
292 /// Color applies to the [image].
293 ///
294 /// See Also:
295 ///
296 /// * [placeholderColor], the color which applies to the [placeholder].
297 final Color? color;
298
299 /// Used to combine [color] with this [image].
300 ///
301 /// The default is [BlendMode.srcIn]. In terms of the blend mode, [color] is
302 /// the source and this image is the destination.
303 ///
304 /// See also:
305 ///
306 /// * [BlendMode], which includes an illustration of the effect of each blend mode.
307 /// * [placeholderColorBlendMode], the color blend mode which applies to the [placeholder].
308 final BlendMode? colorBlendMode;
309
310 /// If non-null, this color is blended with each placeholder image pixel using [placeholderColorBlendMode].
311 ///
312 /// Color applies to the [placeholder].
313 ///
314 /// See Also:
315 ///
316 /// * [color], the color which applies to the [image].
317 final Color? placeholderColor;
318
319 /// Used to combine [placeholderColor] with the [placeholder] image.
320 ///
321 /// The default is [BlendMode.srcIn]. In terms of the blend mode, [placeholderColor] is
322 /// the source and this placeholder is the destination.
323 ///
324 /// See also:
325 ///
326 /// * [BlendMode], which includes an illustration of the effect of each blend mode.
327 /// * [colorBlendMode], the color blend mode which applies to the [image].
328 final BlendMode? placeholderColorBlendMode;
329
330 /// If non-null, require the image to have this height.
331 ///
332 /// If null, the image will pick a size that best preserves its intrinsic
333 /// aspect ratio. This may result in a sudden change if the size of the
334 /// placeholder image does not match that of the target image. The size is
335 /// also affected by the scale factor.
336 final double? height;
337
338 /// How to inscribe the image into the space allocated during layout.
339 ///
340 /// The default varies based on the other fields. See the discussion at
341 /// [paintImage].
342 final BoxFit? fit;
343
344 /// How to inscribe the placeholder image into the space allocated during layout.
345 ///
346 /// If not value set, it will fallback to [fit].
347 final BoxFit? placeholderFit;
348
349 /// The rendering quality of the image.
350 ///
351 /// {@macro flutter.widgets.image.filterQuality}
352 final FilterQuality filterQuality;
353
354 /// The rendering quality of the placeholder image.
355 ///
356 /// {@macro flutter.widgets.image.filterQuality}
357 final FilterQuality? placeholderFilterQuality;
358
359 /// How to align the image within its bounds.
360 ///
361 /// The alignment aligns the given position in the image to the given position
362 /// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
363 /// -1.0) aligns the image to the top-left corner of its layout bounds, while an
364 /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
365 /// image with the bottom right corner of its layout bounds. Similarly, an
366 /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
367 /// middle of the bottom edge of its layout bounds.
368 ///
369 /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
370 /// [AlignmentDirectional]), then an ambient [Directionality] widget
371 /// must be in scope.
372 ///
373 /// Defaults to [Alignment.center].
374 ///
375 /// See also:
376 ///
377 /// * [Alignment], a class with convenient constants typically used to
378 /// specify an [AlignmentGeometry].
379 /// * [AlignmentDirectional], like [Alignment] for specifying alignments
380 /// relative to text direction.
381 final AlignmentGeometry alignment;
382
383 /// How to paint any portions of the layout bounds not covered by the image.
384 final ImageRepeat repeat;
385
386 /// Whether to paint the image in the direction of the [TextDirection].
387 ///
388 /// If this is true, then in [TextDirection.ltr] contexts, the image will be
389 /// drawn with its origin in the top left (the "normal" painting direction for
390 /// images); and in [TextDirection.rtl] contexts, the image will be drawn with
391 /// a scaling factor of -1 in the horizontal direction so that the origin is
392 /// in the top right.
393 ///
394 /// This is occasionally used with images in right-to-left environments, for
395 /// images that were designed for left-to-right locales. Be careful, when
396 /// using this, to not flip images with integral shadows, text, or other
397 /// effects that will look incorrect when flipped.
398 ///
399 /// If this is true, there must be an ambient [Directionality] widget in
400 /// scope.
401 final bool matchTextDirection;
402
403 /// Whether to exclude this image from semantics.
404 ///
405 /// This is useful for images which do not contribute meaningful information
406 /// to an application.
407 final bool excludeFromSemantics;
408
409 /// A semantic description of the [image].
410 ///
411 /// Used to provide a description of the [image] to TalkBack on Android, and
412 /// VoiceOver on iOS.
413 ///
414 /// This description will be used both while the [placeholder] is shown and
415 /// once the image has loaded.
416 final String? imageSemanticLabel;
417
418 @override
419 State<FadeInImage> createState() => _FadeInImageState();
420}
421
422class _FadeInImageState extends State<FadeInImage> {
423 static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);
424 bool targetLoaded = false;
425
426 // These ProxyAnimations are changed to the fade in animation by
427 // [_AnimatedFadeOutFadeInState]. Otherwise these animations are reset to
428 // their defaults by [_resetAnimations].
429 final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation);
430 final ProxyAnimation _placeholderAnimation = ProxyAnimation(_kOpaqueAnimation);
431
432 Image _image({
433 required ImageProvider image,
434 ImageErrorWidgetBuilder? errorBuilder,
435 ImageFrameBuilder? frameBuilder,
436 BoxFit? fit,
437 Color? color,
438 BlendMode? colorBlendMode,
439 required FilterQuality filterQuality,
440 required Animation<double> opacity,
441 }) {
442 return Image(
443 image: image,
444 errorBuilder: errorBuilder,
445 frameBuilder: frameBuilder,
446 opacity: opacity,
447 width: widget.width,
448 height: widget.height,
449 fit: fit,
450 color: color,
451 colorBlendMode: colorBlendMode,
452 filterQuality: filterQuality,
453 alignment: widget.alignment,
454 repeat: widget.repeat,
455 matchTextDirection: widget.matchTextDirection,
456 gaplessPlayback: true,
457 excludeFromSemantics: true,
458 );
459 }
460
461 @override
462 Widget build(BuildContext context) {
463 Widget result = _image(
464 image: widget.image,
465 errorBuilder: widget.imageErrorBuilder,
466 opacity: _imageAnimation,
467 fit: widget.fit,
468 color: widget.color,
469 colorBlendMode: widget.colorBlendMode,
470 filterQuality: widget.filterQuality,
471 frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
472 if (wasSynchronouslyLoaded || frame != null) {
473 targetLoaded = true;
474 }
475 return _AnimatedFadeOutFadeIn(
476 target: child,
477 targetProxyAnimation: _imageAnimation,
478 placeholder: _image(
479 image: widget.placeholder,
480 errorBuilder: widget.placeholderErrorBuilder,
481 opacity: _placeholderAnimation,
482 color: widget.placeholderColor,
483 colorBlendMode: widget.placeholderColorBlendMode,
484 fit: widget.placeholderFit ?? widget.fit,
485 filterQuality: widget.placeholderFilterQuality ?? widget.filterQuality,
486 ),
487 placeholderProxyAnimation: _placeholderAnimation,
488 isTargetLoaded: targetLoaded,
489 wasSynchronouslyLoaded: wasSynchronouslyLoaded,
490 fadeInDuration: widget.fadeInDuration,
491 fadeOutDuration: widget.fadeOutDuration,
492 fadeInCurve: widget.fadeInCurve,
493 fadeOutCurve: widget.fadeOutCurve,
494 );
495 },
496 );
497
498 if (!widget.excludeFromSemantics) {
499 result = Semantics(
500 container: widget.imageSemanticLabel != null,
501 image: true,
502 label: widget.imageSemanticLabel ?? '',
503 child: result,
504 );
505 }
506
507 return result;
508 }
509}
510
511class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
512 const _AnimatedFadeOutFadeIn({
513 required this.target,
514 required this.targetProxyAnimation,
515 required this.placeholder,
516 required this.placeholderProxyAnimation,
517 required this.isTargetLoaded,
518 required this.fadeOutDuration,
519 required this.fadeOutCurve,
520 required this.fadeInDuration,
521 required this.fadeInCurve,
522 required this.wasSynchronouslyLoaded,
523 }) : assert(!wasSynchronouslyLoaded || isTargetLoaded),
524 super(duration: fadeInDuration + fadeOutDuration);
525
526 final Widget target;
527 final ProxyAnimation targetProxyAnimation;
528 final Widget placeholder;
529 final ProxyAnimation placeholderProxyAnimation;
530 final bool isTargetLoaded;
531 final Duration fadeInDuration;
532 final Duration fadeOutDuration;
533 final Curve fadeInCurve;
534 final Curve fadeOutCurve;
535 final bool wasSynchronouslyLoaded;
536
537 @override
538 _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
539}
540
541class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
542 Tween<double>? _targetOpacity;
543 Tween<double>? _placeholderOpacity;
544 Animation<double>? _targetOpacityAnimation;
545 Animation<double>? _placeholderOpacityAnimation;
546
547 @override
548 void forEachTween(TweenVisitor<dynamic> visitor) {
549 _targetOpacity =
550 visitor(
551 _targetOpacity,
552 widget.isTargetLoaded ? 1.0 : 0.0,
553 (dynamic value) => Tween<double>(begin: value as double),
554 )
555 as Tween<double>?;
556 _placeholderOpacity =
557 visitor(
558 _placeholderOpacity,
559 widget.isTargetLoaded ? 0.0 : 1.0,
560 (dynamic value) => Tween<double>(begin: value as double),
561 )
562 as Tween<double>?;
563 }
564
565 @override
566 void didUpdateTweens() {
567 if (widget.wasSynchronouslyLoaded) {
568 // Opacity animations should not be reset if image was synchronously loaded.
569 return;
570 }
571
572 _placeholderOpacityAnimation = animation.drive(
573 TweenSequence<double>(<TweenSequenceItem<double>>[
574 TweenSequenceItem<double>(
575 tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
576 weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
577 ),
578 TweenSequenceItem<double>(
579 tween: ConstantTween<double>(0),
580 weight: widget.fadeInDuration.inMilliseconds.toDouble(),
581 ),
582 ]),
583 )..addStatusListener((AnimationStatus status) {
584 if (_placeholderOpacityAnimation!.isCompleted) {
585 // Need to rebuild to remove placeholder now that it is invisible.
586 setState(() {});
587 }
588 });
589
590 _targetOpacityAnimation = animation.drive(
591 TweenSequence<double>(<TweenSequenceItem<double>>[
592 TweenSequenceItem<double>(
593 tween: ConstantTween<double>(0),
594 weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
595 ),
596 TweenSequenceItem<double>(
597 tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
598 weight: widget.fadeInDuration.inMilliseconds.toDouble(),
599 ),
600 ]),
601 );
602
603 widget.targetProxyAnimation.parent = _targetOpacityAnimation;
604 widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
605 }
606
607 @override
608 Widget build(BuildContext context) {
609 if (widget.wasSynchronouslyLoaded || (_placeholderOpacityAnimation?.isCompleted ?? true)) {
610 return widget.target;
611 }
612
613 return Stack(
614 fit: StackFit.passthrough,
615 alignment: AlignmentDirectional.center,
616 // Text direction is irrelevant here since we're using center alignment,
617 // but it allows the Stack to avoid a call to Directionality.of()
618 textDirection: TextDirection.ltr,
619 children: <Widget>[widget.target, widget.placeholder],
620 );
621 }
622
623 @override
624 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
625 super.debugFillProperties(properties);
626 properties.add(
627 DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation),
628 );
629 properties.add(
630 DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation),
631 );
632 }
633}
634

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com