Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ import com.google.jetpackcamera.ui.components.capture.TestableSnackbar
import com.google.jetpackcamera.ui.components.capture.VIDEO_QUALITY_TAG
import com.google.jetpackcamera.ui.components.capture.VideoQualityIcon
import com.google.jetpackcamera.ui.components.capture.ZoomButtonRow
import com.google.jetpackcamera.ui.components.capture.ZoomState
import com.google.jetpackcamera.ui.components.capture.ZoomStateManager
import com.google.jetpackcamera.ui.components.capture.debouncedOrientationFlow
import com.google.jetpackcamera.ui.components.capture.debug.DebugOverlay
import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsBottomSheet
Expand Down Expand Up @@ -201,12 +201,12 @@ fun PreviewScreen(
debouncedOrientationFlow(context).collect(viewModel::setDisplayRotation)
}
val scope = rememberCoroutineScope()
val zoomState = remember {
val zoomStateManager = remember {
// the initialZoomLevel must be fetched from the settings, not the cameraState.
// since we want to reset the ZoomState on flip, the zoomstate of the cameraState
// may not yet be congruent with the settings

ZoomState(
ZoomStateManager(
initialZoomLevel = (
currentUiState.zoomControlUiState as?
ZoomControlUiState.Enabled
Expand All @@ -225,7 +225,7 @@ fun PreviewScreen(
(currentUiState.flipLensUiState as? FlipLensUiState.Available)
?.selectedLensFacing
) {
zoomState.onChangeLens(
zoomStateManager.onChangeLens(
newInitialZoomLevel = (
currentUiState.zoomControlUiState as?
ZoomControlUiState.Enabled
Expand Down Expand Up @@ -253,7 +253,7 @@ fun PreviewScreen(
Log.d(TAG, "reset pre recording settings")
viewModel.setAudioEnabled(oldAudioEnabled)
viewModel.setLensFacing(oldPrimaryLensFacing)
zoomState.apply {
zoomStateManager.apply {
absoluteZoom(
targetZoomLevel = oldZoomRatios[oldPrimaryLensFacing] ?: 1f,
lensToZoom = LensToZoom.PRIMARY
Expand Down Expand Up @@ -286,31 +286,31 @@ fun PreviewScreen(
onSetImageWell = viewModel::imageWellToRepository,
onAbsoluteZoom = { zoomRatio: Float, lensToZoom: LensToZoom ->
scope.launch {
zoomState.absoluteZoom(
zoomStateManager.absoluteZoom(
zoomRatio,
lensToZoom
)
}
},
onScaleZoom = { zoomRatio: Float, lensToZoom: LensToZoom ->
scope.launch {
zoomState.scaleZoom(
zoomStateManager.scaleZoom(
zoomRatio,
lensToZoom
)
}
},
onAnimateZoom = { zoomRatio: Float, lensToZoom: LensToZoom ->
scope.launch {
zoomState.animatedZoom(
zoomStateManager.animatedZoom(
targetZoomLevel = zoomRatio,
lensToZoom = lensToZoom
)
}
},
onIncrementZoom = { zoomRatio: Float, lensToZoom: LensToZoom ->
scope.launch {
zoomState.incrementZoom(
zoomStateManager.incrementZoom(
zoomRatio,
lensToZoom
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ private fun CaptureKeyHandler(
}
}

/**
* A capture button that can be used for both image and video capture, supporting drag-to-lock for hands-free recording.
*
* This component handles user interactions such as tap for image capture, long-press for video recording,
* and a drag-to-lock gesture to enable continuous, "hands-free recording." When a video recording
* is initiated via a long press, the user can drag their finger towards the lock icon to lock the recording,
* allowing them to lift their finger and continue recording without interruption. Additionally, users can
* drag vertically *above* the capture button (where Y coordinates are negative relative to the button's top edge)
* to zoom in (dragging upwards) or zoom out (dragging downwards).
*
* The button supports three distinct capture modes: Hybrid, Image-only, and Video-only, each with its
* own UI and behavior:
* - **Hybrid Mode:** A single tap captures an image, and a long press initiates video recording.
* - **Image-only Mode:** Only single taps are active for image capture. Long press for video recording is disabled.
* - **Video-only Mode:** A single tap initiates video recording, and a long press also initiates video recording, with the drag-to-lock feature available.
*
* @param modifier the modifier for this component
* @param onImageCapture the callback for an image capture event
* @param onStartRecording the callback for a start recording event
* @param onStopRecording the callback for a stop recording event
* @param onLockVideoRecording The callback for a lock video recording event. The boolean parameter indicates if the recording should be locked.
* @param onIncrementZoom The callback for a zoom increment event, providing the zoom increment value.
Comment thread
Kimblebee marked this conversation as resolved.
* @param captureButtonUiState the [CaptureButtonUiState] for this component
* @param captureButtonSize the size of the capture button
*/
@Composable
internal fun CaptureButton(
modifier: Modifier = Modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,25 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

// these layouts are only concerned with placement. nothing else. no state handling
/**
* The base layout for the camera capture screen.
*
* @param modifier the modifier for this component
* @param viewfinder the viewfinder composable
* @param captureButton the capture button composable
* @param imageWell the image well composable
* @param flipCameraButton the flip camera button composable
* @param zoomLevelDisplay the zoom level display composable
* @param elapsedTimeDisplay the elapsed time display composable
* @param quickSettingsButton the quick settings button composable
* @param indicatorRow the indicator row composable
* @param captureModeToggle the capture mode toggle composable
* @param quickSettingsOverlay the quick settings overlay composable
* @param debugOverlay the debug overlay composable
* @param debugVisibilityWrapper A wrapper that conditionally hides its contents based on debug settings
* @param screenFlashOverlay the screen flash overlay composable
* @param snackBar the snack bar composable for showing messages
*/
@Composable
fun PreviewLayout(
modifier: Modifier = Modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ private const val BLINK_TIME = 100L
private val TAP_TO_FOCUS_INDICATOR_SIZE = 56.dp
private const val FOCUS_INDICATOR_RESULT_DELAY = 100L

/**
* A composable that displays the elapsed time of a video recording in a "MM:SS" format.
* This text is only visible during an active recording.
*
* @param elapsedTimeUiState the [ElapsedTimeUiState] for this component.
*/
@Composable
fun ElapsedTimeText(modifier: Modifier = Modifier, elapsedTimeUiState: ElapsedTimeUiState) {
if (elapsedTimeUiState is ElapsedTimeUiState.Enabled) {
Expand All @@ -146,6 +152,18 @@ fun ElapsedTimeText(modifier: Modifier = Modifier, elapsedTimeUiState: ElapsedTi
}
}

/**
* A toggle button that allows the user to pause and resume video recording.
*
* The button's icon changes to reflect the current recording state: a pause icon is shown when
* recording is active, and a play icon is shown when the recording is paused. This component is only
* visible when a video recording is in progress.
*
* @param modifier the modifier for this component.
* @param onSetPause the callback invoked when the button is tapped.
* @param size the size of the button.
* @param currentRecordingState the current recording state.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun PauseResumeToggleButton(
Expand Down Expand Up @@ -177,6 +195,18 @@ fun PauseResumeToggleButton(
}
}

/**
* A toggle button that allows the user to mute and unmute the microphone during video recording.
*
* When audio is enabled, the button displays a pulsing animation that visualizes the captured
* audio amplitude, providing real-time feedback. The icon switches between a microphone and a
* microphone-off symbol to indicate the current state.
*
* @param modifier the modifier for this component.
* @param buttonSize the size of the button.
* @param audioUiState the [AudioUiState] that determines the button's appearance and enabled status.
* @param onToggleAudio the callback invoked when the button is tapped.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AmplitudeToggleButton(
Expand Down Expand Up @@ -235,6 +265,17 @@ fun AmplitudeToggleButton(
}
}

/**
* A toggle switch that allows the user to switch between image and video capture modes.
*
* This component visually represents the selected mode with distinct icons for photo and video.
* It is only enabled when both capture modes are available to the camera.
*
* @param uiState the [CaptureModeToggleUiState.Available] for this component.
* @param onChangeCaptureMode the callback for changing the capture mode.
* @param onToggleWhenDisabled the callback for when the toggle is disabled.
* @param modifier the modifier for this component.
*/
@Composable
fun CaptureModeToggleButton(
uiState: CaptureModeToggleUiState.Available,
Expand Down Expand Up @@ -304,6 +345,15 @@ fun CaptureModeToggleButton(
)
}

/**
* A composable that displays a snackbar message to the user, with an optional action button
* and a mandatory close button for dismissal.
*
* @param modifier the modifier for this component.
* @param snackbarToShow the [SnackbarData] to show.
* @param snackbarHostState the [SnackbarHostState] for this component.
* @param onSnackbarResult the callback for the snackbar result.
*/
@Composable
fun TestableSnackbar(
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -395,8 +445,22 @@ private fun DetectWindowColorModeChanges(
}

/**
* this is the preview surface display. This view implements gestures tap to focus, pinch to zoom,
* and double-tap to flip camera
* A composable that displays the camera preview and handles user input gestures.
*
* This component is the core of the camera's UI, showing the live feed from the camera.
* It supports several gestures:
* - **Single Tap:** Triggers a tap-to-focus event at the tapped location.
* - **Double Tap:** Flips the camera between front and back lenses.
* - **Pinch Gesture:** Scales the camera's zoom level.
*
* @param previewDisplayUiState the [PreviewDisplayUiState] for this component.
* @param onTapToFocus the callback for tapping to focus.
* @param onFlipCamera the callback for flipping the camera.
* @param onScaleZoom the callback for scaling the zoom.
* @param onRequestWindowColorMode the callback for requesting a window color mode.
* @param surfaceRequest the [SurfaceRequest] for the preview.
* @param focusMeteringUiState the [FocusMeteringUiState] for this component.
* @param modifier the modifier for this component.
*/
@Composable
fun PreviewDisplay(
Expand Down Expand Up @@ -510,6 +574,27 @@ fun PreviewDisplay(
}
}

/**
* A wrapper composable for the primary capture button.
*
* This component serves as the main user interaction point for capturing photos and recording videos.
* It adapts its behavior based on the current [CaptureMode]:
* - In **Hybrid mode**, a tap takes a picture, and a long press starts a video recording.
* - In **Image-only mode**, it only responds to taps for image capture.
* - In **Video-only mode**, a tap starts a video recording that can be locked for hands-free operation.
*
* It also handles gestures for zooming and locking the video recording.
*
* @param modifier the modifier for this component.
* @param captureButtonUiState the [CaptureButtonUiState] that dictates the button's behavior.
* @param isQuickSettingsOpen true if the quick settings panel is open.
* @param onToggleQuickSettings callback to open or close the quick settings.
* @param onIncrementZoom callback to adjust the camera's zoom level.
* @param onCaptureImage callback to trigger image capture.
* @param onStartVideoRecording callback to start video recording.
* @param onStopVideoRecording callback to stop video recording.
* @param onLockVideoRecording callback to lock the video recording for hands-free operation.
*/
@Composable
fun CaptureButton(
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -548,6 +633,16 @@ fun CaptureButton(
)
}

/**
* A composable that displays an icon indicating the current video stabilization mode.
*
* The icon is only visible when a stabilization mode other than 'OFF' is active. It is rendered in
* full white when stabilization is actively being applied, and is greyed out (with reduced alpha)
* if the stabilization mode is enabled but not currently active.
*
* @param stabilizationUiState the [StabilizationUiState] for this component.
* @param modifier the modifier for this component.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun StabilizationIcon(stabilizationUiState: StabilizationUiState, modifier: Modifier = Modifier) {
Expand Down Expand Up @@ -625,6 +720,15 @@ fun StabilizationIcon(stabilizationUiState: StabilizationUiState, modifier: Modi
}
}

/**
* A composable that displays an icon indicating the current video quality setting.
*
* The icon dynamically changes to represent the selected resolution, such as SD, HD, FHD, or UHD.
* It is not displayed if the video quality is unspecified.
*
* @param videoQuality the [VideoQuality] for this component.
* @param modifier the modifier for this component.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun VideoQualityIcon(videoQuality: VideoQuality, modifier: Modifier = Modifier) {
Expand Down Expand Up @@ -671,6 +775,16 @@ fun VideoQualityIcon(videoQuality: VideoQuality, modifier: Modifier = Modifier)
}
}

/**
* A button that allows the user to flip between the front and rear cameras.
*
* This button is only visible and enabled if the device has more than one camera lens available.
*
* @param enabledCondition the enabled condition for this component.
* @param flipLensUiState the [FlipLensUiState] for this component.
* @param onClick the callback for when the button is clicked.
* @param modifier the modifier for this component.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun FlipCameraButton(
Expand Down Expand Up @@ -719,11 +833,7 @@ fun FlipCameraButton(
}

/**
* A composable that displays a focus metering indicator on the viewfinder.
*
* This indicator is displayed when the user taps on the screen to focus. It shows a pulsing
* animation while the focus is in progress, and then shows a success or failure animation
* depending on the result of the focus operation.
* A composable that displays an indicator on the viewfinder when the user taps to focus.
*
* @param focusMeteringUiState The state of the focus metering operation.
* @param coordinateTransformer The coordinate transformer to use to map the surface coordinates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import kotlinx.coroutines.flow.runningFold
/** Orientation hysteresis amount used in rounding, in degrees. */
private const val ORIENTATION_HYSTERESIS = 5

/**
* A flow that emits the device's orientation, debounced to avoid rapid changes.
*
* @param context the application context
*/
fun debouncedOrientationFlow(context: Context) = callbackFlow {
val orientationListener = object : OrientationEventListener(context) {
override fun onOrientationChanged(orientation: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ package com.google.jetpackcamera.ui.components.capture

import com.google.jetpackcamera.ui.uistate.DisableRationale

/**
* Represents reasons why a UI component or functionality might be disabled, providing a test tag
* and a string resource ID for user-facing explanation.
*/
enum class DisabledReason(
// 'override' is required
override val testTag: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ import androidx.core.graphics.createBitmap
import com.google.jetpackcamera.data.media.MediaDescriptor
import com.google.jetpackcamera.ui.uistate.capture.ImageWellUiState

/**
* A composable that displays the last captured image.
*
* @param imageWellUiState the [ImageWellUiState.LastCapture] for this component
* @param onClick the callback for when the image well is clicked
* @param modifier the modifier for this component
* @param shape the shape of the image well
* @param enabled true if the image well is enabled
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalAnimationApi::class)
@Composable
fun ImageWell(
Expand Down
Loading
Loading