From 6d93c1c8dd4e089c56a432244412218df2407e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E9=94=8B?= <2535831261@qq.com> Date: Tue, 30 Jun 2026 16:30:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=AE=89=E5=8D=93=E7=AB=AF?= =?UTF-8?q?=E5=B9=BF=E8=A7=92=E6=97=A0=E6=B3=95=E8=AF=BB=E5=8F=96=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recording/RecordingCameraController.kt | 323 ++++++++++++++++-- .../widgets/widget_recording_hud.dart | 11 +- .../recording/view_model_recording_test.dart | 71 ++-- .../recording/widget_recording_hud_test.dart | 21 +- 4 files changed, 357 insertions(+), 69 deletions(-) diff --git a/android/app/src/main/kotlin/com/run/sportsx/recording/RecordingCameraController.kt b/android/app/src/main/kotlin/com/run/sportsx/recording/RecordingCameraController.kt index 09e770f..5bc2117 100644 --- a/android/app/src/main/kotlin/com/run/sportsx/recording/RecordingCameraController.kt +++ b/android/app/src/main/kotlin/com/run/sportsx/recording/RecordingCameraController.kt @@ -3,8 +3,13 @@ package com.run.sportsx.recording import android.content.Context import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager +import android.hardware.camera2.CaptureRequest +import android.os.Build import android.util.Log +import androidx.camera.camera2.interop.Camera2CameraControl import androidx.camera.camera2.interop.Camera2CameraInfo +import androidx.camera.camera2.interop.CaptureRequestOptions +import androidx.camera.camera2.interop.ExperimentalCamera2Interop import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.Preview @@ -18,8 +23,9 @@ import androidx.camera.video.VideoRecordEvent import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner -import kotlin.math.atan import java.util.concurrent.Executor +import kotlin.math.atan +import kotlin.math.round class RecordingCameraController( private val appContext: Context, @@ -225,15 +231,18 @@ class RecordingCameraController( fun zoomCapabilitiesMap(): Map { val zoomState = camera?.cameraInfo?.zoomState?.value - val logicalMin = zoomState?.minZoomRatio ?: 1f - // 兜底两路超广角来源:独立超广角镜头(0.6) 与 逻辑相机原生 <1.0 变焦范围,取更小者。 + val cameraXMin = zoomState?.minZoomRatio ?: 1f + val cameraXMax = zoomState?.maxZoomRatio ?: 3f + val camera2Range = mainCameraZoomRatioRange() + val logicalMin = camera2Range?.lower?.let { minOf(cameraXMin, it) } ?: cameraXMin + val logicalMax = camera2Range?.upper?.let { maxOf(cameraXMax, it) } ?: cameraXMax val minZoom = if (hasUltraWideCamera()) { minOf(ultraWideZoomRatio, logicalMin) } else { logicalMin } - val maxZoom = zoomState?.maxZoomRatio ?: 3f + val maxZoom = logicalMax val zoom = if (currentLensMode == LensMode.ULTRA_WIDE) { ultraWideZoomRatio @@ -243,7 +252,8 @@ class RecordingCameraController( currentZoomRatio = zoom Log.d( TAG, - "zoomCapabilities hasUltraWide=${hasUltraWideCamera()} logicalMin=$logicalMin " + + "zoomCapabilities hasUltraWide=${hasUltraWideCamera()} cameraXMin=$cameraXMin " + + "cameraXMax=$cameraXMax camera2Range=${camera2Range?.description()} " + "ultraWideZoomRatio=$ultraWideZoomRatio minZoom=$minZoom maxZoom=$maxZoom zoom=$zoom", ) return mapOf( @@ -271,8 +281,11 @@ class RecordingCameraController( } if (ratio < 1.0 && hasUltraWideCamera()) { - switchToUltraWide(onComplete) - return + val logicalRange = mainCameraZoomRatioRange() + if (logicalRange == null || !logicalRange.contains(ratio.toFloat())) { + switchToUltraWide(onComplete) + return + } } if (currentLensMode == LensMode.ULTRA_WIDE) { @@ -281,12 +294,21 @@ class RecordingCameraController( } val zoomState = boundCamera.cameraInfo.zoomState.value - val minZoom = zoomState?.minZoomRatio ?: 1f - val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom() + val camera2Range = mainCameraZoomRatioRange() + val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f + val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom() val nextZoom = ratio.toFloat().coerceIn(minZoom, maxZoom) currentZoomRatio = nextZoom - val future = boundCamera.cameraControl.setZoomRatio(nextZoom) + val future = + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + camera2Range?.contains(nextZoom) == true + ) { + applyCamera2ZoomRatio(boundCamera, nextZoom) + } else { + boundCamera.cameraControl.setZoomRatio(nextZoom) + } future.addListener( { try { @@ -334,10 +356,18 @@ class RecordingCameraController( return } val zoomState = boundCamera.cameraInfo.zoomState.value - val minZoom = zoomState?.minZoomRatio ?: 1f - val maxZoom = zoomState?.maxZoomRatio ?: clampedMaxZoom() + val camera2Range = mainCameraZoomRatioRange() + val minZoom = camera2Range?.lower ?: zoomState?.minZoomRatio ?: 1f + val maxZoom = camera2Range?.upper ?: zoomState?.maxZoomRatio ?: clampedMaxZoom() currentZoomRatio = currentZoomRatio.coerceIn(minZoom, maxZoom) - boundCamera.cameraControl.setZoomRatio(currentZoomRatio) + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + camera2Range?.contains(currentZoomRatio) == true + ) { + applyCamera2ZoomRatio(boundCamera, currentZoomRatio) + } else { + boundCamera.cameraControl.setZoomRatio(currentZoomRatio) + } } private fun clampedMaxZoom(): Float { @@ -345,9 +375,11 @@ class RecordingCameraController( } private fun discoverBackCameras(provider: ProcessCameraProvider) { + val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager if (mainCameraId == null) { mainCameraId = cameraIdForSelector(provider, CameraSelector.DEFAULT_BACK_CAMERA) } + logPublicCameraDiagnostics(provider, manager, mainCameraId) val ultraWideCamera = findUltraWideCamera(provider, mainCameraId) ultraWideCameraId = ultraWideCamera?.cameraId ultraWideZoomRatio = ultraWideCamera?.zoomRatio ?: DEFAULT_ULTRA_WIDE_ZOOM_RATIO @@ -383,6 +415,13 @@ class RecordingCameraController( val candidates = manager.cameraIdList .mapNotNull { cameraId -> backCameraProfile(manager, cameraId) } + .onEach { profile -> + Log.d( + TAG, + "backCamera ${profile.description()} " + + "bindable=${provider.hasCameraSafely(selectorForCameraId(profile.cameraId))}", + ) + } .filter { it.cameraId != excludedCameraId } .filter { provider.hasCameraSafely(selectorForCameraId(it.cameraId)) } .sortedWith( @@ -391,11 +430,16 @@ class RecordingCameraController( ) val mainProfile = excludedCameraId?.let { backCameraProfile(manager, it) } - val widest = candidates.firstOrNull() ?: return null + val widest = + candidates.firstOrNull() + ?: run { + logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId) + return null + } val candidatesDesc = - candidates.joinToString { "id=${it.cameraId} fov=${it.horizontalFov} focal=${it.minFocalLength}" } + candidates.joinToString { it.description() } val mainDesc = - mainProfile?.let { "id=${it.cameraId} fov=${it.horizontalFov} focal=${it.minFocalLength}" } + mainProfile?.description() Log.d(TAG, "ultraWide candidates=[$candidatesDesc] main=$mainDesc") if (mainProfile == null) { return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO) @@ -410,10 +454,98 @@ class RecordingCameraController( "(fovFactor=$ULTRA_WIDE_FOV_FACTOR focalFactor=$ULTRA_WIDE_FOCAL_FACTOR)", ) if (!meaningfullyWider) { + logPhysicalOnlyUltraWideDiagnostics(manager, mainProfile, excludedCameraId) return null } - return UltraWideCamera(widest.cameraId, DEFAULT_ULTRA_WIDE_ZOOM_RATIO) + return UltraWideCamera(widest.cameraId, estimateUltraWideZoomRatio(widest, mainProfile)) + } + + private fun logPublicCameraDiagnostics( + provider: ProcessCameraProvider, + manager: CameraManager, + mainCameraId: String?, + ) { + Log.d(TAG, "publicCameraIds=[${manager.cameraIdList.joinToString()}] mainCameraId=$mainCameraId") + manager.cameraIdList.forEach { cameraId -> + try { + val characteristics = manager.getCameraCharacteristics(cameraId) + val bindable = provider.hasCameraSafely(selectorForCameraId(cameraId)) + val physicalIds = physicalCameraIds(characteristics) + Log.d( + TAG, + "publicCamera id=$cameraId facing=${lensFacingDescription(characteristics)} " + + "physicalIds=[${physicalIds.joinToString()}] " + + "capabilities=[${capabilitiesDescription(characteristics)}] " + + "zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " + + "focals=${focalLengthsDescription(characteristics)} " + + "sensor=${sensorSizeDescription(characteristics)} bindable=$bindable", + ) + if (cameraId == mainCameraId) { + logMainPhysicalCameraDiagnostics(manager, cameraId, physicalIds) + } + } catch (error: Exception) { + Log.w(TAG, "publicCamera diagnostics failed for cameraId=$cameraId", error) + } + } + } + + private fun logMainPhysicalCameraDiagnostics( + manager: CameraManager, + mainCameraId: String, + physicalIds: Set, + ) { + Log.d(TAG, "mainCamera id=$mainCameraId physicalIds=[${physicalIds.joinToString()}]") + physicalIds.forEach { physicalId -> + try { + val characteristics = manager.getCameraCharacteristics(physicalId) + Log.d( + TAG, + "mainPhysicalCamera id=$physicalId facing=${lensFacingDescription(characteristics)} " + + "zoomRange=${zoomRatioRangeFromCharacteristics(characteristics)?.description()} " + + "focals=${focalLengthsDescription(characteristics)} " + + "sensor=${sensorSizeDescription(characteristics)}", + ) + } catch (error: Exception) { + Log.w(TAG, "mainPhysicalCamera diagnostics failed for physicalId=$physicalId", error) + } + } + } + + private fun logPhysicalOnlyUltraWideDiagnostics( + manager: CameraManager, + mainProfile: CameraProfile?, + mainCameraId: String?, + ) { + if (mainProfile == null || mainCameraId == null) { + return + } + val characteristics = + try { + manager.getCameraCharacteristics(mainCameraId) + } catch (error: Exception) { + Log.w(TAG, "physicalOnlyUltraWide diagnostics failed for mainCameraId=$mainCameraId", error) + return + } + val physicalProfiles = + physicalCameraIds(characteristics) + .mapNotNull { physicalId -> backCameraProfile(manager, physicalId) } + .filter { it.cameraId != mainCameraId } + .sortedBy { it.minFocalLength } + val widestPhysical = physicalProfiles.firstOrNull() + if (widestPhysical == null) { + Log.d(TAG, "physicalOnlyUltraWide none main=${mainProfile.description()}") + return + } + val meaningfullyWider = + widestPhysical.horizontalFov > mainProfile.horizontalFov * ULTRA_WIDE_FOV_FACTOR || + widestPhysical.minFocalLength < mainProfile.minFocalLength * ULTRA_WIDE_FOCAL_FACTOR + Log.d( + TAG, + "physicalOnlyUltraWide widest=${widestPhysical.description()} " + + "main=${mainProfile.description()} meaningfullyWider=$meaningfullyWider " + + "exposedAsBindableCamera=false action=diagnostic_only", + ) } private fun backCameraProfile( @@ -435,13 +567,145 @@ class RecordingCameraController( val minFocalLength = focalLengths.minOrNull() ?: return null val horizontalFov = 2.0 * atan((physicalSize.width / (2.0f * minFocalLength)).toDouble()) - CameraProfile(cameraId, minFocalLength, horizontalFov) + CameraProfile( + cameraId, + minFocalLength, + horizontalFov, + zoomRatioRangeFromCharacteristics(characteristics), + ) } catch (error: Exception) { Log.w(TAG, "backCameraProfile failed for cameraId=$cameraId", error) null } } + private fun mainCameraZoomRatioRange(): ZoomRatioRange? { + val cameraId = mainCameraId ?: activeCameraId() + return zoomRatioRangeForCamera(cameraId) + } + + private fun activeCameraId(): String? { + val boundCamera = camera ?: return null + return try { + Camera2CameraInfo.from(boundCamera.cameraInfo).cameraId + } catch (error: Exception) { + null + } + } + + private fun zoomRatioRangeForCamera(cameraId: String?): ZoomRatioRange? { + if (cameraId == null) return null + return try { + val manager = appContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager + zoomRatioRangeFromCharacteristics(manager.getCameraCharacteristics(cameraId)) + } catch (error: Exception) { + Log.w(TAG, "zoomRatioRangeForCamera failed for cameraId=$cameraId", error) + null + } + } + + private fun zoomRatioRangeFromCharacteristics( + characteristics: CameraCharacteristics, + ): ZoomRatioRange? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return null + } + val range = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) + ?: return null + return ZoomRatioRange(range.lower, range.upper) + } + + private fun physicalCameraIds(characteristics: CameraCharacteristics): Set { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return emptySet() + } + return characteristics.physicalCameraIds + } + + private fun lensFacingDescription(characteristics: CameraCharacteristics): String { + return when (val facing = characteristics.get(CameraCharacteristics.LENS_FACING)) { + CameraCharacteristics.LENS_FACING_BACK -> "BACK" + CameraCharacteristics.LENS_FACING_FRONT -> "FRONT" + CameraCharacteristics.LENS_FACING_EXTERNAL -> "EXTERNAL" + null -> "null" + else -> "UNKNOWN($facing)" + } + } + + private fun capabilitiesDescription(characteristics: CameraCharacteristics): String { + val capabilities = + characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) + ?: return "null" + return capabilities.joinToString { capabilityDescription(it) } + } + + private fun capabilityDescription(capability: Int): String { + return when (capability) { + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE -> + "BACKWARD_COMPATIBLE" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR -> "MANUAL_SENSOR" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING -> + "MANUAL_POST_PROCESSING" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW -> "RAW" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING -> + "PRIVATE_REPROCESSING" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS -> + "READ_SENSOR_SETTINGS" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE -> "BURST_CAPTURE" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING -> + "YUV_REPROCESSING" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT -> "DEPTH_OUTPUT" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO -> + "CONSTRAINED_HIGH_SPEED_VIDEO" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING -> "MOTION_TRACKING" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA -> + "LOGICAL_MULTI_CAMERA" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME -> "MONOCHROME" + CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA -> "SECURE_IMAGE_DATA" + else -> "UNKNOWN($capability)" + } + } + + private fun focalLengthsDescription(characteristics: CameraCharacteristics): String { + val focalLengths = + characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) + ?: return "null" + return focalLengths.joinToString(prefix = "[", postfix = "]") + } + + private fun sensorSizeDescription(characteristics: CameraCharacteristics): String { + val size = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) + ?: return "null" + return "${size.width}x${size.height}" + } + + private fun estimateUltraWideZoomRatio( + ultraWide: CameraProfile, + main: CameraProfile, + ): Float { + if (main.minFocalLength <= 0f) { + return DEFAULT_ULTRA_WIDE_ZOOM_RATIO + } + val rawRatio = ultraWide.minFocalLength / main.minFocalLength + val roundedRatio = round(rawRatio * 10f) / 10f + return roundedRatio.coerceIn(MIN_ULTRA_WIDE_ZOOM_RATIO, MAX_ULTRA_WIDE_ZOOM_RATIO) + } + + @androidx.annotation.OptIn(ExperimentalCamera2Interop::class) + private fun applyCamera2ZoomRatio( + boundCamera: Camera, + zoomRatio: Float, + ) = + Camera2CameraControl.from(boundCamera.cameraControl) + .setCaptureRequestOptions( + CaptureRequestOptions.Builder() + .setCaptureRequestOption( + CaptureRequest.CONTROL_ZOOM_RATIO, + zoomRatio, + ) + .build(), + ) + private fun selectorForCurrentLensMode(): CameraSelector { val cameraId = if (currentLensMode == LensMode.ULTRA_WIDE) { @@ -571,7 +835,26 @@ class RecordingCameraController( val cameraId: String, val minFocalLength: Float, val horizontalFov: Double, - ) + val zoomRatioRange: ZoomRatioRange?, + ) { + fun description(): String { + return "id=$cameraId fov=$horizontalFov focal=$minFocalLength " + + "zoomRange=${zoomRatioRange?.description()}" + } + } + + private data class ZoomRatioRange( + val lower: Float, + val upper: Float, + ) { + fun contains(ratio: Float): Boolean { + return ratio >= lower && ratio <= upper + } + + fun description(): String { + return "$lower..$upper" + } + } private data class UltraWideCamera( val cameraId: String, @@ -581,6 +864,8 @@ class RecordingCameraController( companion object { private const val TAG = "RecordingCamera" private const val DEFAULT_ULTRA_WIDE_ZOOM_RATIO = 0.6f + private const val MIN_ULTRA_WIDE_ZOOM_RATIO = 0.3f + private const val MAX_ULTRA_WIDE_ZOOM_RATIO = 0.99f // 适度放宽判定宽容度,覆盖更多机型(更小的 FOV/焦距差异也视为超广角)。 private const val ULTRA_WIDE_FOV_FACTOR = 1.04 private const val ULTRA_WIDE_FOCAL_FACTOR = 0.96 diff --git a/lib/features/recording/widgets/widget_recording_hud.dart b/lib/features/recording/widgets/widget_recording_hud.dart index 10d25af..7159deb 100644 --- a/lib/features/recording/widgets/widget_recording_hud.dart +++ b/lib/features/recording/widgets/widget_recording_hud.dart @@ -56,7 +56,6 @@ class RecordingHudWidget extends StatelessWidget { static double get _recordButtonBottom => 63.r; static double get _overlayInfoLeft => 13.r; static double get _overlayInfoBottom => 10.r; - static const List _zoomPresets = [0.6, 1.0]; @override Widget build(BuildContext context) { @@ -150,7 +149,7 @@ class RecordingHudWidget extends StatelessWidget { zoomRatio: zoomRatio, minZoomRatio: minZoomRatio, maxZoomRatio: maxZoomRatio, - presets: _zoomPresets, + presets: _zoomPresetsForRange(minZoomRatio), onSelected: onZoomSelected, ), ), @@ -191,6 +190,13 @@ class RecordingHudWidget extends StatelessWidget { ], ); } + + List _zoomPresetsForRange(double minZoomRatio) { + return [ + if (minZoomRatio < 1.0) minZoomRatio, + 1.0, + ]; + } } class _ZoomPresetControl extends StatelessWidget { @@ -215,7 +221,6 @@ class _ZoomPresetControl extends StatelessWidget { final availablePresets = presets .where(_isPresetAvailable) .toList(growable: false); - if (availablePresets.isEmpty) { return const SizedBox.shrink(); } diff --git a/test/features/recording/view_model_recording_test.dart b/test/features/recording/view_model_recording_test.dart index 8ac9ad2..58c89e2 100644 --- a/test/features/recording/view_model_recording_test.dart +++ b/test/features/recording/view_model_recording_test.dart @@ -76,45 +76,42 @@ void main() { expect(session.errorMessage, isNull); }); - test( - 'clamps legacy 0.5x request to 0.6x ultra-wide ratio', - () async { - final calls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel(RecordingChannelNames.method), - (call) async { - calls.add(call); - return { - 'zoomRatio': 0.6, - 'minZoomRatio': 0.6, - 'maxZoomRatio': 3.0, - }; - }, - ); - final container = ProviderContainer(); - addTearDown(container.dispose); - final notifier = container.read(recordingViewModelProvider.notifier); - // ignore: invalid_use_of_protected_member - notifier.state = container - .read(recordingViewModelProvider) - .copyWith( - session: const RecordingSessionState( - zoomRatio: 1.0, - minZoomRatio: 0.6, - maxZoomRatio: 3.0, - ), - ); + test('passes 0.5x to native when camera capabilities allow it', () async { + final calls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel(RecordingChannelNames.method), + (call) async { + calls.add(call); + return { + 'zoomRatio': 0.5, + 'minZoomRatio': 0.5, + 'maxZoomRatio': 3.0, + }; + }, + ); + final container = ProviderContainer(); + addTearDown(container.dispose); + final notifier = container.read(recordingViewModelProvider.notifier); + // ignore: invalid_use_of_protected_member + notifier.state = container + .read(recordingViewModelProvider) + .copyWith( + session: const RecordingSessionState( + zoomRatio: 1.0, + minZoomRatio: 0.5, + maxZoomRatio: 3.0, + ), + ); - await notifier.setZoomRatio(0.5); + await notifier.setZoomRatio(0.5); - expect(calls.single.arguments, {'zoomRatio': 0.6}); - final session = container.read(recordingViewModelProvider).session; - expect(session.zoomRatio, 0.6); - expect(session.minZoomRatio, 0.6); - expect(session.maxZoomRatio, 3.0); - }, - ); + expect(calls.single.arguments, {'zoomRatio': 0.5}); + final session = container.read(recordingViewModelProvider).session; + expect(session.zoomRatio, 0.5); + expect(session.minZoomRatio, 0.5); + expect(session.maxZoomRatio, 3.0); + }); test('passes 0.6x to native when camera capabilities allow it', () async { final calls = []; diff --git a/test/features/recording/widget_recording_hud_test.dart b/test/features/recording/widget_recording_hud_test.dart index 382e9bb..8302fd9 100644 --- a/test/features/recording/widget_recording_hud_test.dart +++ b/test/features/recording/widget_recording_hud_test.dart @@ -54,13 +54,13 @@ void main() { expect(find.text('3x'), findsNothing); }); - testWidgets('shows 0.6x when ultra-wide camera capability is below 0.6', ( + testWidgets('shows 0.5x when ultra-wide camera capability is 0.5', ( tester, ) async { await pumpHud(tester, minZoomRatio: 0.5); - expect(find.text('0.5x'), findsNothing); - expect(find.text('0.6x'), findsOneWidget); + expect(find.text('0.5x'), findsOneWidget); + expect(find.text('0.6x'), findsNothing); expect(find.text('1x'), findsOneWidget); expect(find.text('2x'), findsNothing); expect(find.text('3x'), findsNothing); @@ -75,13 +75,13 @@ void main() { expect(find.text('1x'), findsOneWidget); }); - testWidgets('marks current ultra-wide zoom ratio as selected on 0.6x UI', ( + testWidgets('marks current 0.5x zoom ratio as selected', ( tester, ) async { await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5); final selectedButton = tester.widget( - find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)), + find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)), ); expect(selectedButton.enabled, isFalse); }); @@ -98,11 +98,12 @@ void main() { testWidgets('does not expose presets beyond max zoom ratio', (tester) async { await pumpHud(tester, minZoomRatio: 0.5, maxZoomRatio: 0.55); + expect(find.text('0.5x'), findsOneWidget); expect(find.text('0.6x'), findsNothing); expect(find.text('1x'), findsNothing); }); - testWidgets('tapping 0.6x reports 0.6 when camera capability is below 0.6', ( + testWidgets('tapping 0.5x reports 0.5 when camera capability is 0.5', ( tester, ) async { double? selected; @@ -112,10 +113,10 @@ void main() { onZoomSelected: (ratio) => selected = ratio, ); - await tester.tap(find.text('0.6x')); + await tester.tap(find.text('0.5x')); await tester.pump(); - expect(selected, 0.6); + expect(selected, 0.5); }); testWidgets('tapping 0.6x reports 0.6 when camera only supports 0.6x', ( @@ -135,7 +136,7 @@ void main() { }); testWidgets('disables 0.6x while recording on main camera', (tester) async { - await pumpHud(tester, minZoomRatio: 0.5, isRecording: true); + await pumpHud(tester, minZoomRatio: 0.6, isRecording: true); final ultraWideButton = tester.widget( find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)), @@ -154,7 +155,7 @@ void main() { await pumpHud(tester, zoomRatio: 0.5, minZoomRatio: 0.5, isRecording: true); final ultraWideButton = tester.widget( - find.ancestor(of: find.text('0.6x'), matching: find.byType(TextButton)), + find.ancestor(of: find.text('0.5x'), matching: find.byType(TextButton)), ); final mainButton = tester.widget( find.ancestor(of: find.text('1x'), matching: find.byType(TextButton)),