From a55ecae2174758e050ff261a5ee8be3c656d1495 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 15 Oct 2024 14:58:21 -0700 Subject: [PATCH] Elapsed time UI (#270) --- .../core/camera/CameraSession.kt | 15 ++++++++-- .../core/camera/CameraUseCase.kt | 5 +++- .../feature/preview/PreviewUiState.kt | 2 +- .../feature/preview/PreviewViewModel.kt | 3 ++ .../preview/ui/CameraControlsOverlay.kt | 9 ++++++ .../preview/ui/PreviewScreenComponents.kt | 30 ++++++++++++++++++- .../feature/preview/ui/TestTags.kt | 1 + 7 files changed, 59 insertions(+), 6 deletions(-) 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 3759c24d..57d42936 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 @@ -529,13 +529,22 @@ private suspend fun startVideoRecordingInternal( when (onVideoRecordEvent) { is VideoRecordEvent.Finalize -> { when (onVideoRecordEvent.error) { - ERROR_NONE, ERROR_DURATION_LIMIT_REACHED -> + ERROR_NONE -> { onVideoRecord( CameraUseCase.OnVideoRecordEvent.OnVideoRecorded( - onVideoRecordEvent.outputResults.outputUri + onVideoRecordEvent.outputResults.outputUri, + onVideoRecordEvent.recordingStats.recordedDurationNanos ) ) - + } + ERROR_DURATION_LIMIT_REACHED -> { + onVideoRecord( + CameraUseCase.OnVideoRecordEvent.OnVideoRecorded( + onVideoRecordEvent.outputResults.outputUri, + (maxDurationMillis * 1_000_000) // cleanly display the max duration + ) + ) + } else -> onVideoRecord( CameraUseCase.OnVideoRecordEvent.OnVideoRecordError( 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 c29353d6..3d8be744 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 @@ -136,7 +136,10 @@ interface CameraUseCase { * Represents the events for video recording. */ sealed interface OnVideoRecordEvent { - data class OnVideoRecorded(val savedUri: Uri) : OnVideoRecordEvent + data class OnVideoRecorded( + val savedUri: Uri, + val finalDurationNanos: Long + ) : OnVideoRecordEvent data class OnVideoRecordStatus( val audioAmplitude: Double, 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 4c19a342..695cec32 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 @@ -49,7 +49,7 @@ sealed interface PreviewUiState { val isDebugMode: Boolean = false ) : PreviewUiState } - +// todo(kc): add ElapsedTimeUiState class /** * Defines the current state of Video Recording */ 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 d11d9a20..e5c95a9e 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 @@ -164,6 +164,8 @@ class PreviewViewModel @AssistedInject constructor( isDebugMode = isDebugMode, currentLogicalCameraId = cameraState.debugInfo.logicalCameraId, currentPhysicalCameraId = cameraState.debugInfo.physicalCameraId + // TODO(kc): set elapsed time UI state once VideoRecordingState + // refactor is complete. ) } } @@ -645,6 +647,7 @@ class PreviewViewModel @AssistedInject constructor( when (it) { is CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> { Log.d(TAG, "cameraUseCase.startRecording OnVideoRecorded") + timer = it.finalDurationNanos onVideoCapture(VideoCaptureEvent.VideoSaved(it.savedUri)) snackbarToShow = SnackbarData( cookie = cookie, 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 91d817eb..66dfbfa5 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 @@ -246,6 +246,11 @@ private fun ControlsBottom( if (previewUiState.isDebugMode) { CurrentCameraIdText(physicalCameraId, logicalCameraId) } + ElapsedTimeText( + modifier = Modifier.testTag(ELAPSED_TIME_TAG), + videoRecordingState = videoRecordingState, + elapsedNs = previewUiState.recordingElapsedTimeNanos + ) } } @@ -255,6 +260,7 @@ private fun ControlsBottom( .height(IntrinsicSize.Max), verticalAlignment = Alignment.CenterVertically ) { + // Row that holds flip camera, capture button, and audio Row(Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceEvenly) { if (!isQuickSettingsOpen && videoRecordingState == VideoRecordingState.INACTIVE) { FlipCameraButton( @@ -516,6 +522,7 @@ private fun Preview_ControlsBottom() { systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.INACTIVE, audioAmplitude = 0.0 + ) } } @@ -538,6 +545,7 @@ private fun Preview_ControlsBottom_NoZoomLevel() { systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, videoRecordingState = VideoRecordingState.INACTIVE, audioAmplitude = 0.0 + ) } } @@ -588,6 +596,7 @@ private fun Preview_ControlsBottom_NoFlippableCamera() { ), videoRecordingState = VideoRecordingState.INACTIVE, audioAmplitude = 0.0 + ) } } 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 8382af48..386d50a3 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 @@ -25,10 +25,13 @@ import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.SurfaceRequest import androidx.camera.viewfinder.compose.MutableCoordinateTransformer import androidx.camera.viewfinder.surface.ImplementationMode +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseOutExpo import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -93,6 +96,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -103,6 +107,7 @@ import com.google.jetpackcamera.feature.preview.ui.theme.PreviewPreviewTheme import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.LowLightBoost import com.google.jetpackcamera.settings.model.Stabilization +import kotlin.time.Duration.Companion.nanoseconds import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -112,6 +117,27 @@ import kotlinx.coroutines.flow.onCompletion private const val TAG = "PreviewScreen" private const val BLINK_TIME = 100L +@Composable +fun ElapsedTimeText( + modifier: Modifier = Modifier, + videoRecordingState: VideoRecordingState, + elapsedNs: Long +) { + AnimatedVisibility( + visible = (videoRecordingState == VideoRecordingState.ACTIVE), + enter = fadeIn(), + exit = fadeOut(animationSpec = tween(delayMillis = 1000)) + ) { + Text( + modifier = modifier, + text = elapsedNs.nanoseconds.toComponents { minutes, seconds, _ -> + "%02d:%02d".format(minutes, seconds) + }, + textAlign = TextAlign.Center + ) + } +} + @Composable fun AmplitudeVisualizer( modifier: Modifier = Modifier, @@ -439,6 +465,7 @@ fun LowLightBoostIcon(lowLightBoost: LowLightBoost, modifier: Modifier = Modifie modifier = modifier.alpha(0.5f) ) } + LowLightBoost.DISABLED -> { } } @@ -625,7 +652,8 @@ fun ToggleButton( onToggleWhenDisabled() } } - ).semantics { + ) + .semantics { stateDescription = when (toggleState) { ToggleState.Left -> leftIconDescription ToggleState.Right -> rightIconDescription diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt index dcea4ec6..8dc20ba1 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt @@ -40,3 +40,4 @@ const val HDR_VIDEO_UNSUPPORTED_ON_LENS_TAG = "HdrVideoUnsupportedOnDeviceTag" const val ZOOM_RATIO_TAG = "ZoomRatioTag" const val LOGICAL_CAMERA_ID_TAG = "LogicalCameraIdTag" const val PHYSICAL_CAMERA_ID_TAG = "PhysicalCameraIdTag" +const val ELAPSED_TIME_TAG = "ElapsedTimeTag"