| 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 | |