mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 04:57:23 +00:00
Initial commit
This commit is contained in:
321
lib/components/animated_image.dart
Normal file
321
lib/components/animated_image.dart
Normal file
@@ -0,0 +1,321 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.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.low,
|
||||
this.isAntiAlias = false,
|
||||
Map<String, String>? 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<double>? opacity;
|
||||
|
||||
final Color? color;
|
||||
|
||||
final bool isAntiAlias;
|
||||
|
||||
static void clear() => _AnimatedImageState.clear();
|
||||
|
||||
@override
|
||||
State<AnimatedImage> createState() => _AnimatedImageState();
|
||||
}
|
||||
|
||||
class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {
|
||||
ImageStream? _imageStream;
|
||||
ImageInfo? _imageInfo;
|
||||
ImageChunkEvent? _loadingProgress;
|
||||
bool _isListeningToStream = false;
|
||||
late bool _invertColors;
|
||||
int? _frameNumber;
|
||||
bool _wasSynchronouslyLoaded = false;
|
||||
late DisposableBuildContext<State<AnimatedImage>> _scrollAwareContext;
|
||||
Object? _lastException;
|
||||
ImageStreamCompleterHandle? _completerHandle;
|
||||
|
||||
static final Map<int, Size> _cache = {};
|
||||
|
||||
static clear() => _cache.clear();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_scrollAwareContext = DisposableBuildContext<State<AnimatedImage>>(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<Object>(
|
||||
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){
|
||||
// build image
|
||||
result = RawImage(
|
||||
// Do not clone the image, because RawImage is a stateless wrapper.
|
||||
// The image will be disposed by this state object when it is not needed
|
||||
// anymore, such as when it is unmounted or when the image stream pushes
|
||||
// a new image.
|
||||
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,
|
||||
alignment: widget.alignment,
|
||||
repeat: widget.repeat,
|
||||
centerSlice: widget.centerSlice,
|
||||
matchTextDirection: widget.matchTextDirection,
|
||||
invertColors: _invertColors,
|
||||
isAntiAlias: widget.isAntiAlias,
|
||||
filterQuality: widget.filterQuality,
|
||||
fit: widget.fit,
|
||||
);
|
||||
} else if (_lastException != null) {
|
||||
result = const Center(
|
||||
child: Icon(FluentIcons.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<ImageStream>('stream', _imageStream));
|
||||
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
||||
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
|
||||
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
|
||||
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user