fade_in_image.dart 21.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import 'basic.dart';
import 'framework.dart';
import 'image.dart';
10
import 'implicit_animations.dart';
11

12
// Examples can assume:
13
// late Uint8List bytes;
14

15 16 17
/// An image that shows a [placeholder] image while the target [image] is
/// loading, then fades in the new image when it loads.
///
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 23
/// {@youtube 560 315 https://www.youtube.com/watch?v=pK738Pg9cxc}
///
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 27
/// [placeholder] is never displayed.
///
28 29
/// The [fadeOutDuration] and [fadeOutCurve] properties control the fade-out
/// animation of the [placeholder].
30
///
31 32
/// The [fadeInDuration] and [fadeInCurve] properties control the fade-in
/// animation of the target [image].
33
///
34 35
/// Prefer a [placeholder] that's already cached so that it is displayed
/// immediately. This prevents it from popping onto the screen.
36
///
37 38
/// 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
39 40 41 42
/// 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
43
/// new [ImageStream.key] is different, this widget subscribes to the new stream
44 45 46 47 48 49 50
/// 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]).
///
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 63
  /// Creates a widget that displays a [placeholder] while an [image] is loading,
  /// then fades-out the placeholder and fades-in the image.
64
  ///
65 66 67
  /// The [placeholder] and [image] may be composed in a [ResizeImage] to provide
  /// a custom decode/cache size.
  ///
68 69 70
  /// The [placeholder] and [image] may be have their own BoxFit settings via [fit]
  /// and [placeholderFit].
  ///
71
  /// The [placeholder], [image], [fadeOutDuration], [fadeOutCurve],
Ian Hickson's avatar
Ian Hickson committed
72 73
  /// [fadeInDuration], [fadeInCurve], [alignment], [repeat], and
  /// [matchTextDirection] arguments must not be null.
74
  ///
75
  /// If [excludeFromSemantics] is true, then [imageSemanticLabel] will be ignored.
