generated from dellevin/template
146 lines
4.0 KiB
Dart
146 lines
4.0 KiB
Dart
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)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|