diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index eff9974d8e..33ffd805f5 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -16,7 +16,12 @@ package com.google.jetpackcamera.ui.components.capture import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsActions @@ -24,13 +29,18 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertContentDescriptionEquals +import androidx.compose.ui.test.assertWidthIsAtLeast +import androidx.compose.ui.test.assertWidthIsEqualTo +import androidx.compose.ui.test.click import androidx.compose.ui.test.isNotEnabled import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performSemanticsAction +import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator @@ -74,6 +84,7 @@ class CaptureButtonTest { } composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertExists() composeTestRule.onNodeWithTag( CAPTURE_BUTTON ).assertContentDescriptionEquals("Capture Photo") @@ -82,7 +93,8 @@ class CaptureButtonTest { composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .performSemanticsAction(SemanticsActions.OnClick) + .performTouchInput { click() } + composeTestRule.waitForIdle() assertThat(imageCaptured).isTrue() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) @@ -107,6 +119,7 @@ class CaptureButtonTest { } composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() composeTestRule.onNodeWithTag( CAPTURE_BUTTON ).assertContentDescriptionEquals("Capture Photo") @@ -134,6 +147,7 @@ class CaptureButtonTest { } composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() composeTestRule.onNodeWithTag( CAPTURE_BUTTON ).assertContentDescriptionEquals("Start Video Recording") @@ -161,6 +175,7 @@ class CaptureButtonTest { } composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() composeTestRule.onNodeWithTag( CAPTURE_BUTTON ).assertContentDescriptionEquals("Stop Video Recording") @@ -186,6 +201,7 @@ class CaptureButtonTest { } composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() composeTestRule.onNodeWithTag( CAPTURE_BUTTON ).assertContentDescriptionEquals("Recording Video") @@ -215,4 +231,132 @@ class CaptureButtonTest { CAPTURE_BUTTON ).assert(isNotEnabled()) } + + @Test + fun captureButton_unavailable_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag(CAPTURE_BUTTON), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Unavailable + ) + } + composeTestRule.onRoot().tryPerformAccessibilityChecks() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assert(isNotEnabled()) + } + + @Test + fun captureButton_standard_disableAnimations_capturesImageImmediately() { + var imageCaptured = false + composeTestRule.setContent { + CompositionLocalProvider( + LocalDisableAnimations provides true + ) { + CaptureButton( + modifier = Modifier.testTag(CAPTURE_BUTTON), + onImageCapture = { imageCaptured = true }, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) + } + } + + composeTestRule.onNodeWithTag(CAPTURE_BUTTON) + .performTouchInput { click() } + composeTestRule.waitForIdle() + assertThat(imageCaptured).isTrue() + } + + @Test + fun captureButtonNucleus_standardTap_scalesWithSnappySpring() { + var isTapping by mutableStateOf(false) + composeTestRule.setContent { + CaptureButtonNucleus( + modifier = Modifier.testTag(CAPTURE_BUTTON_NUCLEUS), + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD), + captureButtonSize = 100f, + isTapping = isTapping + ) + } + + // Idle STANDARD latent scale is 0.80f (80.dp for 100.dp button) + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(80.dp) + + // Trigger press in manual clock mode to verify underdamped snappy spring overshoot (> 86.dp) + composeTestRule.mainClock.autoAdvance = false + isTapping = true + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.mainClock.advanceTimeBy(80L) + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsAtLeast(86.2.dp) + + // Settle to resting pressed scale 0.86f (86.dp for 100.dp button) + composeTestRule.mainClock.autoAdvance = true + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(86.dp) + + // Release to return to resting idle scale 0.80f (80.dp) + isTapping = false + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(80.dp) + } + + @Test + fun captureButtonNucleus_imageOnlyTap_scales() { + var isTapping by mutableStateOf(false) + composeTestRule.setContent { + CaptureButtonNucleus( + modifier = Modifier.testTag(CAPTURE_BUTTON_NUCLEUS), + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY), + captureButtonSize = 100f, + isTapping = isTapping + ) + } + + // Idle IMAGE_ONLY is 86.dp (IDLE_IMAGE_CAPTURE_SCALE 0.86f * 100f) + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(86.dp) + + isTapping = true + composeTestRule.waitForIdle() + // Pressed IMAGE_ONLY is PRESSED_IMAGE_CAPTURE_SCALE (0.93f -> 93.dp) + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(93.dp) + + isTapping = false + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_NUCLEUS).assertWidthIsEqualTo(86.dp) + } + + @Test + fun captureButtonRing_borderWidthZero_doesNotComposeBorder() { + composeTestRule.setContent { + CaptureButtonRing( + captureButtonSize = 86f, + color = { Color.White }, + borderWidth = { 0f } + ) + } + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertDoesNotExist() + } + + @Test + fun captureButtonRing_borderWidthGreaterThanZero_composesBorder() { + composeTestRule.setContent { + CaptureButtonRing( + captureButtonSize = 86f, + color = { Color.White }, + borderWidth = { 3f } + ) + } + composeTestRule.onNodeWithTag(CAPTURE_BUTTON_RING_BORDER).assertExists() + } } + +private const val CAPTURE_BUTTON_NUCLEUS = "capture_button_nucleus" diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 9dc18431fd..d740f0899e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -19,13 +19,11 @@ import android.view.KeyEvent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.animateColor import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.spring import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.foundation.Canvas @@ -45,9 +43,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MotionScheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -75,6 +75,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -97,20 +98,17 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch private const val TAG = "CaptureButton" -private const val DEFAULT_CAPTURE_BUTTON_SIZE = 76f +private const val DEFAULT_CAPTURE_BUTTON_SIZE = 86f +private const val IDLE_STANDARD_LATENT_SCALE = 0.80f private const val IDLE_IMAGE_CAPTURE_SCALE = 0.86f private const val IDLE_VIDEO_CAPTURE_SCALE = 0.64f private const val PRESSED_IMAGE_CAPTURE_SCALE = 0.93f private const val LOCKED_RECORDING_NUCLEUS_SCALE = 0.51f -private const val MORPH_INTERMEDIATE_SCALE = 0.58f private const val BORDER_WIDTH = 3f -private const val ANIMATION_DURATION_SIZE = 250 -private const val ANIMATION_DURATION_COLOR = 150 -private const val ANIMATION_DURATION_NUCLEUS_RELEASE = 50 -private const val ANIMATION_DURATION_DISABLED = 500 -private const val ANIMATION_DURATION_NUCLEUS_PRESSED = 50 private const val ALPHA_DISABLED_NUCLEUS = 0.6f +private const val ALPHA_PRESSED_IMAGE_ONLY = 0.8f +private const val ALPHA_PRESSED_STANDARD = 0.9f private const val ALPHA_WHITE_20 = 0.2f private const val ALPHA_BLACK_60 = 0.6f @@ -392,6 +390,7 @@ private fun rememberDebouncedVisuallyDisabled( return isVisuallyDisabled } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun CaptureButton( modifier: Modifier = Modifier, @@ -426,34 +425,33 @@ private fun CaptureButton( captureButtonUiState = captureButtonUiState ) + val scope = rememberCoroutineScope() val disableAnimations = LocalDisableAnimations.current val isPressedInteraction by interactionSource.collectIsPressedAsState() - val animatedColor by animateColorAsState( + val fastSpatialSpec = remember { MotionScheme.expressive().fastSpatialSpec() } + val fastEffectsSpec = remember { MotionScheme.expressive().fastEffectsSpec() } + val defaultEffectsSpec = remember { MotionScheme.expressive().defaultEffectsSpec() } + + val isStandardIdle = currentUiState.value.let { + it is CaptureButtonUiState.Enabled.Idle && it.captureMode == CaptureMode.STANDARD + } + + val animatedBorderWidth = animateDpAsState( + targetValue = if (isStandardIdle) BORDER_WIDTH.dp else 0.dp, + animationSpec = if (disableAnimations) snap() else fastSpatialSpec, + label = "Capture Button Ring Border Width" + ) + + val animatedColor = animateColorAsState( targetValue = when { - isVisuallyDisabled -> { - if (currentUiState.value.let { - it is CaptureButtonUiState.Enabled.Idle && - it.captureMode == CaptureMode.STANDARD - } - ) { - LocalContentColor.current.copy(alpha = 0.2f) - } else { - Color.Transparent - } - } - currentUiState.value.let { - it is CaptureButtonUiState.Enabled.Idle && - it.captureMode == CaptureMode.STANDARD - } -> LocalContentColor.current - else -> Color.Transparent + !isStandardIdle -> Color.Transparent + isVisuallyDisabled -> LocalContentColor.current.copy(alpha = 0.2f) + else -> LocalContentColor.current }, - animationSpec = if (disableAnimations) { - snap() - } else { - tween( - durationMillis = - if (isVisuallyDisabled) ANIMATION_DURATION_DISABLED else ANIMATION_DURATION_COLOR - ) + animationSpec = when { + disableAnimations -> snap() + isStandardIdle -> fastEffectsSpec + else -> defaultEffectsSpec }, label = "Capture Button Color" ) @@ -506,7 +504,12 @@ private fun CaptureButton( } finally { isTapping = false isCaptureButtonPressed = false // Manually unset pressed state - interactionSource.tryEmit(PressInteraction.Release(press)) + scope.launch { + if (!disableAnimations) { + delay(50) // Ensure visible press state for fast taps + } + interactionSource.emit(PressInteraction.Release(press)) + } } if (shouldBeLocked()) { onLockVideoRecording(true) @@ -610,7 +613,8 @@ private fun CaptureButton( .focusable() .then(gestureModifier), captureButtonSize = captureButtonSize, - color = animatedColor + color = { animatedColor.value }, + borderWidth = { animatedBorderWidth.value.value } ) { if (useLockSwitch) { LockSwitchCaptureButtonNucleus( @@ -640,24 +644,38 @@ private fun CaptureButton( */ internal val LocalInitialPressedState = compositionLocalOf { false } +/** + * The outer ring and translucent background container of the capture button. + * + * @param modifier [Modifier] to be applied to the outer container. + * @param captureButtonSize Diameter of the capture button ring in dp. + * @param color Lambda provider for the border stroke color of the outer ring. + * @param borderWidth Lambda provider for the border stroke width in dp. + * @param contents Optional composable content rendered inside the ring (such as the nucleus). + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun CaptureButtonRing( modifier: Modifier = Modifier, captureButtonSize: Float, - color: Color, - borderWidth: Float = BORDER_WIDTH, + color: () -> Color, + borderWidth: () -> Float = { BORDER_WIDTH }, contents: (@Composable () -> Unit)? = null ) { + val disableAnimations = LocalDisableAnimations.current val backgroundStyle = LocalCameraControlBackgroundStyle.current val targetBackgroundColor = when (backgroundStyle) { CameraControlBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = ALPHA_WHITE_20) CameraControlBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = ALPHA_BLACK_60) } + val fastEffectsSpec = remember { MotionScheme.expressive().fastEffectsSpec() } val backgroundColor by animateColorAsState( targetValue = targetBackgroundColor, - animationSpec = androidx.compose.animation.core.tween( - durationMillis = ANIMATION_DURATION_COLOR - ), + animationSpec = if (disableAnimations) { + snap() + } else { + fastEffectsSpec + }, label = "backgroundColor" ) Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -667,15 +685,19 @@ internal fun CaptureButtonRing( .background(backgroundColor, CircleShape) ) contents?.invoke() - // todo(): use a canvas instead of a box. - // the sizing gets funny so the scales need to be completely readjusted - Box( - modifier = Modifier - .size( - captureButtonSize.dp - ) - .border(borderWidth.dp, color, CircleShape) - ) + val currentBorderWidth = borderWidth() + if (currentBorderWidth > 0f) { + // todo(): use a canvas instead of a box. + // the sizing gets funny so the scales need to be completely readjusted + Box( + modifier = Modifier + .testTag(CAPTURE_BUTTON_RING_BORDER) + .size( + captureButtonSize.dp + ) + .border(currentBorderWidth.dp, color(), CircleShape) + ) + } } } @@ -797,15 +819,10 @@ private fun LockSwitchCaptureButtonNucleus( } } -private enum class NucleusState { - Disabled, - Idle, - Pressed -} - private enum class NucleusSizeState { Unavailable, IdleStandard, + PressedStandard, IdleImageOnly, IdleVideoOnly, PressedImage, @@ -822,13 +839,14 @@ private enum class NucleusSizeState { * @param idleVideoCaptureScale the scale factor for the idle size of the video-only nucleus. Must be between 0 and 1. * @param pressedVideoCaptureScale the scale factor for the pressed size of the video-only nucleus. Must be between 0 and 1. */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun CaptureButtonNucleus( modifier: Modifier = Modifier, captureButtonUiState: CaptureButtonUiState, isTapping: Boolean, captureButtonSize: Float, - recordingColor: Color = Color.Red, + recordingColor: Color = CaptureTokens.RecordingRed, imageCaptureModeColor: Color = Color.White, idleImageCaptureScale: Float = IDLE_IMAGE_CAPTURE_SCALE, idleVideoCaptureScale: Float = IDLE_VIDEO_CAPTURE_SCALE, @@ -847,6 +865,11 @@ internal fun CaptureButtonNucleus( val currentUiState = rememberUpdatedState(captureButtonUiState) val disableAnimations = LocalDisableAnimations.current + val fastSpatialSpec = remember { MotionScheme.expressive().fastSpatialSpec() } + val snappyStandardTapSpatialSpec = remember { + spring(dampingRatio = 0.65f, stiffness = 1800f) + } + val fastEffectsSpec = remember { MotionScheme.expressive().fastEffectsSpec() } val nucleusSizeState = when (val uiState = currentUiState.value) { CaptureButtonUiState.Enabled.Recording.LockedRecording -> NucleusSizeState.RecordingLocked @@ -855,9 +878,17 @@ internal fun CaptureButtonNucleus( CaptureButtonUiState.Unavailable -> NucleusSizeState.Unavailable is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { CaptureMode.STANDARD -> - if (isTapping) NucleusSizeState.PressedImage else NucleusSizeState.IdleStandard + if (isTapping) { + NucleusSizeState.PressedStandard + } else { + NucleusSizeState.IdleStandard + } CaptureMode.IMAGE_ONLY -> - if (isTapping) NucleusSizeState.PressedImage else NucleusSizeState.IdleImageOnly + if (isTapping) { + NucleusSizeState.PressedImage + } else { + NucleusSizeState.IdleImageOnly + } CaptureMode.VIDEO_ONLY -> NucleusSizeState.IdleVideoOnly } } @@ -869,22 +900,11 @@ internal fun CaptureButtonNucleus( val rawCenterShapeSize by sizeTransition.animateDp( transitionSpec = { - if (disableAnimations || targetState == NucleusSizeState.PressedImage) { - snap() - } else if ( - NucleusSizeState.PressedImage isTransitioningTo NucleusSizeState.IdleImageOnly - ) { - tween(durationMillis = ANIMATION_DURATION_NUCLEUS_RELEASE) - } else if ( - NucleusSizeState.PressedImage isTransitioningTo NucleusSizeState.IdleStandard - ) { - keyframes { - durationMillis = ANIMATION_DURATION_NUCLEUS_RELEASE - (captureButtonSize * PRESSED_IMAGE_CAPTURE_SCALE).dp at - ANIMATION_DURATION_NUCLEUS_RELEASE - } - } else { - tween(durationMillis = ANIMATION_DURATION_SIZE, easing = FastOutSlowInEasing) + when { + disableAnimations -> snap() + NucleusSizeState.PressedStandard in setOf(initialState, targetState) -> + snappyStandardTapSpatialSpec + else -> fastSpatialSpec } }, label = "Nucleus Size" @@ -895,10 +915,11 @@ internal fun CaptureButtonNucleus( (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp NucleusSizeState.RecordingPressed -> (captureButtonSize * pressedVideoCaptureScale).dp - // no inner circle will be visible on Unavailable or idle STANDARD - NucleusSizeState.Unavailable, - NucleusSizeState.IdleStandard -> 0.dp - // large white circle will be visible on IMAGE_ONLY + NucleusSizeState.Unavailable -> 0.dp + NucleusSizeState.IdleStandard -> + (captureButtonSize * IDLE_STANDARD_LATENT_SCALE).dp + // large white circle will be visible on IMAGE_ONLY and when STANDARD is pressed + NucleusSizeState.PressedStandard, NucleusSizeState.IdleImageOnly -> (captureButtonSize * idleImageCaptureScale).dp // small red circle will be visible on VIDEO_ONLY @@ -910,85 +931,47 @@ internal fun CaptureButtonNucleus( } val centerShapeSize = rawCenterShapeSize.coerceAtLeast(0.dp) - val sizeFinal = (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp - val sizeInter = (captureButtonSize * MORPH_INTERMEDIATE_SCALE).dp - val isLocked = currentUiState.value is CaptureButtonUiState.Enabled.Recording.LockedRecording - val cornerRadius = ( - if (isLocked) { - if (centerShapeSize <= sizeInter) { - val fraction = (centerShapeSize - sizeFinal) / (sizeInter - sizeFinal) - val coercedFraction = fraction.coerceIn(0f, 1f) - LOCKED_CORNER_RADIUS + - (centerShapeSize / 2 - LOCKED_CORNER_RADIUS) * coercedFraction - } else { - centerShapeSize / 2 - } + val rawCornerRadius by animateDpAsState( + targetValue = if (nucleusSizeState == NucleusSizeState.RecordingLocked) { + LOCKED_CORNER_RADIUS } else { - centerShapeSize / 2 - } - ).coerceAtLeast(0.dp) - - // used to fade between red/white in the center of the capture button - val isPressableImageMode = currentUiState.value.let { - it is CaptureButtonUiState.Enabled.Idle && - (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) - } - val nucleusState = when { - isVisuallyDisabled -> NucleusState.Disabled - isTapping && isPressableImageMode -> NucleusState.Pressed - else -> NucleusState.Idle - } - - val transition = - updateTransition(targetState = nucleusState, label = "Nucleus Color Transition") - val animatedColor by transition.animateColor( - label = "Nucleus Color", - transitionSpec = { - if (disableAnimations) { - snap() - } else { - when { - NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween( - durationMillis = ANIMATION_DURATION_COLOR - ) - NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween( - durationMillis = ANIMATION_DURATION_DISABLED - ) - NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween( - durationMillis = ANIMATION_DURATION_NUCLEUS_PRESSED - ) - else -> snap() - } - } - } - ) { state -> - when (state) { - NucleusState.Disabled -> Color.Black.copy(alpha = ALPHA_DISABLED_NUCLEUS) - NucleusState.Pressed -> imageCaptureModeColor - NucleusState.Idle -> { - when (val uiState = currentUiState.value) { - is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { - CaptureMode.STANDARD -> - if ( - sizeTransition.currentState == NucleusSizeState.RecordingPressed || - sizeTransition.currentState == NucleusSizeState.RecordingLocked - ) { - imageCaptureModeColor - } else { - imageCaptureModeColor.copy(alpha = 0f) - } - CaptureMode.IMAGE_ONLY -> imageCaptureModeColor - CaptureMode.VIDEO_ONLY -> - if (isTapping) recordingColor else imageCaptureModeColor - } + ((captureButtonSize * PRESSED_IMAGE_CAPTURE_SCALE) / 2f).dp + }, + animationSpec = if (disableAnimations) snap() else fastSpatialSpec, + label = "Nucleus Corner Radius" + ) + val cornerRadius = rawCornerRadius.coerceIn(0.dp, (centerShapeSize / 2).coerceAtLeast(0.dp)) - is CaptureButtonUiState.Enabled.Recording -> recordingColor - is CaptureButtonUiState.Unavailable -> Color.Transparent - } - } + val targetNucleusColor = if (isVisuallyDisabled) { + Color.Black.copy(alpha = ALPHA_DISABLED_NUCLEUS) + } else { + when (nucleusSizeState) { + NucleusSizeState.IdleStandard, + NucleusSizeState.Unavailable -> imageCaptureModeColor.copy(alpha = 0f) + NucleusSizeState.PressedStandard -> + imageCaptureModeColor.copy(alpha = ALPHA_PRESSED_STANDARD) + NucleusSizeState.IdleImageOnly -> imageCaptureModeColor + NucleusSizeState.PressedImage -> + imageCaptureModeColor.copy(alpha = ALPHA_PRESSED_IMAGE_ONLY) + NucleusSizeState.IdleVideoOnly -> + if (isTapping) recordingColor else imageCaptureModeColor + NucleusSizeState.RecordingPressed, + NucleusSizeState.RecordingLocked -> recordingColor } } + val animatedColor by animateColorAsState( + targetValue = targetNucleusColor, + animationSpec = if ( + disableAnimations || nucleusSizeState == NucleusSizeState.PressedStandard + ) { + snap() + } else { + fastEffectsSpec + }, + label = "Nucleus Color" + ) + // this box contains and centers everything Box(modifier = modifier, contentAlignment = Alignment.Center) { // this box is the inner circle @@ -998,16 +981,6 @@ internal fun CaptureButtonNucleus( modifier = Modifier .size(centerShapeSize) .clip(RoundedCornerShape(cornerRadius)) - .alpha( - if (isTapping && - currentUiState.value == - CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) - ) { - .5f // transparency to indicate click ONLY on IMAGE_ONLY - } else { - 1f // solid alpha the rest of the time - } - ) .background(animatedColor) ) {} } @@ -1130,7 +1103,7 @@ internal fun LockSwitchUnlockedPressedRecordingPreview() { // box is here to account for the offset lock switch PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, - modifier = Modifier.width(150.dp), + modifier = Modifier.width(172.dp), contentAlignment = Alignment.CenterEnd ) } @@ -1149,7 +1122,7 @@ internal fun LockSwitchLockedAtThresholdPressedRecordingPreview() { // box is here to account for the offset lock switch Box( modifier = Modifier - .width(150.dp) + .width(172.dp) .background( Brush.verticalGradient( colors = listOf(Color.Gray, Color.DarkGray) @@ -1159,7 +1132,7 @@ internal fun LockSwitchLockedAtThresholdPressedRecordingPreview() { ) { CaptureButtonRing( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - color = Color.Transparent + color = { Color.Transparent } ) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, @@ -1180,7 +1153,7 @@ internal fun LockSwitchLockedPressedRecordingPreview() { // box is here to account for the offset lock switch Box( modifier = Modifier - .width(150.dp) + .width(172.dp) .background( Brush.verticalGradient( colors = listOf(Color.Gray, Color.DarkGray) @@ -1190,7 +1163,7 @@ internal fun LockSwitchLockedPressedRecordingPreview() { ) { CaptureButtonRing( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - color = Color.Transparent + color = { Color.Transparent } ) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index 3a62488329..f2d4817425 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -193,7 +193,7 @@ fun ElapsedTimeText( contentDescription = accessibilityText } .defaultMinSize(minWidth = 72.dp, minHeight = 32.dp) - .background(color = Color(0xFFED0000), shape = CircleShape) + .background(color = CaptureTokens.RecordingRed, shape = CircleShape) .padding(horizontal = 12.dp, vertical = 6.dp), contentAlignment = Alignment.Center ) { diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureTokens.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureTokens.kt new file mode 100644 index 0000000000..91b1e543d1 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureTokens.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 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.ui.components.capture + +import androidx.compose.ui.graphics.Color + +/** + * Design tokens for camera capture UI components. + */ +internal object CaptureTokens { + /** + * Standard red color indicating an active video recording state across capture components. + */ + val RecordingRed = Color(0xFFED0000) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt index 4171c487d8..95863a1988 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/TestTags.kt @@ -18,6 +18,7 @@ package com.google.jetpackcamera.ui.components.capture import androidx.compose.ui.semantics.SemanticsPropertyKey import androidx.compose.ui.semantics.SemanticsPropertyReceiver const val CAPTURE_BUTTON = "CaptureButton" +const val CAPTURE_BUTTON_RING_BORDER = "CaptureButtonRingBorder" const val CAPTURE_MODE_TOGGLE_BUTTON = "CaptureModeToggleButton" const val FLIP_CAMERA_BUTTON = "FlipCameraButton" const val SNACKBAR_NODE_TAG = "SnackbarNodeTag" diff --git a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt index 76e5f11c63..e47919875a 100644 --- a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt +++ b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt @@ -196,7 +196,7 @@ fun DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview() { @Composable fun PressedRecordingScreenshotPreview() { PreviewCaptureButton( - modifier = Modifier.width(150.dp), + modifier = Modifier.width(172.dp), captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording ) } @@ -216,7 +216,7 @@ fun PressedRecordingBlack60ScreenshotPreview() { LocalCameraControlBackgroundStyle provides CameraControlBackgroundStyle.BLACK_60 ) { PreviewCaptureButton( - modifier = Modifier.width(150.dp), + modifier = Modifier.width(172.dp), captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording ) } diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png index 7748076b73..1d9655af9f 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png index b4e34ecc88..096665d932 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonBlack60ScreenshotPreview_0.png index f764c47b9a..6b330e4e30 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonScreenshotPreview_0.png index a7d77b0016..cf4549d24d 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png index bf4741c3b6..151d83899a 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png index 716fe005e8..4e2ec44da3 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png index 2cf1bf5a3c..8bd081018d 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png index 352406ae73..07a6147a08 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonBlack60ScreenshotPreview_0.png index ce155e88df..fd59c06bde 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonScreenshotPreview_0.png index 93a1df57c9..b46f7ab674 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png index 89be0a994f..43afb4febc 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonScreenshotPreview_0.png index 790101ad3b..3e1bedeb8e 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingBlack60ScreenshotPreview_0.png index 26b4ebddac..5880984919 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingScreenshotPreview_0.png index 3410b62ade..de7bccafc7 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png index 704e42c1c2..c98acbbb68 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png index f8ec851aab..d4f346a0ac 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png index a1dd91a3e9..4379932d96 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png index 13785ff2f8..1eb4e14488 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png index 92946e3f5b..55c722a0aa 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png index bb61aff4a5..841eaa0f17 100644 Binary files a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png and b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png differ diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/CaptureTokensTest.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/CaptureTokensTest.kt new file mode 100644 index 0000000000..aaaf7600e2 --- /dev/null +++ b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/CaptureTokensTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 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.ui.components.capture + +import androidx.compose.ui.graphics.Color +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CaptureTokensTest { + + @Test + fun recordingRed_hasExpectedColorValue() { + assertThat(CaptureTokens.RecordingRed).isEqualTo(Color(0xFFED0000)) + } +}