9edcb3f2b1
P0-2 查看器缩放:InteractiveViewer(5x max,放大后平移,1x 时穿透到 PageView 滑动切页) P0-3 相机后台释放:WidgetsBindingObserver——paused 时 dispose(省电+ 隐私指示灯),resumed 时重开(沿用前后摄/闪光灯偏好) P0-6 图片内存管理:PhotoThumbnail 加 maxCacheWidth 参数——网格/列表 缩略图限制解码宽度(600/400px),查看器大图不限 397/397 全绿、analyze 0 Co-Authored-By: Claude Code <noreply@anthropic.com>
76 lines
2.6 KiB
Dart
76 lines
2.6 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,
|
|
this.maxCacheWidth,
|
|
super.key,
|
|
});
|
|
|
|
/// 图片 URL;null 显示常驻占位(无封面/无预览语义)。
|
|
final String? url;
|
|
|
|
/// 宽高比(相册封面 16:9、网格 1:1 等)——占位与图片共用,尺寸恒定。
|
|
final double aspectRatio;
|
|
|
|
/// 图片适配(默认 cover 裁切;大图查看用 contain)。
|
|
final BoxFit fit;
|
|
|
|
/// 解码宽度上限(P0-6 内存管理):网格/列表传入,限制 Image.network
|
|
/// 实际解码的像素宽度——防止 4000px 原图全量解码导致 OOM。
|
|
/// null = 不限制(查看器大图等需要完整分辨率的场景)。
|
|
final int? maxCacheWidth;
|
|
|
|
@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,
|
|
// P0-6:限制解码宽度(网格缩略图无需全量分辨率)。
|
|
cacheWidth: maxCacheWidth,
|
|
// 帧 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),
|
|
),
|
|
);
|
|
}
|
|
}
|