76
  const FadeInImage({
77
    super.key,
78
    required this.placeholder,
79
    this.placeholderErrorBuilder,
80
    required this.image,
81
    this.imageErrorBuilder,
82 83
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
84 85 86 87
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
88 89 90
    this.width,
    this.height,
    this.fit,
91
    this.placeholderFit,
92 93 94
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
95 96 97 98 99 100
  }) : assert(placeholder != null),
       assert(image != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
101
       assert(alignment != null),
102
       assert(repeat != null),
103
       assert(matchTextDirection != null);
104 105 106 107

  /// Creates a widget that uses a placeholder image stored in memory while
  /// loading the final image from the network.
  ///
108
  /// The `placeholder` argument contains the bytes of the in-memory image.
109
  ///
110
  /// The `image` argument is the URL of the final image.
111
  ///
112 113
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
114
  ///
115 116 117 118 119 120 121
  /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
  /// or [imageCacheHeight] are provided, it indicates to the
  /// engine that the respective image should be decoded at the specified size.
  /// The image will be rendered to the constraints of the layout or [width]
  /// and [height] regardless of these parameters. These parameters are primarily
  /// intended to reduce the memory usage of [ImageCache].
  ///
122
  /// The [placeholder], [image], [placeholderScale], [imageScale],
Ian Hickson's avatar
Ian Hickson committed
123 124 125
  /// [fadeOutDuration], [fadeOutCurve], [fadeInDuration], [fadeInCurve],
  /// [alignment], [repeat], and [matchTextDirection] arguments must not be
  /// null.
126 127 128
  ///
  /// See also:
  ///
129
  ///  * [Image.memory], which has more details about loading images from
130
  ///    memory.
131
  ///  * [Image.network], which has more details about loading images from
132 133
  ///    the network.
  FadeInImage.memoryNetwork({
134
    super.key,
135
    required Uint8List placeholder,
136
    this.placeholderErrorBuilder,
137
    required String image,
138
    this.imageErrorBuilder,
139 140
    double placeholderScale = 1.0,
    double imageScale = 1.0,
141 142
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
143 144 145 146
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
147 148 149
    this.width,
    this.height,
    this.fit,
150
    this.placeholderFit,
151 152 153
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
154 155 156 157
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
158 159 160 161 162 163 164 165
  }) : assert(placeholder != null),
       assert(image != null),
       assert(placeholderScale != null),
       assert(imageScale != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
166
       assert(alignment != null),
167
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
168
       assert(matchTextDirection != null),
169
       placeholder = ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, MemoryImage(placeholder, scale: placeholderScale)),
170
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
171 172 173 174

  /// Creates a widget that uses a placeholder image stored in an asset bundle
  /// while loading the final image from the network.
  ///
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 180
  /// The `placeholderScale` and `imageScale` arguments are passed to their
  /// respective [ImageProvider]s (see also [ImageInfo.scale]).
181
  ///
182
  /// If `placeholderScale` is omitted or is null, pixel-density-aware asset
183 184 185
  /// resolution will be attempted for the [placeholder] image. Otherwise, the
  /// exact asset specified will be used.
  ///
186 187 188 189 190 191 192
  /// If [placeholderCacheWidth], [placeholderCacheHeight], [imageCacheWidth],
  /// or [imageCacheHeight] are provided, it indicates to the
  /// engine that the respective image should be decoded at the specified size.
  /// The image will be rendered to the constraints of the layout or [width]
  /// and [height] regardless of these parameters. These parameters are primarily
  /// intended to reduce the memory usage of [ImageCache].
  ///
193
  /// The [placeholder], [image], [imageScale], [fadeOutDuration],
Ian Hickson's avatar
Ian Hickson committed
194 195
  /// [fadeOutCurve], [fadeInDuration], [fadeInCurve], [alignment], [repeat],
  /// and [matchTextDirection] arguments must not be null.
196 197 198
  ///
  /// See also:
  ///
199
  ///  * [Image.asset], which has more details about loading images from
200
  ///    asset bundles.
201
  ///  * [Image.network], which has more details about loading images from
202 203
  ///    the network.
  FadeInImage.assetNetwork({
204
    super.key,
205
    required String placeholder,
206
    this.placeholderErrorBuilder,
207
    required String image,
208
    this.imageErrorBuilder,
209 210
    AssetBundle? bundle,
    double? placeholderScale,
211
    double imageScale = 1.0,
212 213
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
214 215 216 217
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
218 219 220
    this.width,
    this.height,
    this.fit,
221
    this.placeholderFit,
222 223 224
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
225 226 227 228
    int? placeholderCacheWidth,
    int? placeholderCacheHeight,
    int? imageCacheWidth,
    int? imageCacheHeight,
229 230 231
  }) : assert(placeholder != null),
       assert(image != null),
       placeholder = placeholderScale != null
232 233
         ? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
         : ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
234 235 236 237 238
       assert(imageScale != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
Ian Hickson's avatar
Ian Hickson committed
239
       assert(alignment != null),
240
       assert(repeat != null),
Ian Hickson's avatar
Ian Hickson committed
241
       assert(matchTextDirection != null),
242
       image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale));
243 244 245 246

  /// Image displayed while the target [image] is loading.
  final ImageProvider placeholder;

247 248 249 250 251 252
  /// A builder function that is called if an error occurs during placeholder
  /// image loading.
  ///
  /// If this builder is not provided, any exceptions will be reported to
  /// [FlutterError.onError]. If it is provided, the caller should either handle
  /// the exception by providing a replacement widget, or rethrow the exception.
253
  final ImageErrorWidgetBuilder? placeholderErrorBuilder;
254

255
  /// The target image that is displayed once it has loaded.
256 257
  final ImageProvider image;

258 259 260 261 262
  /// A builder function that is called if an error occurs during image loading.
  ///
  /// If this builder is not provided, any exceptions will be reported to
  /// [FlutterError.onError]. If it is provided, the caller should either handle
  /// the exception by providing a replacement widget, or rethrow the exception.
263
  final ImageErrorWidgetBuilder? imageErrorBuilder;
264

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  /// 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.
283
  final double? width;
284 285 286 287 288 289 290

  /// 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.
291
  final double? height;
292 293 294 295 296

  /// How to inscribe the image into the space allocated during layout.
  ///
  /// The default varies based on the other fields. See the discussion at
  /// [paintImage].
297
  final BoxFit? fit;
298

