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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | |
7 | import 'basic.dart'; |
8 | import 'framework.dart'; |
9 | import 'image.dart'; |
10 | import '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} |
61 | class 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 = placeholderScale != null |
232 | ? ResizeImage.resizeIfNeeded( |
233 | placeholderCacheWidth, |
234 | placeholderCacheHeight, |
235 | ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale), |
236 | ) |
237 | : ResizeImage.resizeIfNeeded( |
238 | placeholderCacheWidth, |
239 | placeholderCacheHeight, |
240 | AssetImage(placeholder, bundle: bundle), |
241 | ), |
242 | image = ResizeImage.resizeIfNeeded( |
243 | imageCacheWidth, |
244 | imageCacheHeight, |
245 | NetworkImage(image, scale: imageScale), |
246 | ); |
247 | |
248 | /// Image displayed while the target [image] is loading. |
249 | final ImageProvider placeholder; |
250 | |
251 | /// A builder function that is called if an error occurs during placeholder |
252 | /// image loading. |
253 | /// |
254 | /// If this builder is not provided, any exceptions will be reported to |
255 | /// [FlutterError.onError]. If it is provided, the caller should either handle |
256 | /// the exception by providing a replacement widget, or rethrow the exception. |
257 | final ImageErrorWidgetBuilder? placeholderErrorBuilder; |
258 | |
259 | /// The target image that is displayed once it has loaded. |
260 | final ImageProvider image; |
261 | |
262 | /// A builder function that is called if an error occurs during image loading. |
263 | /// |
264 | /// If this builder is not provided, any exceptions will be reported to |
265 | /// [FlutterError.onError]. If it is provided, the caller should either handle |
266 | /// the exception by providing a replacement widget, or rethrow the exception. |
267 | final ImageErrorWidgetBuilder? imageErrorBuilder; |
268 | |
269 | /// The duration of the fade-out animation for the [placeholder]. |
270 | final Duration fadeOutDuration; |
271 | |
272 | /// The curve of the fade-out animation for the [placeholder]. |
273 | final Curve fadeOutCurve; |
274 | |
275 | /// The duration of the fade-in animation for the [image]. |
276 | final Duration fadeInDuration; |
277 | |
278 | /// The curve of the fade-in animation for the [image]. |
279 | final Curve fadeInCurve; |
280 | |
281 | /// If non-null, require the image to have this width. |
282 | /// |
283 | /// If null, the image will pick a size that best preserves its intrinsic |
284 | /// aspect ratio. This may result in a sudden change if the size of the |
285 | /// placeholder image does not match that of the target image. The size is |
286 | /// also affected by the scale factor. |
287 | final double? width; |
288 | |
289 | /// If non-null, this color is blended with each image pixel using [colorBlendMode]. |
290 | /// |
291 | /// Color applies to the [image]. |
292 | /// |
293 | /// See Also: |
294 | /// |
295 | /// * [placeholderColor], the color which applies to the [placeholder]. |
296 | final Color? color; |
297 | |
298 | /// Used to combine [color] with this [image]. |
299 | /// |
300 | /// The default is [BlendMode.srcIn]. In terms of the blend mode, [color] is |
301 | /// the source and this image is the destination. |
302 | /// |
303 | /// See also: |
304 | /// |
305 | /// * [BlendMode], which includes an illustration of the effect of each blend mode. |
306 | /// * [placeholderColorBlendMode], the color blend mode which applies to the [placeholder]. |
307 | final BlendMode? colorBlendMode; |
308 | |
309 | /// If non-null, this color is blended with each placeholder image pixel using [placeholderColorBlendMode]. |
310 | /// |
311 | /// Color applies to the [placeholder]. |
312 | /// |
313 | /// See Also: |
314 | /// |
315 | /// * [color], the color which applies to the [image]. |
316 | final Color? placeholderColor; |
317 | |
318 | /// Used to combine [placeholderColor] with the [placeholder] image. |
319 | /// |
320 | /// The default is [BlendMode.srcIn]. In terms of the blend mode, [placeholderColor] is |
321 | /// the source and this placeholder is the destination. |
322 | /// |
323 | /// See also: |
324 | /// |
325 | /// * [BlendMode], which includes an illustration of the effect of each blend mode. |
326 | /// * [colorBlendMode], the color blend mode which applies to the [image]. |
327 | final BlendMode? placeholderColorBlendMode; |
328 | |
329 | /// If non-null, require the image to have this height. |
330 | /// |
331 | /// If null, the image will pick a size that best preserves its intrinsic |
332 | /// aspect ratio. This may result in a sudden change if the size of the |
333 | /// placeholder image does not match that of the target image. The size is |
334 | /// also affected by the scale factor. |
335 | final double? height; |
336 | |
337 | /// How to inscribe the image into the space allocated during layout. |
338 | /// |
339 | /// The default varies based on the other fields. See the discussion at |
340 | /// [paintImage]. |
341 | final BoxFit? fit; |
342 | |
343 | /// How to inscribe the placeholder image into the space allocated during layout. |
344 | /// |
345 | /// If not value set, it will fallback to [fit]. |
346 | final BoxFit? placeholderFit; |
347 | |
348 | /// The rendering quality of the image. |
349 | /// |
350 | /// {@macro flutter.widgets.image.filterQuality} |
351 | final FilterQuality filterQuality; |
352 | |
353 | /// The rendering quality of the placeholder image. |
354 | /// |
355 | /// {@macro flutter.widgets.image.filterQuality} |
356 | final FilterQuality? placeholderFilterQuality; |
357 | |
358 | /// How to align the image within its bounds. |
359 | /// |
360 | /// The alignment aligns the given position in the image to the given position |
361 | /// in the layout bounds. For example, an [Alignment] alignment of (-1.0, |
362 | /// -1.0) aligns the image to the top-left corner of its layout bounds, while an |
363 | /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the |
364 | /// image with the bottom right corner of its layout bounds. Similarly, an |
365 | /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the |
366 | /// middle of the bottom edge of its layout bounds. |
367 | /// |
368 | /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a |
369 | /// [AlignmentDirectional]), then an ambient [Directionality] widget |
370 | /// must be in scope. |
371 | /// |
372 | /// Defaults to [Alignment.center]. |
373 | /// |
374 | /// See also: |
375 | /// |
376 | /// * [Alignment], a class with convenient constants typically used to |
377 | /// specify an [AlignmentGeometry]. |
378 | /// * [AlignmentDirectional], like [Alignment] for specifying alignments |
379 | /// relative to text direction. |
380 | final AlignmentGeometry alignment; |
381 | |
382 | /// How to paint any portions of the layout bounds not covered by the image. |
383 | final ImageRepeat repeat; |
384 | |
385 | /// Whether to paint the image in the direction of the [TextDirection]. |
386 | /// |
387 | /// If this is true, then in [TextDirection.ltr] contexts, the image will be |
388 | /// drawn with its origin in the top left (the "normal" painting direction for |
389 | /// images); and in [TextDirection.rtl] contexts, the image will be drawn with |
390 | /// a scaling factor of -1 in the horizontal direction so that the origin is |
391 | /// in the top right. |
392 | /// |
393 | /// This is occasionally used with images in right-to-left environments, for |
394 | /// images that were designed for left-to-right locales. Be careful, when |
395 | /// using this, to not flip images with integral shadows, text, or other |
396 | /// effects that will look incorrect when flipped. |
397 | /// |
398 | /// If this is true, there must be an ambient [Directionality] widget in |
399 | /// scope. |
400 | final bool matchTextDirection; |
401 | |
402 | /// Whether to exclude this image from semantics. |
403 | /// |
404 | /// This is useful for images which do not contribute meaningful information |
405 | /// to an application. |
406 | final bool excludeFromSemantics; |
407 | |
408 | /// A semantic description of the [image]. |
409 | /// |
410 | /// Used to provide a description of the [image] to TalkBack on Android, and |
411 | /// VoiceOver on iOS. |
412 | /// |
413 | /// This description will be used both while the [placeholder] is shown and |
414 | /// once the image has loaded. |
415 | final String? imageSemanticLabel; |
416 | |
417 | @override |
418 | State<FadeInImage> createState() => _FadeInImageState(); |
419 | } |
420 | |
421 | class _FadeInImageState extends State<FadeInImage> { |
422 | static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0); |
423 | bool targetLoaded = false; |
424 | |
425 | // These ProxyAnimations are changed to the fade in animation by |
426 | // [_AnimatedFadeOutFadeInState]. Otherwise these animations are reset to |
427 | // their defaults by [_resetAnimations]. |
428 | final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation); |
429 | final ProxyAnimation _placeholderAnimation = ProxyAnimation(_kOpaqueAnimation); |
430 | |
431 | Image _image({ |
432 | required ImageProvider image, |
433 | ImageErrorWidgetBuilder? errorBuilder, |
434 | ImageFrameBuilder? frameBuilder, |
435 | BoxFit? fit, |
436 | Color? color, |
437 | BlendMode? colorBlendMode, |
438 | required FilterQuality filterQuality, |
439 | required Animation<double> opacity, |
440 | }) { |
441 | return Image( |
442 | image: image, |
443 | errorBuilder: errorBuilder, |
444 | frameBuilder: frameBuilder, |
445 | opacity: opacity, |
446 | width: widget.width, |
447 | height: widget.height, |
448 | fit: fit, |
449 | color: color, |
450 | colorBlendMode: colorBlendMode, |
451 | filterQuality: filterQuality, |
452 | alignment: widget.alignment, |
453 | repeat: widget.repeat, |
454 | matchTextDirection: widget.matchTextDirection, |
455 | gaplessPlayback: true, |
456 | excludeFromSemantics: true, |
457 | ); |
458 | } |
459 | |
460 | @override |
461 | Widget build(BuildContext context) { |
462 | Widget result = _image( |
463 | image: widget.image, |
464 | errorBuilder: widget.imageErrorBuilder, |
465 | opacity: _imageAnimation, |
466 | fit: widget.fit, |
467 | color: widget.color, |
468 | colorBlendMode: widget.colorBlendMode, |
469 | filterQuality: widget.filterQuality, |
470 | frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
471 | if (wasSynchronouslyLoaded || frame != null) { |
472 | targetLoaded = true; |
473 | } |
474 | return _AnimatedFadeOutFadeIn( |
475 | target: child, |
476 | targetProxyAnimation: _imageAnimation, |
477 | placeholder: _image( |
478 | image: widget.placeholder, |
479 | errorBuilder: widget.placeholderErrorBuilder, |
480 | opacity: _placeholderAnimation, |
481 | color: widget.placeholderColor, |
482 | colorBlendMode: widget.placeholderColorBlendMode, |
483 | fit: widget.placeholderFit ?? widget.fit, |
484 | filterQuality: widget.placeholderFilterQuality ?? widget.filterQuality, |
485 | ), |
486 | placeholderProxyAnimation: _placeholderAnimation, |
487 | isTargetLoaded: targetLoaded, |
488 | wasSynchronouslyLoaded: wasSynchronouslyLoaded, |
489 | fadeInDuration: widget.fadeInDuration, |
490 | fadeOutDuration: widget.fadeOutDuration, |
491 | fadeInCurve: widget.fadeInCurve, |
492 | fadeOutCurve: widget.fadeOutCurve, |
493 | ); |
494 | }, |
495 | ); |
496 | |
497 | if (!widget.excludeFromSemantics) { |
498 | result = Semantics( |
499 | container: widget.imageSemanticLabel != null, |
500 | image: true, |
501 | label: widget.imageSemanticLabel ?? '' , |
502 | child: result, |
503 | ); |
504 | } |
505 | |
506 | return result; |
507 | } |
508 | } |
509 | |
510 | class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget { |
511 | const _AnimatedFadeOutFadeIn({ |
512 | required this.target, |
513 | required this.targetProxyAnimation, |
514 | required this.placeholder, |
515 | required this.placeholderProxyAnimation, |
516 | required this.isTargetLoaded, |
517 | required this.fadeOutDuration, |
518 | required this.fadeOutCurve, |
519 | required this.fadeInDuration, |
520 | required this.fadeInCurve, |
521 | required this.wasSynchronouslyLoaded, |
522 | }) : assert(!wasSynchronouslyLoaded || isTargetLoaded), |
523 | super(duration: fadeInDuration + fadeOutDuration); |
524 | |
525 | final Widget target; |
526 | final ProxyAnimation targetProxyAnimation; |
527 | final Widget placeholder; |
528 | final ProxyAnimation placeholderProxyAnimation; |
529 | final bool isTargetLoaded; |
530 | final Duration fadeInDuration; |
531 | final Duration fadeOutDuration; |
532 | final Curve fadeInCurve; |
533 | final Curve fadeOutCurve; |
534 | final bool wasSynchronouslyLoaded; |
535 | |
536 | @override |
537 | _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState(); |
538 | } |
539 | |
540 | class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> { |
541 | Tween<double>? _targetOpacity; |
542 | Tween<double>? _placeholderOpacity; |
543 | Animation<double>? _targetOpacityAnimation; |
544 | Animation<double>? _placeholderOpacityAnimation; |
545 | |
546 | @override |
547 | void forEachTween(TweenVisitor<dynamic> visitor) { |
548 | _targetOpacity = |
549 | visitor( |
550 | _targetOpacity, |
551 | widget.isTargetLoaded ? 1.0 : 0.0, |
552 | (dynamic value) => Tween<double>(begin: value as double), |
553 | ) |
554 | as Tween<double>?; |
555 | _placeholderOpacity = |
556 | visitor( |
557 | _placeholderOpacity, |
558 | widget.isTargetLoaded ? 0.0 : 1.0, |
559 | (dynamic value) => Tween<double>(begin: value as double), |
560 | ) |
561 | as Tween<double>?; |
562 | } |
563 | |
564 | @override |
565 | void didUpdateTweens() { |
566 | if (widget.wasSynchronouslyLoaded) { |
567 | // Opacity animations should not be reset if image was synchronously loaded. |
568 | return; |
569 | } |
570 | |
571 | _placeholderOpacityAnimation = |
572 | 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 | |