180 lines
4.5 KiB
Dart
180 lines
4.5 KiB
Dart
|
|
// lib/widgets/smart_image.dart
|
|||
|
|
import 'package:flutter/material.dart';
|
|||
|
|
|
|||
|
|
/// SmartAssetImage
|
|||
|
|
/// - 自动计算目标像素宽度(devicePixelRatio * context.width)
|
|||
|
|
/// - 使用 ResizeImage 限制解码尺寸以降低首次解码开销
|
|||
|
|
/// - 预缓存(precacheImage),并提供占位色/小占位Widget
|
|||
|
|
/// - gaplessPlayback 避免重建时闪烁
|
|||
|
|
class SmartAssetImage extends StatefulWidget {
|
|||
|
|
final String assetPath;
|
|||
|
|
final double? height;
|
|||
|
|
final double? width;
|
|||
|
|
final BoxFit fit;
|
|||
|
|
final Color placeholderColor;
|
|||
|
|
final Duration precacheTimeout;
|
|||
|
|
|
|||
|
|
const SmartAssetImage({
|
|||
|
|
Key? key,
|
|||
|
|
required this.assetPath,
|
|||
|
|
this.height,
|
|||
|
|
this.width,
|
|||
|
|
this.fit = BoxFit.cover,
|
|||
|
|
this.placeholderColor = const Color(0xFFE8F4FD),
|
|||
|
|
this.precacheTimeout = const Duration(milliseconds: 800),
|
|||
|
|
}) : super(key: key);
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
State<SmartAssetImage> createState() => _SmartAssetImageState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _SmartAssetImageState extends State<SmartAssetImage> {
|
|||
|
|
ImageProvider? _provider;
|
|||
|
|
bool _done = false;
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void didChangeDependencies() {
|
|||
|
|
super.didChangeDependencies();
|
|||
|
|
_prepareImage();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _prepareImage() async {
|
|||
|
|
// 避免重复处理
|
|||
|
|
if (_done) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
final mq = MediaQuery.of(context);
|
|||
|
|
final logicalWidth = widget.width ?? mq.size.width;
|
|||
|
|
final devicePixelRatio = mq.devicePixelRatio;
|
|||
|
|
final int targetWidth = (logicalWidth * devicePixelRatio).toInt();
|
|||
|
|
|
|||
|
|
// 使用 ResizeImage 限制解码到合适的像素宽度
|
|||
|
|
final provider = ResizeImage(
|
|||
|
|
AssetImage(widget.assetPath),
|
|||
|
|
width: targetWidth,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// attempt to precache but don't block too long
|
|||
|
|
final precacheFuture = precacheImage(provider, context);
|
|||
|
|
// optional: bound the wait time to prevent waiting forever
|
|||
|
|
await precacheFuture
|
|||
|
|
.timeout(widget.precacheTimeout, onTimeout: () => null);
|
|||
|
|
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_provider = provider;
|
|||
|
|
_done = true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
// 如果预缓存失败,仍然回退到常规 AssetImage
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_provider = AssetImage(widget.assetPath);
|
|||
|
|
_done = true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
final placeholder = Container(
|
|||
|
|
width: widget.width ?? double.infinity,
|
|||
|
|
height: widget.height,
|
|||
|
|
color: widget.placeholderColor,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (_provider == null) {
|
|||
|
|
return placeholder;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Image(
|
|||
|
|
image: _provider!,
|
|||
|
|
width: widget.width ?? double.infinity,
|
|||
|
|
height: widget.height,
|
|||
|
|
fit: widget.fit,
|
|||
|
|
gaplessPlayback: true,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// SmartNetworkImage (无第三方包版本)
|
|||
|
|
/// - 先显示 placeholder / local placeholder
|
|||
|
|
/// - 再通过 precacheImage 加载 NetworkImage
|
|||
|
|
class SmartNetworkImage extends StatefulWidget {
|
|||
|
|
final String url;
|
|||
|
|
final double? height;
|
|||
|
|
final double? width;
|
|||
|
|
final BoxFit fit;
|
|||
|
|
final Widget? placeholder;
|
|||
|
|
final Duration precacheTimeout;
|
|||
|
|
|
|||
|
|
const SmartNetworkImage({
|
|||
|
|
Key? key,
|
|||
|
|
required this.url,
|
|||
|
|
this.height,
|
|||
|
|
this.width,
|
|||
|
|
this.fit = BoxFit.cover,
|
|||
|
|
this.placeholder,
|
|||
|
|
this.precacheTimeout = const Duration(milliseconds: 800),
|
|||
|
|
}) : super(key: key);
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
State<SmartNetworkImage> createState() => _SmartNetworkImageState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _SmartNetworkImageState extends State<SmartNetworkImage> {
|
|||
|
|
ImageProvider? _provider;
|
|||
|
|
bool _done = false;
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void didChangeDependencies() {
|
|||
|
|
super.didChangeDependencies();
|
|||
|
|
_prepare();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> _prepare() async {
|
|||
|
|
if (_done) return;
|
|||
|
|
try {
|
|||
|
|
final provider = NetworkImage(widget.url);
|
|||
|
|
await precacheImage(provider, context).timeout(widget.precacheTimeout, onTimeout: () => null);
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_provider = provider;
|
|||
|
|
_done = true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_provider = NetworkImage(widget.url); // fallback
|
|||
|
|
_done = true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
final placeholder = widget.placeholder ??
|
|||
|
|
Container(
|
|||
|
|
width: widget.width ?? double.infinity,
|
|||
|
|
height: widget.height,
|
|||
|
|
color: const Color(0xFFE8F4FD),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (_provider == null) {
|
|||
|
|
return placeholder;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Image(
|
|||
|
|
image: _provider!,
|
|||
|
|
width: widget.width ?? double.infinity,
|
|||
|
|
height: widget.height,
|
|||
|
|
fit: widget.fit,
|
|||
|
|
gaplessPlayback: true,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|