299 300 301 302 303
  /// How to inscribe the placeholder image into the space allocated during layout.
  ///
  /// If not value set, it will fallback to [fit].
  final BoxFit? placeholderFit;

304 305
  /// How to align the image within its bounds.
  ///
Ian Hickson's avatar
Ian Hickson committed
306
  /// The alignment aligns the given position in the image to the given position
307 308
  /// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
  /// -1.0) aligns the image to the top-left corner of its layout bounds, while an
309
  /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
Ian Hickson's avatar
Ian Hickson committed
310
  /// image with the bottom right corner of its layout bounds. Similarly, an
311
  /// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
Ian Hickson's avatar
Ian Hickson committed
312 313 314
  /// middle of the bottom edge of its layout bounds.
  ///
  /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
315
  /// [AlignmentDirectional]), then an ambient [Directionality] widget
Ian Hickson's avatar
Ian Hickson committed
316 317
  /// must be in scope.
  ///
318
  /// Defaults to [Alignment.center].
319 320 321 322 323 324 325
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
326
  final AlignmentGeometry alignment;
327 328 329 330

  /// How to paint any portions of the layout bounds not covered by the image.
  final ImageRepeat repeat;

Ian Hickson's avatar
Ian Hickson committed
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  /// Whether to paint the image in the direction of the [TextDirection].
  ///
  /// If this is true, then in [TextDirection.ltr] contexts, the image will be
  /// drawn with its origin in the top left (the "normal" painting direction for
  /// images); and in [TextDirection.rtl] contexts, the image will be drawn with
  /// a scaling factor of -1 in the horizontal direction so that the origin is
  /// in the top right.
  ///
  /// This is occasionally used with images in right-to-left environments, for
  /// images that were designed for left-to-right locales. Be careful, when
  /// using this, to not flip images with integral shadows, text, or other
  /// effects that will look incorrect when flipped.
  ///
  /// If this is true, there must be an ambient [Directionality] widget in
  /// scope.
  final bool matchTextDirection;

348 349
  /// Whether to exclude this image from semantics.
  ///
350 351
  /// This is useful for images which do not contribute meaningful information
  /// to an application.
352 353
  final bool excludeFromSemantics;

354
  /// A semantic description of the [image].
355 356 357
  ///
  /// Used to provide a description of the [image] to TalkBack on Android, and
  /// VoiceOver on iOS.
358
  ///
359 360
  /// This description will be used both while the [placeholder] is shown and
  /// once the image has loaded.
361
  final String? imageSemanticLabel;
362

363 364 365 366 367 368
  @override
  State<FadeInImage> createState() => _FadeInImageState();
}

class _FadeInImageState extends State<FadeInImage> {
  static const Animation<double> _kOpaqueAnimation = AlwaysStoppedAnimation<double>(1.0);
369
  bool targetLoaded = false;
370 371 372 373 374 375 376

  // These ProxyAnimations are changed to the fade in animation by
  // [_AnimatedFadeOutFadeInState]. Otherwise these animations are reset to
  // their defaults by [_resetAnimations].
  final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation);
  final ProxyAnimation _placeholderAnimation = ProxyAnimation(_kOpaqueAnimation);

377
  Image _image({
378 379 380
    required ImageProvider image,
    ImageErrorWidgetBuilder? errorBuilder,
    ImageFrameBuilder? frameBuilder,
381
    BoxFit? fit,
382
    required Animation<double> opacity,
383 384 385 386
  }) {
    assert(image != null);
    return Image(
      image: image,
387
      errorBuilder: errorBuilder,
388
      frameBuilder: frameBuilder,
389 390 391
      opacity: opacity,
      width: widget.width,
      height: widget.height,
392
      fit: fit,
393 394 395
      alignment: widget.alignment,
      repeat: widget.repeat,
      matchTextDirection: widget.matchTextDirection,
396 397 398 399
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }
400

401 402 403
  @override
  Widget build(BuildContext context) {
    Widget result = _image(
404 405 406
      image: widget.image,
      errorBuilder: widget.imageErrorBuilder,
      opacity: _imageAnimation,
407
      fit: widget.fit,
408
      frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
409 410
        if (wasSynchronouslyLoaded || frame != null) {
          targetLoaded = true;
411
        }
412 413
        return _AnimatedFadeOutFadeIn(
          target: child,
414 415 416 417 418
          targetProxyAnimation: _imageAnimation,
          placeholder: _image(
            image: widget.placeholder,
            errorBuilder: widget.placeholderErrorBuilder,
            opacity: _placeholderAnimation,
419
            fit: widget.placeholderFit ?? widget.fit,
420 421
          ),
          placeholderProxyAnimation: _placeholderAnimation,
422 423
          isTargetLoaded: targetLoaded,
          wasSynchronouslyLoaded: wasSynchronouslyLoaded,
424 425 426 427
          fadeInDuration: widget.fadeInDuration,
          fadeOutDuration: widget.fadeOutDuration,
          fadeInCurve: widget.fadeInCurve,
          fadeOutCurve: widget.fadeOutCurve,
428 429 430
        );
      },
    );
431

432
    if (!widget.excludeFromSemantics) {
433
      result = Semantics(
434
        container: widget.imageSemanticLabel != null,
435
        image: true,
436
        label: widget.imageSemanticLabel ?? '',
437 438
        child: result,
      );
439 440
    }

441
    return result;
442 443 444
  }
}

