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>
This commit is contained in:
@@ -568,43 +568,65 @@ class _ViewerImage extends StatefulWidget {
|
||||
class _ViewerImageState extends State<_ViewerImage> {
|
||||
bool _autoRefreshScheduled = false;
|
||||
|
||||
/// 当前缩放比例(1.0 = 原始;> 1.0 = 放大中,PageView 滑动被禁用)。
|
||||
/// 通过 [InheritedWidget] 通知父级 PageView 切换 physics。
|
||||
final ValueNotifier<double> _scaleNotifier = ValueNotifier(1.0);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Image.network(
|
||||
widget.imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: (context, child, progress) => progress == null
|
||||
? child
|
||||
: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
errorBuilder: (_, _, _) {
|
||||
// 失败 → 一次受控刷新(新签名 URL 触发重建重载)。
|
||||
if (!_autoRefreshScheduled &&
|
||||
widget.controller.canRefresh(widget.photoId)) {
|
||||
_autoRefreshScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.controller.refreshPreview(widget.photoId);
|
||||
});
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.broken_image_outlined,
|
||||
size: 48,
|
||||
color: Colors.white38,
|
||||
),
|
||||
const SizedBox(height: kSpace8),
|
||||
Text(
|
||||
l10n.photoPreviewUnavailable,
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// P0-2(UI06 分析 §二):双指缩放 + 放大后平移;1x 时手势穿透到
|
||||
// PageView 左右滑动切页(InteractiveViewer 在内容不溢出时不拦截)。
|
||||
return InteractiveViewer(
|
||||
maxScale: 5.0,
|
||||
minScale: 1.0,
|
||||
clipBehavior: Clip.none,
|
||||
onInteractionUpdate: (details) => _scaleNotifier.value = details.scale,
|
||||
onInteractionEnd: (_) {
|
||||
// 回弹到 1x 时恢复 PageView 滑动。
|
||||
if (_scaleNotifier.value <= 1.01) _scaleNotifier.value = 1.0;
|
||||
},
|
||||
child: Image.network(
|
||||
widget.imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: (context, child, progress) => progress == null
|
||||
? child
|
||||
: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
errorBuilder: (_, _, _) {
|
||||
// 失败 → 一次受控刷新(新签名 URL 触发重建重载)。
|
||||
if (!_autoRefreshScheduled &&
|
||||
widget.controller.canRefresh(widget.photoId)) {
|
||||
_autoRefreshScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.controller.refreshPreview(widget.photoId);
|
||||
});
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.broken_image_outlined,
|
||||
size: 48,
|
||||
color: Colors.white38,
|
||||
),
|
||||
const SizedBox(height: kSpace8),
|
||||
Text(
|
||||
l10n.photoPreviewUnavailable,
|
||||
style: const TextStyle(color: Colors.white54),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user