151 lines
5.0 KiB
Dart
151 lines
5.0 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../config/app_config.dart';
|
|
import '../theme/app_colors.dart';
|
|
import 'image_placeholder_shimmer.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.showFallbackIcon = false,
|
|
this.fallbackIconKey,
|
|
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 bool showFallbackIcon;
|
|
final Key? fallbackIconKey;
|
|
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(brand: showFallbackIcon);
|
|
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(brand: showFallbackIcon),
|
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
|
if (wasSynchronouslyLoaded || frame != null) return child;
|
|
return _loadingFallback();
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _loadingFallback() {
|
|
final custom = fallbackWidget;
|
|
if (custom != null) return custom;
|
|
return ImagePlaceholderShimmer(child: _fallback(brand: showFallbackIcon));
|
|
}
|
|
|
|
Widget _fallback({required bool brand}) {
|
|
final custom = fallbackWidget;
|
|
if (custom != null) return custom;
|
|
final mark = brand
|
|
? LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final width = constraints.maxWidth.isFinite
|
|
? constraints.maxWidth
|
|
: 120.0;
|
|
return Center(
|
|
child: Image.asset(
|
|
'assets/icon/splash_logo.png',
|
|
key: fallbackIconKey,
|
|
width: width * 0.72,
|
|
fit: BoxFit.contain,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: null;
|
|
final fallback = Container(
|
|
width: width,
|
|
height: height,
|
|
color: brand ? AppColors.mediaBlack : fallbackColor,
|
|
alignment: Alignment.center,
|
|
child: mark,
|
|
);
|
|
final ratio = fallbackAspectRatio;
|
|
if (ratio == null) return fallback;
|
|
return AspectRatio(aspectRatio: ratio, child: fallback);
|
|
}
|
|
}
|