445 446
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
447
    required this.target,
448
    required this.targetProxyAnimation,
449
    required this.placeholder,
450
    required this.placeholderProxyAnimation,
451 452 453 454 455
    required this.isTargetLoaded,
    required this.fadeOutDuration,
    required this.fadeOutCurve,
    required this.fadeInDuration,
    required this.fadeInCurve,
456
    required this.wasSynchronouslyLoaded,
457 458 459 460 461 462 463
  }) : assert(target != null),
       assert(placeholder != null),
       assert(isTargetLoaded != null),
       assert(fadeOutDuration != null),
       assert(fadeOutCurve != null),
       assert(fadeInDuration != null),
       assert(fadeInCurve != null),
464
       assert(!wasSynchronouslyLoaded || isTargetLoaded),
465
       super(duration: fadeInDuration + fadeOutDuration);
466

467
  final Widget target;
468
  final ProxyAnimation targetProxyAnimation;
469
  final Widget placeholder;
470
  final ProxyAnimation placeholderProxyAnimation;
471 472 473 474 475
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;
476
  final bool wasSynchronouslyLoaded;
477 478

  @override
479 480
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
481

482
class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
483 484 485 486
  Tween<double>? _targetOpacity;
  Tween<double>? _placeholderOpacity;
  Animation<double>? _targetOpacityAnimation;
  Animation<double>? _placeholderOpacityAnimation;
487 488

  @override
489 490 491 492
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
493
      (dynamic value) => Tween<double>(begin: value as double),
494
    ) as Tween<double>?;
495 496 497
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
498
      (dynamic value) => Tween<double>(begin: value as double),
499
    ) as Tween<double>?;
500 501 502
  }

  @override
503
  void didUpdateTweens() {
504 505 506 507 508
    if (widget.wasSynchronouslyLoaded) {
      // Opacity animations should not be reset if image was synchronously loaded.
      return;
    }

509
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
510
      TweenSequenceItem<double>(
511
        tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
512 513 514 515 516 517
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
518
    ]))..addStatusListener((AnimationStatus status) {
519
      if (_placeholderOpacityAnimation!.isCompleted) {
520
        // Need to rebuild to remove placeholder now that it is invisible.
521 522 523 524
        setState(() {});
      }
    });

525
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
526 527 528 529 530
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
531
        tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
532 533 534
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
535 536 537

    widget.targetProxyAnimation.parent = _targetOpacityAnimation;
    widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
538 539 540 541
  }

  @override
  Widget build(BuildContext context) {
542
    if (widget.wasSynchronouslyLoaded || _placeholderOpacityAnimation!.isCompleted) {
543
      return widget.target;
544 545
    }

546 547 548 549 550 551 552
    return Stack(
      fit: StackFit.passthrough,
      alignment: AlignmentDirectional.center,
      // Text direction is irrelevant here since we're using center alignment,
      // but it allows the Stack to avoid a call to Directionality.of()
      textDirection: TextDirection.ltr,
      children: <Widget>[
553 554
        widget.target,
        widget.placeholder,
555
      ],
556
    );
557 558 559
  }

  @override
560 561 562 563
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
564 565
  }
}