Files
ReelShort/lib/core/widgets/remote_image.dart
T

119 lines
4.1 KiB
Dart

// Author: fruit.com.cn
// 职责:统一网络图片入口,补服务地址,并在加载中/失败时显示纯色占位块。
import 'package:flutter/material.dart';
import '../config/app_config.dart';
import '../theme/app_colors.dart';
const int _minImageDecodePixels = 96;
const int _maxImageDecodePixels = 2048;
/// 将布局逻辑尺寸换算成有限像素尺寸,兼顾小头像和全屏首帧。
int resolveImageDecodePixels(double? logicalExtent, double devicePixelRatio) {
final ratio = devicePixelRatio.isFinite && devicePixelRatio > 0
? devicePixelRatio
: 1.0;
final extent =
logicalExtent != null && logicalExtent.isFinite && logicalExtent > 0
? logicalExtent
: 120.0;
return (extent * ratio)
.ceil()
.clamp(_minImageDecodePixels, _maxImageDecodePixels)
.toInt();
}
/// 封面图统一入口:后端给的是相对路径,这里补服务地址并处理加载失败。
class RemoteImage extends StatelessWidget {
const RemoteImage(
this.path, {
super.key,
this.width,
this.height,
this.fit = BoxFit.cover,
this.fallbackAspectRatio,
this.fallbackColor = AppColors.raised,
this.fallbackWidget,
this.decodeWidth,
this.decodeHeight,
});
final String path;
final double? width;
final double? height;
final BoxFit fit;
final double? fallbackAspectRatio;
final Color fallbackColor;
/// 自定义占位:地址为空、加载中、加载失败时都显示它(优先于 [fallbackColor])。
final Widget? fallbackWidget;
final int? decodeWidth;
final int? decodeHeight;
@override
Widget build(BuildContext context) {
final url = AppConfig.mediaUrl(path);
if (url.isEmpty) return _fallback();
return LayoutBuilder(
builder: (context, constraints) {
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final layoutWidth = constraints.hasBoundedWidth
? constraints.maxWidth
: width;
final aspectRatio =
fallbackAspectRatio != null &&
fallbackAspectRatio!.isFinite &&
fallbackAspectRatio! > 0
? fallbackAspectRatio
: null;
final inferredHeight = constraints.hasBoundedHeight
? constraints.maxHeight
: height ??
(layoutWidth != null && aspectRatio != null
? layoutWidth / aspectRatio
: null);
final cacheWidth =
decodeWidth ?? resolveImageDecodePixels(layoutWidth, pixelRatio);
// cover/contain 本身会保持宽高比,此时若同时强制 cacheWidth 和
// cacheHeight,解码会把竖版图强行拉成横版像素。只在显式指定了
// decodeHeight、或 fit 不保持宽高比时才限定解码高度。
final keepsAspectRatio = fit == BoxFit.cover || fit == BoxFit.contain;
final cacheHeight =
decodeHeight ??
(keepsAspectRatio || inferredHeight == null
? null
: resolveImageDecodePixels(inferredHeight, pixelRatio));
return Image.network(
url,
width: width,
height: height,
fit: fit,
cacheWidth: cacheWidth
.clamp(_minImageDecodePixels, _maxImageDecodePixels)
.toInt(),
cacheHeight: cacheHeight
?.clamp(_minImageDecodePixels, _maxImageDecodePixels)
.toInt(),
gaplessPlayback: true,
errorBuilder: (_, _, _) => _fallback(),
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) return child;
return _fallback();
},
);
},
);
}
Widget _fallback() {
final custom = fallbackWidget;
final content = custom != null
? SizedBox(width: width, height: height, child: custom)
: Container(width: width, height: height, color: fallbackColor);
final ratio = fallbackAspectRatio;
if (ratio == null) return content;
return AspectRatio(aspectRatio: ratio, child: content);
}
}