part of 'components.dart'; class AnimatedImage extends StatefulWidget { /// show animation when loading is complete. AnimatedImage({ required ImageProvider image, super.key, double scale = 1.0, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.filterQuality = FilterQuality.medium, this.isAntiAlias = false, Map? headers, int? cacheWidth, int? cacheHeight, } ): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image), assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); final ImageProvider image; final String? semanticLabel; final bool excludeFromSemantics; final double? width; final double? height; final bool gaplessPlayback; final bool matchTextDirection; final Rect? centerSlice; final ImageRepeat repeat; final AlignmentGeometry alignment; final BoxFit? fit; final BlendMode? colorBlendMode; final FilterQuality filterQuality; final Animation? opacity; final Color? color; final bool isAntiAlias; static void clear() => _AnimatedImageState.clear(); @override State createState() => _AnimatedImageState(); } class _AnimatedImageState extends State with WidgetsBindingObserver { ImageStream? _imageStream; ImageInfo? _imageInfo; ImageChunkEvent? _loadingProgress; bool _isListeningToStream = false; late bool _invertColors; int? _frameNumber; bool _wasSynchronouslyLoaded = false; late DisposableBuildContext> _scrollAwareContext; Object? _lastException; ImageStreamCompleterHandle? _completerHandle; static final Map _cache = {}; static clear() => _cache.clear(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _scrollAwareContext = DisposableBuildContext>(this); } @override void dispose() { assert(_imageStream != null); WidgetsBinding.instance.removeObserver(this); _stopListeningToStream(); _completerHandle?.dispose(); _scrollAwareContext.dispose(); _replaceImage(info: null); super.dispose(); } @override void didChangeDependencies() { _updateInvertColors(); _resolveImage(); if (TickerMode.of(context)) { _listenToStream(); } else { _stopListeningToStream(keepStreamAlive: true); } super.didChangeDependencies(); } @override void didUpdateWidget(AnimatedImage oldWidget) { super.didUpdateWidget(oldWidget); if (widget.image != oldWidget.image) { _resolveImage(); } } @override void didChangeAccessibilityFeatures() { super.didChangeAccessibilityFeatures(); setState(() { _updateInvertColors(); }); } @override void reassemble() { _resolveImage(); // in case the image cache was flushed super.reassemble(); } void _updateInvertColors() { _invertColors = MediaQuery.maybeInvertColorsOf(context) ?? SemanticsBinding.instance.accessibilityFeatures.invertColors; } void _resolveImage() { final ScrollAwareImageProvider provider = ScrollAwareImageProvider( context: _scrollAwareContext, imageProvider: widget.image, ); final ImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, )); _updateSourceStream(newStream); } ImageStreamListener? _imageStreamListener; ImageStreamListener _getListener({bool recreateListener = false}) { if(_imageStreamListener == null || recreateListener) { _lastException = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, onChunk: _handleImageChunk, onError: (Object error, StackTrace? stackTrace) { setState(() { _lastException = error; }); }, ); } return _imageStreamListener!; } void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _replaceImage(info: imageInfo); _loadingProgress = null; _lastException = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall; }); } void _handleImageChunk(ImageChunkEvent event) { setState(() { _loadingProgress = event; _lastException = null; }); } void _replaceImage({required ImageInfo? info}) { final ImageInfo? oldImageInfo = _imageInfo; SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose()); _imageInfo = info; } // Updates _imageStream to newStream, and moves the stream listener // registration from the old stream to the new stream (if a listener was // registered). void _updateSourceStream(ImageStream newStream) { if (_imageStream?.key == newStream.key) { return; } if (_isListeningToStream) { _imageStream!.removeListener(_getListener()); } if (!widget.gaplessPlayback) { setState(() { _replaceImage(info: null); }); } setState(() { _loadingProgress = null; _frameNumber = null; _wasSynchronouslyLoaded = false; }); _imageStream = newStream; if (_isListeningToStream) { _imageStream!.addListener(_getListener()); } } void _listenToStream() { if (_isListeningToStream) { return; } _imageStream!.addListener(_getListener()); _completerHandle?.dispose(); _completerHandle = null; _isListeningToStream = true; } /// Stops listening to the image stream, if this state object has attached a /// listener. /// /// If the listener from this state is the last listener on the stream, the /// stream will be disposed. To keep the stream alive, set `keepStreamAlive` /// to true, which create [ImageStreamCompleterHandle] to keep the completer /// alive and is compatible with the [TickerMode] being off. void _stopListeningToStream({bool keepStreamAlive = false}) { if (!_isListeningToStream) { return; } if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) { _completerHandle = _imageStream!.completer!.keepAlive(); } _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } @override Widget build(BuildContext context) { Widget result; if(_imageInfo != null){ result = RawImage( image: _imageInfo?.image, width: widget.width, height: widget.height, debugImageLabel: _imageInfo?.debugLabel, scale: _imageInfo?.scale ?? 1.0, color: widget.color, opacity: widget.opacity, colorBlendMode: widget.colorBlendMode, fit: BoxFit.cover, alignment: widget.alignment, repeat: widget.repeat, centerSlice: widget.centerSlice, matchTextDirection: widget.matchTextDirection, invertColors: _invertColors, isAntiAlias: widget.isAntiAlias, filterQuality: widget.filterQuality, ); } else if (_lastException != null) { result = const Center( child: Icon(Icons.error), ); if (!widget.excludeFromSemantics) { result = Semantics( container: widget.semanticLabel != null, image: true, label: widget.semanticLabel ?? '', child: result, ); } } else{ result = const Center(); } return AnimatedSwitcher( duration: const Duration(milliseconds: 200), reverseDuration: const Duration(milliseconds: 200), child: result, ); } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(DiagnosticsProperty('stream', _imageStream)); description.add(DiagnosticsProperty('pixels', _imageInfo)); description.add(DiagnosticsProperty('loadingProgress', _loadingProgress)); description.add(DiagnosticsProperty('frameNumber', _frameNumber)); description.add(DiagnosticsProperty('wasSynchronouslyLoaded', _wasSynchronouslyLoaded)); } }