Files
photo-live/apps/mobile/lib/ui/components/photo_thumbnail.dart
T
浅念 9edcb3f2b1 feat(app): P0 batch 1 — photo viewer zoom, camera lifecycle, image memory management
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>
2026-09-28 10:35:14 +08:00

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),
),
);
}
}