fix(app): camera preview distortion — swap sensor W/H for portrait
previewSize 报横向(W>H),竖屏直用会把画面拉扁。交换宽高转纵向比例再 FittedBox cover 铺满(裁切不变形)。397/397 全绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -62,14 +62,16 @@ class CameraPluginSource extends CaptureSource {
|
||||
throw CameraException('noCamera', 'no camera available');
|
||||
}
|
||||
CameraDescription pick() => cameras.firstWhere(
|
||||
(c) => c.lensDirection ==
|
||||
(preferFront
|
||||
? CameraLensDirection.front
|
||||
: CameraLensDirection.back),
|
||||
orElse: () => cameras.first,
|
||||
);
|
||||
final controller = CameraController(pick(), ResolutionPreset.max,
|
||||
enableAudio: false);
|
||||
(c) =>
|
||||
c.lensDirection ==
|
||||
(preferFront ? CameraLensDirection.front : CameraLensDirection.back),
|
||||
orElse: () => cameras.first,
|
||||
);
|
||||
final controller = CameraController(
|
||||
pick(),
|
||||
ResolutionPreset.max,
|
||||
enableAudio: false,
|
||||
);
|
||||
await controller.initialize();
|
||||
return CameraPluginSource._(controller);
|
||||
}
|
||||
@@ -84,15 +86,17 @@ class CameraPluginSource extends CaptureSource {
|
||||
|
||||
@override
|
||||
Widget buildPreview() {
|
||||
// 纵横比校正(§4 页面 13):预览按传感器尺寸布局,再 cover 铺满——
|
||||
// CameraPreview 直填父约束会把画面拉扁/拉高。
|
||||
// 纵横比校正(§4 页面 13):传感器 previewSize 报的是**横向**(width >
|
||||
// height),竖屏 App 直接用它会把画面拉扁——需交换宽高转为纵向比例,
|
||||
// 再 FittedBox cover 铺满(裁切不变形)。
|
||||
final size = _controller.value.previewSize;
|
||||
Widget preview = CameraPreview(_controller);
|
||||
if (size != null && size.width > 0 && size.height > 0) {
|
||||
preview = FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SizedBox(width: size.width, height: size.height, child: preview),
|
||||
// 交换:竖屏用 height 作宽、width 作高(传感器横向 → 纵向)。
|
||||
child: SizedBox(width: size.height, height: size.width, child: preview),
|
||||
);
|
||||
}
|
||||
return preview;
|
||||
@@ -102,9 +106,8 @@ class CameraPluginSource extends CaptureSource {
|
||||
bool get supportsFlash => true;
|
||||
|
||||
@override
|
||||
Future<void> setFlashEnabled(bool enabled) => _controller.setFlashMode(
|
||||
enabled ? FlashMode.always : FlashMode.off,
|
||||
);
|
||||
Future<void> setFlashEnabled(bool enabled) =>
|
||||
_controller.setFlashMode(enabled ? FlashMode.always : FlashMode.off);
|
||||
|
||||
@override
|
||||
Future<void> dispose() => _controller.dispose();
|
||||
@@ -113,7 +116,8 @@ class CameraPluginSource extends CaptureSource {
|
||||
/// 拍摄源工厂 Provider(测试 override 为替身)。
|
||||
final captureSourceFactoryProvider =
|
||||
Provider<Future<CaptureSource> Function({bool preferFront})>(
|
||||
(ref) => CameraPluginSource.create);
|
||||
(ref) => CameraPluginSource.create,
|
||||
);
|
||||
|
||||
// ── 连拍页 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -207,9 +211,11 @@ class _CapturePageState extends ConsumerState<CapturePage> {
|
||||
});
|
||||
} on Object catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() => _phase = _isPermissionDenied(error)
|
||||
? _CameraPhase.denied
|
||||
: _CameraPhase.failed);
|
||||
setState(
|
||||
() => _phase = _isPermissionDenied(error)
|
||||
? _CameraPhase.denied
|
||||
: _CameraPhase.failed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,23 +253,25 @@ class _CapturePageState extends ConsumerState<CapturePage> {
|
||||
final source = _source;
|
||||
if (source == null || _phase != _CameraPhase.ready) return;
|
||||
final categoryId = _categoryId;
|
||||
_shotTail = _shotTail.then((_) async {
|
||||
final photo = await source.capture();
|
||||
if (!mounted) return;
|
||||
// 最近一张缩略图(拍摄原文件;缺失走占位)。
|
||||
setState(() => _lastShotPath = photo.path);
|
||||
final enqueued = await enqueueForUpload(
|
||||
ref,
|
||||
accountId: widget.target.accountId,
|
||||
albumId: widget.target.albumId,
|
||||
categoryId: categoryId,
|
||||
sourcePaths: [photo.path],
|
||||
);
|
||||
// 入队成功:短暂反馈(轻触 + 200ms 浮层),不逐张弹确认(§4.5)。
|
||||
if (enqueued > 0 && mounted) _flashEnqueued();
|
||||
}).catchError((Object _) {
|
||||
// 单张失败吞掉(连拍不中断;失败任务/错误在徽章与队列页可见)。
|
||||
});
|
||||
_shotTail = _shotTail
|
||||
.then((_) async {
|
||||
final photo = await source.capture();
|
||||
if (!mounted) return;
|
||||
// 最近一张缩略图(拍摄原文件;缺失走占位)。
|
||||
setState(() => _lastShotPath = photo.path);
|
||||
final enqueued = await enqueueForUpload(
|
||||
ref,
|
||||
accountId: widget.target.accountId,
|
||||
albumId: widget.target.albumId,
|
||||
categoryId: categoryId,
|
||||
sourcePaths: [photo.path],
|
||||
);
|
||||
// 入队成功:短暂反馈(轻触 + 200ms 浮层),不逐张弹确认(§4.5)。
|
||||
if (enqueued > 0 && mounted) _flashEnqueued();
|
||||
})
|
||||
.catchError((Object _) {
|
||||
// 单张失败吞掉(连拍不中断;失败任务/错误在徽章与队列页可见)。
|
||||
});
|
||||
}
|
||||
|
||||
/// 入队成功反馈:触觉轻点 + 顶部短暂浮层(200ms 后自动消退)。
|
||||
@@ -488,14 +496,19 @@ class _TopBar extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: MaterialLocalizations.of(context)
|
||||
.backButtonTooltip, // 语义标签;深色界面固定白色前景。
|
||||
tooltip: MaterialLocalizations.of(
|
||||
context,
|
||||
).backButtonTooltip, // 语义标签;深色界面固定白色前景。
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: onBack,
|
||||
),
|
||||
const SizedBox(width: kSpace4),
|
||||
const Icon(Icons.photo_album_outlined, size: 16, color: Colors.white70),
|
||||
const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
size: 16,
|
||||
color: Colors.white70,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -609,10 +622,7 @@ class _Badge extends StatelessWidget {
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: foreground, fontSize: 12),
|
||||
),
|
||||
child: Text(label, style: TextStyle(color: foreground, fontSize: 12)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -711,11 +721,8 @@ class _LastShotButton extends StatelessWidget {
|
||||
content = Image.file(
|
||||
File(lastShotPath!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) => const Icon(
|
||||
Icons.image_outlined,
|
||||
size: 24,
|
||||
color: Colors.white,
|
||||
),
|
||||
errorBuilder: (_, _, _) =>
|
||||
const Icon(Icons.image_outlined, size: 24, color: Colors.white),
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
|
||||
Reference in New Issue
Block a user