Files
MookNote/lib/widgets/fade_in_local_image.dart
2026-08-09 21:46:23 +08:00

146 lines
4.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'package:flutter/material.dart';
/// 带淡入动画的图片组件(支持本地文件 + HTTP URL
class FadeInLocalImage extends StatefulWidget {
final String? path;
final double? width;
final double? height;
final BoxFit fit;
final Widget? placeholder;
final Widget? errorWidget;
final Duration duration;
const FadeInLocalImage({
super.key,
required this.path,
this.width,
this.height,
this.fit = BoxFit.cover,
this.placeholder,
this.errorWidget,
this.duration = const Duration(milliseconds: 400),
});
@override
State<FadeInLocalImage> createState() => _FadeInLocalImageState();
}
class _FadeInLocalImageState extends State<FadeInLocalImage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacity;
bool _loaded = false;
bool _error = false;
String? _imageUrl;
bool _useNetwork = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_opacity = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_loadImage();
}
Future<void> _loadImage() async {
if (widget.path == null || widget.path!.isEmpty) {
setState(() => _error = true);
return;
}
if (widget.path!.startsWith('http')) {
_useNetwork = true;
_imageUrl = widget.path;
setState(() => _loaded = true);
_controller.forward();
return;
}
final file = File(widget.path!);
final exists = await file.exists();
if (!mounted) return;
if (exists) {
setState(() => _loaded = true);
_controller.forward();
return;
}
if (mounted) setState(() => _error = true);
}
@override
void didUpdateWidget(FadeInLocalImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.path != oldWidget.path) {
_error = false;
_loaded = false;
_useNetwork = false;
_imageUrl = null;
_controller.reset();
_loadImage();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (_error) {
return widget.errorWidget ??
Container(
width: widget.width,
height: widget.height,
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
);
}
if (!_loaded) {
return widget.placeholder ??
Container(
width: widget.width,
height: widget.height,
color: colors.surfaceContainerHighest,
);
}
return FadeTransition(
opacity: _opacity,
child: _useNetwork
? Image.network(
_imageUrl!,
width: widget.width,
height: widget.height,
fit: widget.fit,
errorBuilder: (_, e, __) {
debugPrint('[Image] 网络加载失败: $_imageUrl, 错误: $e');
return widget.errorWidget ??
Container(
width: widget.width,
height: widget.height,
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
);
},
)
: Image.file(
File(widget.path!),
width: widget.width,
height: widget.height,
fit: widget.fit,
errorBuilder: (_, __, ___) => widget.errorWidget ??
Container(
width: widget.width,
height: widget.height,
color: colors.surfaceContainerHighest,
child: Icon(Icons.broken_image_outlined, size: 24, color: colors.onSurface.withValues(alpha: 0.25)),
),
),
);
}
}