feat(app): camera preview WYSIWYG aspect + pinch zoom with double-tap reset
Preview now centers at the sensor aspect ratio (letterboxed on the dark backdrop) instead of FittedBox cover, which cropped ~38% horizontally and made the preview differ from the full-frame takePicture output. Zoom capability hangs off CaptureSource as default implementations (minZoom/maxZoom = 1, setZoom no-op) so existing fakes stay untouched; CameraPluginSource probes getMin/MaxZoomLevel after initialize (failure keeps 1/1 and disables the gesture). The page attaches a scale GestureDetector only when maxZoom > 1: pinch drives setZoom (throttled at 0.05 delta, serialized to prevent out-of-order platform calls), a badge shows the current level and hides after 1s, double-tap resets to 1.0x, and camera flip/reopen resets zoom state. _StaticCaptureSource in album_detail_page_test.dart switches implements -> extends to inherit the new defaults. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -403,8 +403,9 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
/// 相机源替身(详情页跳连拍页测试:仅就绪,不拍摄)。
|
||||
class _StaticCaptureSource implements CaptureSource {
|
||||
/// 相机源替身(详情页跳连拍页测试:仅就绪,不拍摄)。extends 继承
|
||||
/// 接口默认实现(闪光灯/变焦能力默认不支持——UI 不显示开关不启用手势)。
|
||||
class _StaticCaptureSource extends CaptureSource {
|
||||
@override
|
||||
Future<void> initialize() async {}
|
||||
|
||||
@@ -414,12 +415,6 @@ class _StaticCaptureSource implements CaptureSource {
|
||||
@override
|
||||
Widget buildPreview() => const SizedBox();
|
||||
|
||||
@override
|
||||
bool get supportsFlash => false;
|
||||
|
||||
@override
|
||||
Future<void> setFlashEnabled(bool enabled) async {}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
/// 准备器 + 非 Android 权限门替身。覆盖:快门即拍即入队、连拍串行、分类
|
||||
/// 切换仅影响之后拍摄、多选补充入队、待传/失败徽章实时刷新、有待传任务
|
||||
/// 退出确认、权限拒绝/初始化失败视图与重试、深色界面(#10161E)、快门
|
||||
/// ≥72dp、闪光灯按能力显示、前后摄切换重建、最近一张缩略图。
|
||||
/// ≥72dp、闪光灯按能力显示、前后摄切换重建、最近一张缩略图、捏合变焦
|
||||
/// 与双击复位。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
@@ -81,6 +82,23 @@ class FakeCaptureSource extends CaptureSource {
|
||||
Future<void> dispose() async => disposeCalls++;
|
||||
}
|
||||
|
||||
/// 变焦能力替身(Task 7 捏合变焦):能力 1-8×,记录 setZoom 调用序列。
|
||||
/// 继承 [FakeCaptureSource](预览标记/初始化/释放行为一致)。
|
||||
class ZoomFakeCaptureSource extends FakeCaptureSource {
|
||||
ZoomFakeCaptureSource(super.files);
|
||||
|
||||
final List<double> zoomCalls = <double>[];
|
||||
|
||||
@override
|
||||
double get minZoom => 1;
|
||||
|
||||
@override
|
||||
double get maxZoom => 8;
|
||||
|
||||
@override
|
||||
Future<void> setZoom(double level) async => zoomCalls.add(level);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late FakeSessionStore session;
|
||||
late TaskStore store;
|
||||
@@ -147,7 +165,8 @@ void main() {
|
||||
categories: categories,
|
||||
);
|
||||
|
||||
Future<void> pumpCapture(WidgetTester tester) async {
|
||||
Future<void> pumpCapture(WidgetTester tester, {CaptureSource? cameraSource}) async {
|
||||
final effectiveSource = cameraSource ?? source;
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
@@ -164,7 +183,7 @@ void main() {
|
||||
captureSourceFactoryProvider.overrideWith(
|
||||
(ref) => ({bool preferFront = false}) async {
|
||||
factoryPreferFront.add(preferFront);
|
||||
return source;
|
||||
return effectiveSource;
|
||||
},
|
||||
),
|
||||
uploadPreparerProvider.overrideWith(
|
||||
@@ -462,4 +481,56 @@ void main() {
|
||||
expect(find.text('Ceremony'), findsOneWidget);
|
||||
expect(find.text('Gala'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('捏合变焦:双指张开驱动 setZoom 并显示倍数徽标', (tester) async {
|
||||
final zoomSource = ZoomFakeCaptureSource(<File>[]);
|
||||
await pumpCapture(tester, cameraSource: zoomSource);
|
||||
expect(find.text('PREVIEW_MARK'), findsOneWidget, reason: '相机预览就绪');
|
||||
|
||||
final center = tester.getCenter(find.text('PREVIEW_MARK'));
|
||||
// 必须两根指针:ScaleGestureRecognizer 的 scale = 当前指距/初始指距,
|
||||
// 单指针 span=0 → scale 恒为 1.0,永远测不到变焦。
|
||||
final p1 = await tester.startGesture(center.translate(-40, 0));
|
||||
final p2 = await tester.startGesture(center.translate(40, 0));
|
||||
await p1.moveBy(const Offset(-40, 0));
|
||||
await p2.moveBy(const Offset(40, 0));
|
||||
await tester.pump();
|
||||
await p1.up();
|
||||
await p2.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(zoomSource.zoomCalls.last, greaterThan(1.0));
|
||||
expect(find.textContaining('×'), findsOneWidget, reason: '捏合期间显示倍数徽标');
|
||||
|
||||
// 停止 1s 后徽标隐藏;同时冲刷计时器避免悬挂。
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.textContaining('×'), findsNothing, reason: '1s 后徽标隐藏');
|
||||
});
|
||||
|
||||
testWidgets('双击复位 1.0×', (tester) async {
|
||||
final zoomSource = ZoomFakeCaptureSource(<File>[]);
|
||||
await pumpCapture(tester, cameraSource: zoomSource);
|
||||
|
||||
// 先捏合放大到 >1×,再双击复位。
|
||||
final center = tester.getCenter(find.text('PREVIEW_MARK'));
|
||||
final p1 = await tester.startGesture(center.translate(-40, 0));
|
||||
final p2 = await tester.startGesture(center.translate(40, 0));
|
||||
await p1.moveBy(const Offset(-40, 0));
|
||||
await p2.moveBy(const Offset(40, 0));
|
||||
await p1.up();
|
||||
await p2.up();
|
||||
await tester.pump();
|
||||
expect(zoomSource.zoomCalls.last, greaterThan(1.0), reason: '前置:已放大');
|
||||
|
||||
await tester.tap(find.text('PREVIEW_MARK'));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.tap(find.text('PREVIEW_MARK'));
|
||||
await tester.pump();
|
||||
|
||||
expect(zoomSource.zoomCalls.last, 1.0);
|
||||
expect(find.text('1.0×'), findsOneWidget, reason: '复位后徽标显示 1.0×');
|
||||
|
||||
// 冲刷徽标隐藏计时器避免悬挂。
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user