c94b9e5e58
Co-Authored-By: Claude Code <noreply@anthropic.com>
68 lines
2.2 KiB
Dart
68 lines
2.2 KiB
Dart
/// 网络照片缩略图(UI-01 §4.1):固定宽高比预留 + 占位 + 失败替代图。
|
|
///
|
|
/// 布局稳定性优先:占位不是转圈,而是同一宽高比的中性色块(网格/卡片
|
|
/// 不因加载状态跳动);加载完成前显示占位,失败显示中性色块 + 小图标
|
|
/// (不显示破图)。[url] 为 null(无封面/已删除)时常驻占位。使用
|
|
/// Image.network + errorBuilder(项目未引入 cached_network_image,保持
|
|
/// 零新增依赖)。
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// 网络缩略图:`AspectRatio` 包裹,cover 裁切。
|
|
class PhotoThumbnail extends StatelessWidget {
|
|
const PhotoThumbnail({
|
|
required this.url,
|
|
required this.aspectRatio,
|
|
this.fit = BoxFit.cover,
|
|
super.key,
|
|
});
|
|
|
|
/// 图片 URL;null 显示常驻占位(无封面/无预览语义)。
|
|
final String? url;
|
|
|
|
/// 宽高比(相册封面 16:9、网格 1:1 等)——占位与图片共用,尺寸恒定。
|
|
final double aspectRatio;
|
|
|
|
/// 图片适配(默认 cover 裁切;大图查看用 contain)。
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (url == null) {
|
|
return AspectRatio(
|
|
aspectRatio: aspectRatio,
|
|
child: _placeholder(context),
|
|
);
|
|
}
|
|
return AspectRatio(
|
|
aspectRatio: aspectRatio,
|
|
child: Image.network(
|
|
url!,
|
|
fit: fit,
|
|
// 帧 ready 前显示中性占位(不是 spinner):布局稳定优先。
|
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) =>
|
|
wasSynchronouslyLoaded || frame != null
|
|
? child
|
|
: _placeholder(context),
|
|
errorBuilder: (_, _, _) =>
|
|
_placeholder(context, icon: Icons.broken_image_outlined),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 中性占位色块 + 小图标(加载中 / 无封面 / 加载失败共用)。
|
|
Widget _placeholder(
|
|
BuildContext context, {
|
|
IconData icon = Icons.image_outlined,
|
|
}) {
|
|
final theme = Theme.of(context);
|
|
return ColoredBox(
|
|
color: theme.colorScheme.surfaceContainerHighest,
|
|
child: Center(
|
|
child: Icon(icon, size: 24, color: theme.colorScheme.onSurfaceVariant),
|
|
),
|
|
);
|
|
}
|
|
}
|