diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index c13359583..37da3a5f6 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -68,6 +68,7 @@ import androidx.core.content.ContextCompat.checkSelfPermission import androidx.lifecycle.asFlow import com.google.jetpackcamera.core.camera.effects.SingleSurfaceForcingEffect import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode @@ -98,9 +99,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -117,7 +120,8 @@ internal suspend fun runSingleCameraSession( sessionSettings: PerpetualSessionSettings.SingleCamera, useCaseMode: CameraUseCase.UseCaseMode, // TODO(tm): ImageCapture should go through an event channel like VideoCapture - onImageCaptureCreated: (ImageCapture) -> Unit = {} + onImageCaptureCreated: (ImageCapture) -> Unit = {}, + onSetZoomRatioMap: (Map) -> Unit = { _ -> } ) = coroutineScope { Log.d(TAG, "Starting new single camera session") @@ -215,6 +219,85 @@ internal suspend fun runSingleCameraSession( } } + // update camerastate to mirror current zoomstate + launch { + /* + TODO bug?? Flaky behavior here. does not always update zoomstate properly when: + switching HDR on or off on front lens + Setting aspect ratio on either lens... + basically anything that restarts the session + */ + camera.cameraInfo.zoomState.asFlow() + .filterNotNull() + .distinctUntilChanged() + .onCompletion { + // reset current camera state when changing cameras. + currentCameraState.update { old -> + old.copy( + zoomRatios = emptyMap(), + linearZoomScales = emptyMap() + ) + } + } + .collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }.toMap(), + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.linearZoom) + }.toMap() + ) + } + // update current settings to mirror current camera state + onSetZoomRatioMap( + currentCameraState.value.zoomRatios + ) + } + } + + launch { + // Immediately Apply camera zoom from current settings when opening a new camera + camera.cameraControl.setZoomRatio( + currentTransientSettings.zoomRatios[camera.cameraInfo.appLensFacing] ?: 1f + ) + Log.d( + TAG, + "Starting camera ${camera.cameraInfo.appLensFacing} at zoom ratio " + + "${camera.cameraInfo.zoomState.value?.zoomRatio}" + ) + + // Apply zoom changes to camera + zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> + val currentZoomState = camera.cameraInfo.zoomState + .asFlow().filterNotNull().first() + when (zoomChange) { + is CameraZoomState.Ratio -> { + camera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + ) + } + + is CameraZoomState.Linear -> { + camera.cameraControl.setLinearZoom(zoomChange.value) + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + camera.cameraControl.setZoomRatio(newRatio) + } + } + } + } + applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( camera, @@ -252,21 +335,6 @@ internal suspend fun processTransientSettingEvents( val newTransientSettings = it.first val cameraState = it.second - // Apply camera zoom - if (prevTransientSettings.zoomScale != newTransientSettings.zoomScale) { - camera.cameraInfo.zoomState.value?.let { zoomState -> - val finalScale = - (zoomState.zoomRatio * newTransientSettings.zoomScale).coerceIn( - zoomState.minZoomRatio, - zoomState.maxZoomRatio - ) - camera.cameraControl.setZoomRatio(finalScale) - currentCameraState.update { old -> - old.copy(zoomScale = finalScale) - } - } - } - // todo(): How should we handle torch on Auto FlashMode? // enable torch only while recording is in progress if ((cameraState.videoRecordingState !is VideoRecordingState.Inactive) && @@ -414,13 +482,12 @@ internal fun createUseCaseGroup( }.build() } -private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality { - return resolution?.let { res -> +private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality = + resolution?.let { res -> QUALITY_RANGE_MAP.firstNotNullOfOrNull { if (it.value.contains(res.height)) it.key else null } } ?: VideoQuality.UNSPECIFIED -} private fun getWidthFromCropRect(cropRect: Rect?): Int { if (cropRect == null) { @@ -698,7 +765,8 @@ private suspend fun startVideoRecordingInternal( context: Context, pendingRecord: PendingRecording, maxDurationMillis: Long, - onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit + onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, + onRestoreSettings: () -> Unit = {} ): Recording { Log.d(TAG, "recordVideo") // todo(b/336886716): default setting to enable or disable audio when permission is granted @@ -819,6 +887,7 @@ private suspend fun startVideoRecordingInternal( onVideoRecordEvent.outputResults.outputUri ) ) + onRestoreSettings() } ERROR_DURATION_LIMIT_REACHED -> { @@ -874,7 +943,8 @@ private suspend fun runVideoRecording( videoCaptureUri: Uri?, videoControlEvents: Channel, shouldUseUri: Boolean, - onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit + onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, + onRestoreSettings: () -> Unit = {} ) = coroutineScope { var currentSettings = transientSettings.filterNotNull().first() @@ -892,7 +962,8 @@ private suspend fun runVideoRecording( context = context, pendingRecord = it, maxDurationMillis = maxDurationMillis, - onVideoRecord = onVideoRecord + onVideoRecord = onVideoRecord, + onRestoreSettings = onRestoreSettings ).use { recording -> val recordingSettingsUpdater = launch { fun TransientSessionSettings.isFlashModeOn() = flashMode == FlashMode.ON @@ -975,7 +1046,8 @@ internal suspend fun processVideoControlEvents( event.videoCaptureUri, videoCaptureControlEvents, event.shouldUseUri, - event.onVideoRecord + event.onVideoRecord, + event.onRestoreSettings ) } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt index 1425bbbb8..a2e219136 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt @@ -18,6 +18,7 @@ package com.google.jetpackcamera.core.camera import android.content.Context import androidx.camera.core.SurfaceRequest import androidx.camera.lifecycle.ProcessCameraProvider +import com.google.jetpackcamera.settings.model.CameraZoomState import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel @@ -37,6 +38,7 @@ internal data class CameraSessionContext( val screenFlashEvents: SendChannel, val focusMeteringEvents: Channel, val videoCaptureControlEvents: Channel, + val zoomChanges: StateFlow, val currentCameraState: MutableStateFlow, val surfaceRequests: MutableStateFlow, val transientSettings: StateFlow diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt index 898ab5d24..7aad0fe53 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt @@ -63,6 +63,6 @@ internal data class TransientSessionSettings( val isAudioEnabled: Boolean, val deviceRotation: DeviceRotation, val flashMode: FlashMode, - val zoomScale: Float, - val primaryLensFacing: LensFacing + val primaryLensFacing: LensFacing, + val zoomRatios: Map ) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt index 1a5558c14..bfdc16b57 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt @@ -21,6 +21,7 @@ import androidx.camera.core.ImageCapture import androidx.camera.core.SurfaceRequest import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange @@ -86,7 +87,7 @@ interface CameraUseCase { suspend fun stopVideoRecording() - fun setZoomScale(scale: Float) + fun changeZoom(newZoomState: CameraZoomState) fun getCurrentCameraState(): StateFlow @@ -190,7 +191,8 @@ sealed interface VideoRecordingState { data class CameraState( val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), - val zoomScale: Float = 1f, + val zoomRatios: Map = mapOf(), + val linearZoomScales: Map = mapOf(), val sessionFirstFrameTimestamp: Long = 0L, val torchEnabled: Boolean = false, val stabilizationMode: StabilizationMode = StabilizationMode.OFF, diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index 050da3f9d..fadc9734c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -43,6 +43,7 @@ import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_15 import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_60 +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange @@ -111,9 +112,12 @@ constructor( private val currentSettings = MutableStateFlow(null) + // todo: zoomchanges init with cameraappsettings zoomratio + private val zoomChanges = MutableStateFlow(null) + // Could be improved by setting initial value only when camera is initialized - private var _currentCameraState = MutableStateFlow(CameraState()) - override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() + private var currentCameraState = MutableStateFlow(CameraState()) + override fun getCurrentCameraState(): StateFlow = currentCameraState.asStateFlow() private val _surfaceRequest = MutableStateFlow(null) @@ -294,7 +298,7 @@ constructor( deviceRotation = currentCameraSettings.deviceRotation, flashMode = currentCameraSettings.flashMode, primaryLensFacing = currentCameraSettings.cameraLensFacing, - zoomScale = currentCameraSettings.zoomScale + zoomRatios = currentCameraSettings.defaultZoomRatios ) when (currentCameraSettings.concurrentCameraMode) { @@ -360,7 +364,8 @@ constructor( screenFlashEvents = screenFlashEvents, focusMeteringEvents = focusMeteringEvents, videoCaptureControlEvents = videoCaptureControlEvents, - currentCameraState = _currentCameraState, + zoomChanges = zoomChanges.asStateFlow(), + currentCameraState = currentCameraState, surfaceRequests = _surfaceRequest, transientSettings = transientSettings ) @@ -370,6 +375,16 @@ constructor( is PerpetualSessionSettings.SingleCamera -> runSingleCameraSession( sessionSettings, useCaseMode = useCaseMode, + onSetZoomRatioMap = { newZoomRatios -> + currentSettings.update { old -> + old?.copy( + defaultZoomRatios = + old.defaultZoomRatios.toMutableMap().apply { + putAll(newZoomRatios) + } + ) + } + }, onImageCaptureCreated = { imageCapture -> imageCaptureUseCase = imageCapture } @@ -378,6 +393,16 @@ constructor( is PerpetualSessionSettings.ConcurrentCamera -> runConcurrentCameraSession( sessionSettings, + onSetZoomRatioMap = { newZoomRatios -> + currentSettings.update { old -> + old?.copy( + defaultZoomRatios = + old.defaultZoomRatios.toMutableMap().apply { + putAll(newZoomRatios) + } + ) + } + }, useCaseMode = CameraUseCase.UseCaseMode.VIDEO_ONLY ) } @@ -523,6 +548,7 @@ constructor( shouldUseUri: Boolean, onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit ) { + val initialRecordSettings = currentSettings.value if (shouldUseUri && videoCaptureUri == null) { val e = RuntimeException("Null Uri is provided.") Log.d(TAG, "takePicture onError: $e") @@ -534,7 +560,30 @@ constructor( shouldUseUri, currentSettings.value?.maxVideoDurationMillis ?: UNLIMITED_VIDEO_DURATION, - onVideoRecord + onVideoRecord = onVideoRecord, + + onRestoreSettings = { + // restore settings to be called after video recording completes. + // this resets certain settings to their values pre-recording + initialRecordSettings?.let { + currentSettings.update { old -> + old?.copy( + cameraLensFacing = initialRecordSettings.cameraLensFacing, + defaultZoomRatios = initialRecordSettings.defaultZoomRatios + ) + } + + // if the lens doesnt flip when restoring settings, the zoom will need to be + // manually reapplied since zoom changes are processed through state changes to + // zoomchanges and not settings + zoomChanges.update { + CameraZoomState.Ratio( + initialRecordSettings + .defaultZoomRatios[initialRecordSettings.cameraLensFacing] ?: 1f + ) + } + } + } ) ) } @@ -551,9 +600,9 @@ constructor( videoCaptureControlEvents.send(VideoCaptureControlEvent.StopRecordingEvent) } - override fun setZoomScale(scale: Float) { - currentSettings.update { old -> - old?.copy(zoomScale = scale) + override fun changeZoom(newZoomState: CameraZoomState) { + zoomChanges.update { + newZoomState } } @@ -660,8 +709,8 @@ constructor( } } - private fun CameraAppSettings.tryApplyVideoQualityConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyVideoQualityConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedVideoQualitiesMap) { val newVideoQuality = get(dynamicRange).let { if (it == null) { @@ -678,7 +727,6 @@ constructor( ) } } ?: this - } private fun CameraAppSettings.tryApplyFlashModeConstraints(): CameraAppSettings = systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index c38235f81..24bb3d133 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -19,13 +19,18 @@ import android.annotation.SuppressLint import android.util.Log import androidx.camera.core.CompositionSettings import androidx.camera.core.TorchState +import androidx.concurrent.futures.await import androidx.lifecycle.asFlow +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.ImageOutputFormat +import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.StabilizationMode import com.google.jetpackcamera.settings.model.VideoQuality import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update @@ -37,6 +42,7 @@ context(CameraSessionContext) @SuppressLint("RestrictedApi") internal suspend fun runConcurrentCameraSession( sessionSettings: PerpetualSessionSettings.ConcurrentCamera, + onSetZoomRatioMap: (Map) -> Unit = { _ -> }, useCaseMode: CameraUseCase.UseCaseMode ) = coroutineScope { val primaryLensFacing = sessionSettings.primaryCameraInfo.appLensFacing @@ -99,6 +105,7 @@ internal suspend fun runConcurrentCameraSession( cameraProvider.runWithConcurrent(cameraConfigs, useCaseGroup) { concurrentCamera -> Log.d(TAG, "Concurrent camera session started") + // todo: bug?? concurrent camera only ever lists one camera val primaryCamera = concurrentCamera.cameras.first { it.cameraInfo.appLensFacing == sessionSettings.primaryCameraInfo.appLensFacing } @@ -122,6 +129,70 @@ internal suspend fun runConcurrentCameraSession( } } + // update cameraState to mirror the current zoomState + launch { + // todo bug? why isn't this catching the initial setZoomRatio? the camerastate zoom is not updating properly + primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().distinctUntilChanged() + .collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }.toMap(), + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.linearZoom) + }.toMap() + ) + } + // update current settings to mirror current camera state + onSetZoomRatioMap( + currentCameraState.value.zoomRatios + ) + } + } + + launch { + // Immediately Apply camera zoom from current settings when opening a new camera + primaryCamera.cameraControl.setZoomRatio( + initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f + ).await() + + // todo: what is happening after the first setZoomRatio? The initial setzoom applies but is not reflected in cameraInfo.ZoomState? + // the only ways this works is to call it twice for some reason... somethings going wrong somewhere + primaryCamera.cameraControl.setZoomRatio( + initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f + ).await() + zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> + val currentZoomState = primaryCamera.cameraInfo.zoomState + .asFlow() + .filterNotNull() + .first() + when (zoomChange) { + is CameraZoomState.Ratio -> { + primaryCamera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + ) + } + + is CameraZoomState.Linear -> { + primaryCamera.cameraControl.setLinearZoom(zoomChange.value) + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + primaryCamera.cameraControl.setZoomRatio(newRatio) + } + } + } + } + applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( primaryCamera, diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt index 1132d8e2f..cdd35a2a4 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt @@ -31,6 +31,7 @@ sealed interface VideoCaptureControlEvent { val videoCaptureUri: Uri?, val shouldUseUri: Boolean, val maxVideoDuration: Long, + val onRestoreSettings: () -> Unit, val onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit ) : VideoCaptureControlEvent diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt index b6c5fa702..3c4e1c790 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt @@ -24,6 +24,7 @@ import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.core.camera.CameraUseCase import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange @@ -58,7 +59,7 @@ class FakeCameraUseCase(defaultCameraSettings: CameraAppSettings = CameraAppSett private var isScreenFlash = true private var screenFlashEvents = Channel(capacity = UNLIMITED) - + private val zoomChanges = MutableStateFlow(null) private val currentSettings = MutableStateFlow(defaultCameraSettings) override suspend fun initialize( @@ -150,10 +151,8 @@ class FakeCameraUseCase(defaultCameraSettings: CameraAppSettings = CameraAppSett } private val _currentCameraState = MutableStateFlow(CameraState()) - override fun setZoomScale(scale: Float) { - currentSettings.update { old -> - old.copy(zoomScale = scale) - } + override fun changeZoom(newZoomState: CameraZoomState) { + zoomChanges.update { newZoomState } } override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index 892e0d752..5ac5851dd 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -32,7 +32,7 @@ data class CameraAppSettings( val stabilizationMode: StabilizationMode = StabilizationMode.AUTO, val dynamicRange: DynamicRange = DynamicRange.SDR, val videoQuality: VideoQuality = VideoQuality.UNSPECIFIED, - val zoomScale: Float = 1f, + val defaultZoomRatios: Map = mapOf(), val targetFrameRate: Int = TARGET_FPS_AUTO, val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG, val audioEnabled: Boolean = true, diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt new file mode 100644 index 000000000..e26a8dc02 --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.settings.model + +interface CameraZoomState { + val value: Float + data class Scale(override val value: Float) : CameraZoomState + data class Linear(override val value: Float) : CameraZoomState + data class Ratio(override val value: Float) : CameraZoomState +} diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index d94cfe4e2..7d80658a2 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -62,6 +62,7 @@ import com.google.jetpackcamera.feature.preview.ui.ZoomLevelDisplayState import com.google.jetpackcamera.feature.preview.ui.debouncedOrientationFlow import com.google.jetpackcamera.feature.preview.ui.debug.DebugOverlayComponent import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DynamicRange @@ -140,7 +141,7 @@ fun PreviewScreen( onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, onSetLensFacing = viewModel::setLensFacing, onTapToFocus = viewModel::tapToFocus, - onChangeZoomScale = viewModel::setZoomScale, + onChangeZoomScale = viewModel::setZoom, onChangeFlash = viewModel::setFlash, onChangeAspectRatio = viewModel::setAspectRatio, onChangeCaptureMode = viewModel::setCaptureMode, @@ -175,7 +176,7 @@ private fun ContentScreen( onClearUiScreenBrightness: (Float) -> Unit = {}, onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {}, onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> }, - onChangeZoomScale: (Float) -> Unit = {}, + onChangeZoomScale: (CameraZoomState) -> Unit = {}, onChangeFlash: (FlashMode) -> Unit = {}, onChangeAspectRatio: (AspectRatio) -> Unit = {}, onChangeCaptureMode: (StreamConfig) -> Unit = {}, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt index 33817e970..baea6a233 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt @@ -21,6 +21,7 @@ import com.google.jetpackcamera.feature.preview.ui.SnackbarData import com.google.jetpackcamera.feature.preview.ui.ToastMessage import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.FlashMode +import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.StabilizationMode import com.google.jetpackcamera.settings.model.SystemConstraints import com.google.jetpackcamera.settings.model.VideoQuality @@ -35,7 +36,7 @@ sealed interface PreviewUiState { // "quick" settings val currentCameraSettings: CameraAppSettings = CameraAppSettings(), val systemConstraints: SystemConstraints = SystemConstraints(), - val zoomScale: Float = 1f, + val zoomRatios: Map = mapOf(), val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), val quickSettingsIsOpen: Boolean = false, // val audioMuted: Boolean = false, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 59ab52fe6..cd08a6fcb 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -41,6 +41,7 @@ import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange @@ -180,7 +181,7 @@ class PreviewViewModel @AssistedInject constructor( previewMode = previewMode, currentCameraSettings = cameraAppSettings, systemConstraints = systemConstraints, - zoomScale = cameraState.zoomScale, + zoomRatios = cameraState.zoomRatios, videoRecordingState = cameraState.videoRecordingState, sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, captureModeToggleUiState = getCaptureToggleUiState( @@ -836,8 +837,8 @@ class PreviewViewModel @AssistedInject constructor( } } - fun setZoomScale(scale: Float) { - cameraUseCase.setZoomScale(scale = scale) + fun setZoom(newZoomState: CameraZoomState) { + cameraUseCase.changeZoom(newZoomState = newZoomState) } fun setDynamicRange(dynamicRange: DynamicRange) { diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt index 80cc89cd5..896e25dda 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt @@ -118,7 +118,7 @@ fun CameraControlsOverlay( ) { // Show the current zoom level for a short period of time, only when the level changes. var firstRun by remember { mutableStateOf(true) } - LaunchedEffect(previewUiState.zoomScale) { + LaunchedEffect(previewUiState.zoomRatios) { if (firstRun) { firstRun = false } else { @@ -154,7 +154,7 @@ fun CameraControlsOverlay( .fillMaxWidth() .align(Alignment.BottomCenter), previewUiState = previewUiState, - zoomLevel = previewUiState.zoomScale, + zoomRatios = previewUiState.zoomRatios, physicalCameraId = previewUiState.currentPhysicalCameraId, logicalCameraId = previewUiState.currentLogicalCameraId, showZoomLevel = zoomLevelDisplayState.showZoomLevel, @@ -248,7 +248,7 @@ private fun ControlsBottom( previewUiState: PreviewUiState.Ready, physicalCameraId: String? = null, logicalCameraId: String? = null, - zoomLevel: Float, + zoomRatios: Map = mapOf(), showZoomLevel: Boolean, isQuickSettingsOpen: Boolean, systemConstraints: SystemConstraints, @@ -278,7 +278,11 @@ private fun ControlsBottom( ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { if (showZoomLevel) { - ZoomScaleText(zoomLevel) + zoomRatios[previewUiState.currentCameraSettings.cameraLensFacing]?.let { + zoomRatio + -> + ZoomRatioText(zoomRatio) + } } if (previewUiState.debugUiState.isDebugMode) { CurrentCameraIdText(physicalCameraId, logicalCameraId) @@ -596,7 +600,7 @@ private fun Preview_ControlsBottom() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -617,7 +621,7 @@ private fun Preview_ControlsBottom_NoZoomLevel() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = false, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -638,7 +642,7 @@ private fun Preview_ControlsBottom_QuickSettingsOpen() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = true, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -659,7 +663,7 @@ private fun Preview_ControlsBottom_NoFlippableCamera() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS.copy( @@ -687,7 +691,7 @@ private fun Preview_ControlsBottom_Recording() { videoRecordingState = VideoRecordingState.Active.Recording(0L, .9, 1_000_000_000) ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt index be7fb2441..926fc675b 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt @@ -110,6 +110,7 @@ import com.google.jetpackcamera.feature.preview.R import com.google.jetpackcamera.feature.preview.StabilizationUiState import com.google.jetpackcamera.feature.preview.ui.theme.PreviewPreviewTheme import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.StabilizationMode import com.google.jetpackcamera.settings.model.VideoQuality import kotlin.time.Duration.Companion.nanoseconds @@ -378,15 +379,15 @@ fun PreviewDisplay( previewUiState: PreviewUiState.Ready, onTapToFocus: (x: Float, y: Float) -> Unit, onFlipCamera: () -> Unit, - onZoomChange: (Float) -> Unit, + onZoomChange: (CameraZoomState) -> Unit, onRequestWindowColorMode: (Int) -> Unit, aspectRatio: AspectRatio, surfaceRequest: SurfaceRequest?, modifier: Modifier = Modifier ) { val transformableState = rememberTransformableState( - onTransformation = { zoomChange, _, _ -> - onZoomChange(zoomChange) + onTransformation = { pinchZoomChange, _, _ -> + onZoomChange(CameraZoomState.Scale(pinchZoomChange)) } ) @@ -639,7 +640,7 @@ fun SettingsNavButton(onNavigateToSettings: () -> Unit, modifier: Modifier = Mod } @Composable -fun ZoomScaleText(zoomScale: Float) { +fun ZoomRatioText(zoomRatio: Float) { val contentAlpha = animateFloatAsState( targetValue = 10f, label = "zoomScaleAlphaAnimation", @@ -649,7 +650,7 @@ fun ZoomScaleText(zoomScale: Float) { modifier = Modifier .alpha(contentAlpha.value) .testTag(ZOOM_RATIO_TAG), - text = stringResource(id = R.string.zoom_scale_text, zoomScale) + text = stringResource(id = R.string.zoom_scale_text, zoomRatio) ) } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt index 47ed19aab..45dc94e87 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt @@ -34,10 +34,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -53,6 +51,7 @@ import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SET_ZOOM_RATIO_ import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SHOW_CAMERA_PROPERTIES_BUTTON import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_VIDEO_RESOLUTION_TAG +import com.google.jetpackcamera.settings.model.CameraZoomState import kotlin.math.abs private const val TAG = "DebugOverlayComponents" @@ -67,7 +66,7 @@ fun DebugOverlayToggleButton(modifier: Modifier = Modifier, toggleIsOpen: () -> @Composable fun DebugOverlayComponent( modifier: Modifier = Modifier, - onChangeZoomScale: (Float) -> Unit, + onChangeZoomScale: (CameraZoomState) -> Unit, toggleIsOpen: () -> Unit, previewUiState: PreviewUiState.Ready ) { @@ -154,7 +153,7 @@ fun DebugOverlayComponent( // Set zoom ratio if (zoomRatioDialog.value) { - SetZoomRatioComponent(previewUiState, onChangeZoomScale) { + SetZoomRatioComponent(onChangeZoomScale) { zoomRatioDialog.value = false } } @@ -185,8 +184,7 @@ private fun CameraPropertiesJSONComponent( @Composable private fun SetZoomRatioComponent( - previewUiState: PreviewUiState.Ready, - onChangeZoomScale: (Float) -> Unit, + onChangeZoomRatio: (CameraZoomState.Ratio) -> Unit, onClose: () -> Unit ) { var zoomRatioText = remember { mutableStateOf("") } @@ -211,14 +209,12 @@ private fun SetZoomRatioComponent( ), onClick = { try { - val relativeRatio = if (zoomRatioText.value.isEmpty()) { + val newRatio = if (zoomRatioText.value.isEmpty()) { 1f } else { zoomRatioText.value.toFloat() } - val currentRatio = previewUiState.zoomScale - val absoluteRatio = relativeRatio / currentRatio - onChangeZoomScale(absoluteRatio) + onChangeZoomRatio(CameraZoomState.Ratio(newRatio)) } catch (e: NumberFormatException) { Log.d(TAG, "Zoom ratio should be a float") } diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt index c81f154ee..e7f408b8a 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt @@ -104,8 +104,7 @@ sealed interface DisabledRationale { data class VideoQualityUnsupportedRationale( override val affectedSettingNameResId: Int, val currentDynamicRange: Int = R.string.video_quality_rationale_suffix_default - ) : - DisabledRationale { + ) : DisabledRationale { override val reasonTextResId = R.string.video_quality_unsupported override val testTag = VIDEO_QUALITY_UNSUPPORTED_TAG }