From 13472ba73fe9aa645386529963490a7b8b0ef119 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 15:49:12 +0200 Subject: [PATCH 1/7] Extract EditScreen composables out of CreationScreen --- .../androidify/creation/CreationScreen.kt | 878 ------------------ .../androidify/creation/EditScreen.kt | 196 ++++ .../androidify/creation/EditScreenCompact.kt | 244 +++++ .../androidify/creation/EditScreenMedium.kt | 154 +++ .../androidify/creation/PhotoPrompt.kt | 362 ++++++++ .../androidify/creation/PromptTypePager.kt | 201 ++++ .../androidify/creation/TextPrompt.kt | 198 ++++ .../androidify/creation/TransformButton.kt | 53 ++ 8 files changed, 1408 insertions(+), 878 deletions(-) create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/TextPrompt.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/TransformButton.kt diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt index db0043e9..37b6258c 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt @@ -22,141 +22,24 @@ package com.android.developers.androidify.creation -import android.net.Uri -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterExitState import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.draganddrop.dragAndDropTarget -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.isImeVisible -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeContentPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FloatingToolbarColors -import androidx.compose.material3.HorizontalFloatingToolbar -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialShapes -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarDefaults -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.ToggleButton -import androidx.compose.material3.ToggleButtonDefaults -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.ripple -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.TextStyle -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.sp import androidx.core.net.toUri -import androidx.graphics.shapes.RoundedPolygon -import androidx.graphics.shapes.rectangle import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil3.compose.AsyncImage -import coil3.request.ImageRequest -import coil3.request.crossfade import com.android.developers.androidify.customize.CustomizeAndExportScreen import com.android.developers.androidify.customize.CustomizeExportViewModel -import com.android.developers.androidify.data.DropBehaviourFactory import com.android.developers.androidify.results.ResultsScreen -import com.android.developers.androidify.theme.AndroidifyTheme -import com.android.developers.androidify.theme.LimeGreen -import com.android.developers.androidify.theme.LocalSharedTransitionScope -import com.android.developers.androidify.theme.Primary90 -import com.android.developers.androidify.theme.R -import com.android.developers.androidify.theme.Secondary -import com.android.developers.androidify.theme.SharedElementKey -import com.android.developers.androidify.theme.components.AboutButton -import com.android.developers.androidify.theme.components.AndroidifyTopAppBar -import com.android.developers.androidify.theme.components.GradientAssistElevatedChip -import com.android.developers.androidify.theme.components.PrimaryButton -import com.android.developers.androidify.theme.components.ScaleIndicationNodeFactory -import com.android.developers.androidify.theme.components.SecondaryOutlinedButton -import com.android.developers.androidify.theme.components.SquiggleBackground -import com.android.developers.androidify.theme.components.gradientChipColorDefaults -import com.android.developers.androidify.theme.components.infinitelyAnimatingLinearGradient -import com.android.developers.androidify.theme.sharedBoundsRevealWithShapeMorph -import com.android.developers.androidify.theme.sharedBoundsWithDefaults -import com.android.developers.androidify.util.AnimatedTextField -import com.android.developers.androidify.util.LargeScreensPreview -import com.android.developers.androidify.util.dashedRoundedRectBorder import com.android.developers.androidify.util.isAtLeastMedium -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import com.android.developers.androidify.creation.R as CreationR @Composable fun CreationScreen( @@ -255,764 +138,3 @@ fun CreationScreen( } } } - -@Composable -fun EditScreen( - snackbarHostState: SnackbarHostState, - dropBehaviourFactory: DropBehaviourFactory, - isExpanded: Boolean, - onCameraPressed: () -> Unit, - onBackPressed: () -> Unit, - onAboutPressed: () -> Unit, - uiState: CreationState, - onChooseImageClicked: (PickVisualMedia.VisualMediaType) -> Unit, - onPromptOptionSelected: (PromptType) -> Unit, - onUndoPressed: () -> Unit, - onPromptGenerationPressed: () -> Unit, - onBotColorSelected: (BotColor) -> Unit, - onStartClicked: () -> Unit, - onDropCallback: (Uri) -> Unit = {}, -) { - Scaffold( - snackbarHost = { - SnackbarHost( - hostState = snackbarHostState, - snackbar = { snackbarData -> - Snackbar( - snackbarData, - shape = SnackbarDefaults.shape, - modifier = Modifier.padding(4.dp), - ) - }, - modifier = Modifier.safeContentPadding(), - ) - }, - topBar = { - AndroidifyTopAppBar( - backEnabled = true, - isMediumWindowSize = isExpanded, - actions = { - AboutButton { onAboutPressed() } - }, - onBackPressed = onBackPressed, - expandedCenterButtons = { - PromptTypeToolbar( - uiState.selectedPromptOption, - modifier = Modifier.padding(start = 16.dp, end = 16.dp), - onOptionSelected = onPromptOptionSelected, - ) - }, - ) - }, - containerColor = MaterialTheme.colorScheme.surface, - ) { contentPadding -> - SquiggleBackground(offsetHeightFraction = 0.5f) - Box( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) - .imePadding(), - ) { - var showColorPickerBottomSheet by remember { mutableStateOf(false) } - Column( - modifier = Modifier.fillMaxSize(), - ) { - Spacer(modifier = Modifier.height(8.dp)) - if (!isExpanded) { - PromptTypeToolbar( - uiState.selectedPromptOption, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp) - .align(Alignment.CenterHorizontally), - onOptionSelected = onPromptOptionSelected, - ) - } - if (isExpanded) { - Row( - modifier = Modifier - .weight(1f) - .padding(end = 16.dp), - ) { - MainCreationPane( - uiState, - dropBehaviourFactory = dropBehaviourFactory, - modifier = Modifier.weight(.6f), - onCameraPressed = onCameraPressed, - onChooseImageClicked = { - onChooseImageClicked(PickVisualMedia.ImageOnly) - }, - onUndoPressed = onUndoPressed, - onPromptGenerationPressed = onPromptGenerationPressed, - onSelectedPromptOptionChanged = onPromptOptionSelected, - onDropCallback = onDropCallback, - ) - Box( - modifier = Modifier - .weight(.4f) - .padding(top = 16.dp, bottom = 16.dp) - .fillMaxSize() - .background( - color = MaterialTheme.colorScheme.surfaceContainerLowest, - shape = MaterialTheme.shapes.large, - ) - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.outline, - shape = MaterialTheme.shapes.large, - ), - ) { - AndroidBotColorPicker( - selectedBotColor = uiState.botColor, - modifier = Modifier.padding(16.dp), - onBotColorSelected = onBotColorSelected, - listBotColor = uiState.listBotColors, - ) - } - } - } else { - MainCreationPane( - uiState, - dropBehaviourFactory = dropBehaviourFactory, - modifier = Modifier.weight(1f), - onCameraPressed = onCameraPressed, - onChooseImageClicked = { - onChooseImageClicked(PickVisualMedia.ImageOnly) - }, - onUndoPressed = onUndoPressed, - onPromptGenerationPressed = onPromptGenerationPressed, - onSelectedPromptOptionChanged = onPromptOptionSelected, - onDropCallback = onDropCallback, - ) - } - - if (isExpanded) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, end = 16.dp), - contentAlignment = Alignment.CenterEnd, - ) { - TransformButton( - modifier = Modifier.padding(bottom = 8.dp), - buttonText = stringResource(CreationR.string.start_transformation_button), - onClicked = onStartClicked, - ) - } - } else { - BottomButtons( - onButtonColorClicked = { - showColorPickerBottomSheet = !showColorPickerBottomSheet - }, - uiState = uiState, - onStartClicked = onStartClicked, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) - } - } - - BotColorPickerBottomSheet( - showColorPickerBottomSheet, - dismissBottomSheet = { - showColorPickerBottomSheet = false - }, - onColorChanged = onBotColorSelected, - listBotColors = uiState.listBotColors, - selectedBotColor = uiState.botColor, - ) - } - } -} - -@Composable -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -private fun MainCreationPane( - uiState: CreationState, - dropBehaviourFactory: DropBehaviourFactory, - modifier: Modifier = Modifier, - onCameraPressed: () -> Unit, - onChooseImageClicked: () -> Unit = {}, - onUndoPressed: () -> Unit = {}, - onPromptGenerationPressed: () -> Unit, - onSelectedPromptOptionChanged: (PromptType) -> Unit, - onDropCallback: (Uri) -> Unit, -) { - val defaultDropAreaBackgroundColor = MaterialTheme.colorScheme.surface - val alternateDropAreaBackgroundColor = MaterialTheme.colorScheme.surfaceVariant - var background by remember { mutableStateOf(defaultDropAreaBackgroundColor) } - - val activity = LocalActivity.current as ComponentActivity - val externalAppCallback = remember { - dropBehaviourFactory.createTargetCallback( - activity = activity, - onImageDropped = { uri -> onDropCallback(uri) }, - onDropStarted = { background = alternateDropAreaBackgroundColor }, - onDropEnded = { background = defaultDropAreaBackgroundColor }, - ) - } - - Box( - modifier = modifier, - ) { - val spatialSpec = MaterialTheme.motionScheme.slowSpatialSpec() - val pagerState = - rememberPagerState(uiState.selectedPromptOption.ordinal) { PromptType.entries.size } - val focusManager = LocalFocusManager.current - LaunchedEffect(uiState.selectedPromptOption) { - launch { - pagerState.animateScrollToPage( - uiState.selectedPromptOption.ordinal, - animationSpec = spatialSpec, - ) - }.invokeOnCompletion { - if (uiState.selectedPromptOption != PromptType.entries[pagerState.currentPage]) { - onSelectedPromptOptionChanged(PromptType.entries[pagerState.currentPage]) - } - } - } - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.currentPage }.collect { page -> - onSelectedPromptOptionChanged(PromptType.entries[page]) - } - } - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.targetPage }.collect { - if (pagerState.targetPage != PromptType.TEXT.ordinal) { - focusManager.clearFocus() - } - } - } - HorizontalPager( - pagerState, - modifier.fillMaxSize(), - pageSpacing = 16.dp, - contentPadding = PaddingValues(16.dp), - ) { - when (it) { - PromptType.PHOTO.ordinal -> { - val imageUri = uiState.imageUri - if (imageUri == null) { - UploadEmptyState( - modifier = Modifier - .background( - color = background, - shape = MaterialTheme.shapes.large, - ) - .dashedRoundedRectBorder( - 2.dp, - MaterialTheme.colorScheme.outline, - cornerRadius = 28.dp, - ) - .dragAndDropTarget( - shouldStartDragAndDrop = { event -> - dropBehaviourFactory.shouldStartDragAndDrop( - event, - ) - }, - target = externalAppCallback, - ) - .fillMaxSize() - .padding(2.dp), - onCameraPressed = onCameraPressed, - onChooseImagePress = onChooseImageClicked, - ) - } else { - ImagePreview( - imageUri, - onUndoPressed, - onChooseImagePressed = onChooseImageClicked, - modifier = Modifier - .fillMaxSize() - .heightIn(min = 200.dp), - ) - } - } - - PromptType.TEXT.ordinal -> { - // Workaround for https://issuetracker.google.com/432431393 - val showTextPrompt by remember { - derivedStateOf { - pagerState.currentPage == PromptType.TEXT.ordinal && - pagerState.targetPage == pagerState.currentPage - } - } - if (showTextPrompt) { - TextPrompt( - textFieldState = uiState.descriptionText, - promptGenerationInProgress = uiState.promptGenerationInProgress, - generatedPrompt = uiState.generatedPrompt, - onPromptGenerationPressed = onPromptGenerationPressed, - modifier = Modifier - .fillMaxSize() - .heightIn(min = 200.dp) - .padding(2.dp), - ) - } - } - } - } - } -} - -@Composable -private fun ColumnScope.BottomButtons( - onButtonColorClicked: () -> Unit, - uiState: CreationState, - onStartClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - FlowRow( - maxItemsInEachRow = 3, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - .wrapContentSize() - .padding(8.dp) - .align(Alignment.CenterHorizontally), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - SecondaryOutlinedButton( - onClick = { - onButtonColorClicked() - }, - buttonText = stringResource(CreationR.string.bot_color_button), - modifier = Modifier.fillMaxRowHeight(), - leadingIcon = { - Row { - DisplayBotColor( - uiState.botColor, - modifier = Modifier - .clip(CircleShape) - .border( - 2.dp, - color = MaterialTheme.colorScheme.outline, - CircleShape, - ) - .size(32.dp), - ) - Spacer(modifier = Modifier.width(8.dp)) - } - }, - ) - TransformButton( - modifier = Modifier.fillMaxRowHeight(), - onClicked = onStartClicked, - ) - } -} - -@Composable -private fun TransformButton( - modifier: Modifier = Modifier, - buttonText: String = stringResource(CreationR.string.transform_button), - onClicked: () -> Unit = {}, -) { - PrimaryButton( - modifier = modifier, - onClick = onClicked, - buttonText = buttonText, - trailingIcon = { - Row { - Spacer(modifier = Modifier.width(8.dp)) - Icon( - ImageVector.vectorResource(R.drawable.rounded_arrow_forward_24), - contentDescription = null, - ) - } - }, - ) -} - -@Composable -private fun BotColorPickerBottomSheet( - showColorPickerBottomSheet: Boolean, - dismissBottomSheet: () -> Unit, - onColorChanged: (BotColor) -> Unit, - listBotColors: List, - selectedBotColor: BotColor, -) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - if (showColorPickerBottomSheet) { - ModalBottomSheet( - modifier = Modifier, - sheetState = sheetState, - onDismissRequest = { - dismissBottomSheet() - }, - ) { - val scope = rememberCoroutineScope() - Column( - modifier = Modifier.padding( - start = 36.dp, - end = 36.dp, - top = 16.dp, - bottom = 8.dp, - ), - ) { - AndroidBotColorPicker( - selectedBotColor, - onBotColorSelected = { - onColorChanged(it) - scope.launch { - delay(400) - sheetState.hide() - }.invokeOnCompletion { - if (!sheetState.isVisible) { - dismissBottomSheet() - } - } - }, - listBotColor = listBotColors, - ) - } - } - } -} - -@Composable -fun ImagePreview( - uri: Uri, - onUndoPressed: () -> Unit, - onChooseImagePressed: () -> Unit, - modifier: Modifier = Modifier, -) { - val sharedElementScope = LocalSharedTransitionScope.current - with(sharedElementScope) { - Box(modifier) { - AsyncImage( - ImageRequest.Builder(LocalContext.current).data(uri).crossfade(false).build(), - placeholder = null, - contentDescription = stringResource(CreationR.string.cd_selected_image), - modifier = Modifier - .align(Alignment.Center) - .sharedBoundsWithDefaults(rememberSharedContentState(SharedElementKey.CaptureImageToDetails)) - .clip(MaterialTheme.shapes.large) - .fillMaxSize(), - contentScale = ContentScale.Crop, - ) - - Row( - modifier = Modifier - .align(Alignment.BottomStart) - .padding(8.dp), - ) { - SecondaryOutlinedButton( - onClick = { - onUndoPressed() - }, - leadingIcon = { - Icon( - ImageVector.vectorResource(CreationR.drawable.rounded_redo_24), - contentDescription = stringResource(CreationR.string.cd_retake_photo), - ) - }, - ) - Spacer(modifier = Modifier.width(8.dp)) - SecondaryOutlinedButton( - onClick = { - onChooseImagePressed() - }, - buttonText = stringResource(CreationR.string.photo_picker_choose_photo_label), // Reusing existing - leadingIcon = { - Icon( - ImageVector.vectorResource(CreationR.drawable.rounded_photo_24), - contentDescription = stringResource(CreationR.string.cd_choose_photo), - ) - }, - ) - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun TextPromptGenerationPreview() { - AndroidifyTheme { - TextPrompt( - TextFieldState(), - false, - generatedPrompt = "wearing a red sweater", - onPromptGenerationPressed = {}, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun TextPromptGenerationInProgressPreview() { - AndroidifyTheme { - TextPrompt( - TextFieldState(), - true, - generatedPrompt = "wearing a red sweater", - onPromptGenerationPressed = {}, - ) - } -} - -@Composable -fun TextPrompt( - textFieldState: TextFieldState, - promptGenerationInProgress: Boolean, - modifier: Modifier = Modifier, - generatedPrompt: String? = null, - onPromptGenerationPressed: () -> Unit, -) { - Column(modifier) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - rememberVectorPainter(ImageVector.vectorResource(CreationR.drawable.rounded_draw_24)), - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - stringResource(CreationR.string.headline_my_bot_is), - style = MaterialTheme.typography.headlineLarge, - fontSize = 24.sp, - ) - } - Spacer(modifier = Modifier.size(8.dp)) - Column( - modifier = Modifier - .dashedRoundedRectBorder( - 2.dp, - MaterialTheme.colorScheme.outline, - cornerRadius = 28.dp, - ) - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 16.dp) - .fillMaxSize(), - ) { - AnimatedTextField( - textFieldState, - targetEndState = generatedPrompt, - modifier = Modifier - .weight(1f) - .fillMaxSize(), - textStyle = TextStyle(fontSize = 24.sp), - decorator = { innerTextField -> - if (textFieldState.text.isEmpty()) { - Text( - stringResource(CreationR.string.prompt_text_hint).trimIndent(), - color = Color.Gray, - fontSize = 24.sp, - ) - } - innerTextField() - }, - ) - AnimatedVisibility( - !WindowInsets.isImeVisible, - enter = fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec()), - exit = fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()), - ) { - HelpMeWriteButton(promptGenerationInProgress, onPromptGenerationPressed) - } - } - } -} - -@Composable -private fun HelpMeWriteButton( - promptGenerationInProgress: Boolean, - onPromptGenerationPressed: () -> Unit, -) { - val color = if (promptGenerationInProgress) { - Brush.infinitelyAnimatingLinearGradient( - listOf( - LimeGreen, - Primary90, - Secondary, - ), - ) - } else { - SolidColor(MaterialTheme.colorScheme.surfaceContainerLow) - } - GradientAssistElevatedChip( - onClick = { - onPromptGenerationPressed() - }, - label = { - if (promptGenerationInProgress) { - Text(stringResource(CreationR.string.writing)) - } else { - Text(stringResource(CreationR.string.write_me_a_prompt)) - } - }, - leadingIcon = { - Icon( - rememberVectorPainter(ImageVector.vectorResource(CreationR.drawable.pen_spark_24)), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - }, - colors = gradientChipColorDefaults().copy( - containerColor = color, - disabledContainerColor = color, - ), - enabled = !promptGenerationInProgress, - ) -} - -@Composable -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun PromptTypeToolbar( - selectedOption: PromptType, - modifier: Modifier = Modifier, - onOptionSelected: (PromptType) -> Unit, -) { - val options = PromptType.entries - HorizontalFloatingToolbar( - modifier = modifier.border( - 2.dp, - color = MaterialTheme.colorScheme.outline, - shape = MaterialTheme.shapes.large, - ), - colors = FloatingToolbarColors( - toolbarContainerColor = MaterialTheme.colorScheme.surface, - toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - fabContainerColor = MaterialTheme.colorScheme.tertiary, - fabContentColor = MaterialTheme.colorScheme.onTertiary, - ), - expanded = true, - ) { - options.forEachIndexed { index, label -> - ToggleButton( - modifier = Modifier, - checked = selectedOption == label, - onCheckedChange = { onOptionSelected(label) }, - shapes = ToggleButtonDefaults.shapes(checkedShape = MaterialTheme.shapes.large), - colors = ToggleButtonDefaults.toggleButtonColors( - checkedContainerColor = MaterialTheme.colorScheme.onSurface, - containerColor = MaterialTheme.colorScheme.surface, - ), - ) { - Text(label.displayName, maxLines = 1) - } - if (index != options.size - 1) { - Spacer(Modifier.width(8.dp)) - } - } - } -} - -@LargeScreensPreview -@Preview -@Composable -private fun UploadEmptyPreview() { - AndroidifyTheme { - UploadEmptyState( - {}, - {}, - modifier = Modifier - .height(300.dp) - .fillMaxWidth(), - ) - UploadEmptyState( - {}, - {}, - modifier = Modifier - .height(400.dp) - .fillMaxWidth(), - ) - } -} - -@Composable -private fun UploadEmptyState( - onCameraPressed: () -> Unit, - onChooseImagePress: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - stringResource(CreationR.string.photo_picker_title), - fontSize = 28.sp, - textAlign = TextAlign.Center, - lineHeight = 40.sp, - minLines = 2, - maxLines = 2, - ) - Spacer(modifier = Modifier.height(16.dp)) - TakePhotoButton(onCameraPressed) - Spacer(modifier = Modifier.height(32.dp)) - SecondaryOutlinedButton( - onClick = { - onChooseImagePress() - }, - leadingIcon = { - Image( - painterResource(CreationR.drawable.choose_picture_image), - contentDescription = null, - modifier = Modifier - .padding(end = 8.dp) - .size(24.dp), - ) - }, - buttonText = stringResource(CreationR.string.photo_picker_choose_photo_label), - ) - } -} - -@Composable -private fun TakePhotoButton(onCameraPressed: () -> Unit) { - val interactionSource = remember { MutableInteractionSource() } - val animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec() - val sharedElementScope = LocalSharedTransitionScope.current - with(sharedElementScope) { - Box( - modifier = Modifier - .defaultMinSize(minHeight = 48.dp, minWidth = 48.dp) - .sizeIn( - minHeight = 48.dp, - maxHeight = ButtonDefaults.ExtraLargeContainerHeight, - minWidth = 48.dp, - maxWidth = ButtonDefaults.ExtraLargeContainerHeight, - ) - .aspectRatio(1f, matchHeightConstraintsFirst = true) - .indication(interactionSource, ScaleIndicationNodeFactory(animationSpec)) - .background( - MaterialTheme.colorScheme.onSurface, - MaterialShapes.Cookie9Sided.toShape(), - ) - .clickable( - interactionSource = interactionSource, - indication = ripple(color = Color.White), - onClick = { - onCameraPressed() - }, - role = Role.Button, - enabled = true, - onClickLabel = stringResource(CreationR.string.take_picture_content_description), - ) - .sharedBoundsRevealWithShapeMorph( - rememberSharedContentState(SharedElementKey.CameraButtonToFullScreenCamera), - restingShape = MaterialShapes.Cookie9Sided, - targetShape = RoundedPolygon.rectangle().normalized(), - targetValueByState = { - when (it) { - EnterExitState.PreEnter -> 0f - EnterExitState.Visible -> 1f - EnterExitState.PostExit -> 1f - } - }, - ), - ) { - Image( - painterResource(R.drawable.photo_camera), - contentDescription = stringResource(CreationR.string.take_picture_content_description), - modifier = Modifier - .sizeIn(minHeight = 24.dp, maxHeight = 58.dp) - .padding(8.dp) - .aspectRatio(1f) - .align(Alignment.Center), - ) - } - } -} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt new file mode 100644 index 00000000..febce604 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.components.AboutButton +import com.android.developers.androidify.theme.components.AndroidifyTopAppBar +import com.android.developers.androidify.theme.components.SquiggleBackground +import com.android.developers.androidify.util.AdaptivePreview +import com.android.developers.androidify.util.isAtLeastMedium + +@Composable +fun EditScreen( + snackbarHostState: SnackbarHostState, + dropBehaviourFactory: DropBehaviourFactory, + isExpanded: Boolean = isAtLeastMedium(), + onCameraPressed: () -> Unit, + onBackPressed: () -> Unit, + onAboutPressed: () -> Unit, + uiState: CreationState, + onChooseImageClicked: (PickVisualMedia.VisualMediaType) -> Unit, + onPromptOptionSelected: (PromptType) -> Unit, + onUndoPressed: () -> Unit, + onPromptGenerationPressed: () -> Unit, + onBotColorSelected: (BotColor) -> Unit, + onStartClicked: () -> Unit, + onDropCallback: (Uri) -> Unit = {}, +) { + EditScreenScaffold( + snackbarHostState, + topBar = { + AndroidifyTopAppBar( + backEnabled = true, + isMediumWindowSize = isExpanded, + onBackPressed = onBackPressed, + expandedCenterButtons = { + PromptTypeToolbar( + uiState.selectedPromptOption, + modifier = Modifier.padding(start = 16.dp, end = 16.dp), + onOptionSelected = onPromptOptionSelected, + ) + }, + actions = { + AboutButton { onAboutPressed() } + } + ) + }, + ) { contentPadding -> + SquiggleBackground(offsetHeightFraction = 0.5f) + Box( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .imePadding(), + ) { + if (isExpanded) { + EditScreenContentsMedium( + dropBehaviourFactory, + onCameraPressed, + uiState, + onChooseImageClicked, + onPromptOptionSelected, + onUndoPressed, + onPromptGenerationPressed, + onBotColorSelected, + onStartClicked, + onDropCallback, + ) + } else { + EditScreenContentsCompact( + dropBehaviourFactory, + onCameraPressed, + uiState, + onChooseImageClicked, + onPromptOptionSelected, + onUndoPressed, + onPromptGenerationPressed, + onBotColorSelected, + onStartClicked, + onDropCallback, + ) + } + } + } +} + +@Composable +fun EditScreenScaffold( + snackbarHostState: SnackbarHostState, + topBar: @Composable () -> Unit = {}, + content: @Composable (PaddingValues) -> Unit, +) { + Scaffold( + containerColor = MaterialTheme.colorScheme.surface, + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { snackbarData -> + Snackbar( + snackbarData, + shape = SnackbarDefaults.shape, + modifier = Modifier.padding(4.dp), + ) + }, + modifier = Modifier.safeContentPadding(), + ) + }, + topBar = topBar, + content = content, + ) +} + +@Composable +@AdaptivePreview +private fun EditScreenPreview() { + AndroidifyTheme { + SharedElementContextPreview { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + ) + } + } +} + +val fakeDropBehaviourFactory = object : DropBehaviourFactory { + override fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean { + TODO("Stub") + } + + override fun createTargetCallback( + activity: ComponentActivity, + onImageDropped: (Uri) -> Unit, + onDropStarted: () -> Unit, + onDropEnded: () -> Unit, + ): DragAndDropTarget { + TODO("Stub") + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt new file mode 100644 index 00000000..6635a99b --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.components.SecondaryOutlinedButton +import com.android.developers.androidify.util.PhonePreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.android.developers.androidify.creation.R as CreationR + +@Composable +fun EditScreenContentsCompact( + dropBehaviourFactory: DropBehaviourFactory, + onCameraPressed: () -> Unit, + uiState: CreationState, + onChooseImageClicked: (PickVisualMedia.VisualMediaType) -> Unit, + onPromptOptionSelected: (PromptType) -> Unit, + onUndoPressed: () -> Unit, + onPromptGenerationPressed: () -> Unit, + onBotColorSelected: (BotColor) -> Unit, + onStartClicked: () -> Unit, + onDropCallback: (Uri) -> Unit = {}, +) { + var showColorPickerBottomSheet by remember { mutableStateOf(false) } + Column( + modifier = Modifier.fillMaxSize(), + ) { + Spacer(modifier = Modifier.height(8.dp)) + PromptTypeToolbar( + uiState.selectedPromptOption, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp) + .align(Alignment.CenterHorizontally), + onOptionSelected = onPromptOptionSelected, + ) + + MainCreationPane( + uiState, + dropBehaviourFactory = dropBehaviourFactory, + modifier = Modifier.weight(1f), + onCameraPressed = onCameraPressed, + onChooseImageClicked = { + onChooseImageClicked(PickVisualMedia.ImageOnly) + }, + onUndoPressed = onUndoPressed, + onPromptGenerationPressed = onPromptGenerationPressed, + onSelectedPromptOptionChanged = onPromptOptionSelected, + onDropCallback = onDropCallback, + ) + + BottomButtons( + onButtonColorClicked = { + showColorPickerBottomSheet = !showColorPickerBottomSheet + }, + uiState = uiState, + onStartClicked = onStartClicked, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + BotColorPickerBottomSheet( + showColorPickerBottomSheet, + dismissBottomSheet = { + showColorPickerBottomSheet = false + }, + onColorChanged = onBotColorSelected, + listBotColors = uiState.listBotColors, + selectedBotColor = uiState.botColor, + ) +} + +@Composable +private fun BotColorPickerBottomSheet( + showColorPickerBottomSheet: Boolean, + dismissBottomSheet: () -> Unit, + onColorChanged: (BotColor) -> Unit, + listBotColors: List, + selectedBotColor: BotColor, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + if (showColorPickerBottomSheet) { + ModalBottomSheet( + modifier = Modifier, + sheetState = sheetState, + onDismissRequest = { + dismissBottomSheet() + }, + ) { + val scope = rememberCoroutineScope() + Column( + modifier = Modifier.padding( + start = 36.dp, + end = 36.dp, + top = 16.dp, + bottom = 8.dp, + ), + ) { + AndroidBotColorPicker( + selectedBotColor, + onBotColorSelected = { + onColorChanged(it) + scope.launch { + delay(400) + sheetState.hide() + }.invokeOnCompletion { + if (!sheetState.isVisible) { + dismissBottomSheet() + } + } + }, + listBotColor = listBotColors, + ) + } + } + } +} + +@Composable +private fun ColumnScope.BottomButtons( + onButtonColorClicked: () -> Unit, + uiState: CreationState, + onStartClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + FlowRow( + maxItemsInEachRow = 3, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .wrapContentSize() + .padding(8.dp) + .align(Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SecondaryOutlinedButton( + onClick = { + onButtonColorClicked() + }, + buttonText = stringResource(CreationR.string.bot_color_button), + modifier = Modifier.fillMaxRowHeight(), + leadingIcon = { + Row { + DisplayBotColor( + uiState.botColor, + modifier = Modifier + .clip(CircleShape) + .border( + 2.dp, + color = MaterialTheme.colorScheme.outline, + CircleShape, + ) + .size(32.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + } + }, + ) + TransformButton( + modifier = Modifier.fillMaxRowHeight(), + onClicked = onStartClicked, + ) + } +} + +@Composable +@PhonePreview +private fun EditScreenPreview() { + AndroidifyTheme { + SharedElementContextPreview { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + isExpanded = false, + ) + } + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt new file mode 100644 index 00000000..9587e567 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.util.LargeScreensPreview +import com.android.developers.androidify.creation.R as CreationR + +@Composable +fun EditScreenContentsMedium( + dropBehaviourFactory: DropBehaviourFactory, + onCameraPressed: () -> Unit, + uiState: CreationState, + onChooseImageClicked: (PickVisualMedia.VisualMediaType) -> Unit, + onPromptOptionSelected: (PromptType) -> Unit, + onUndoPressed: () -> Unit, + onPromptGenerationPressed: () -> Unit, + onBotColorSelected: (BotColor) -> Unit, + onStartClicked: () -> Unit, + onDropCallback: (Uri) -> Unit = {}, +) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp), + ) { + MainCreationPane( + uiState, + dropBehaviourFactory = dropBehaviourFactory, + modifier = Modifier.weight(.6f), + onCameraPressed = onCameraPressed, + onChooseImageClicked = { + onChooseImageClicked(PickVisualMedia.ImageOnly) + }, + onUndoPressed = onUndoPressed, + onPromptGenerationPressed = onPromptGenerationPressed, + onSelectedPromptOptionChanged = onPromptOptionSelected, + onDropCallback = onDropCallback, + ) + Box( + modifier = Modifier + .weight(.4f) + .padding(top = 16.dp, bottom = 16.dp) + .fillMaxSize() + .background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = MaterialTheme.shapes.large, + ) + .border( + width = 2.dp, + color = MaterialTheme.colorScheme.outline, + shape = MaterialTheme.shapes.large, + ), + ) { + AndroidBotColorPicker( + selectedBotColor = uiState.botColor, + modifier = Modifier.padding(16.dp), + onBotColorSelected = onBotColorSelected, + listBotColor = uiState.listBotColors, + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, end = 16.dp), + contentAlignment = Alignment.CenterEnd, + ) { + TransformButton( + modifier = Modifier.padding(bottom = 8.dp), + buttonText = stringResource(CreationR.string.start_transformation_button), + onClicked = onStartClicked, + ) + } + } +} + +@Composable +@LargeScreensPreview +private fun EditScreenPreview() { + AndroidifyTheme { + SharedElementContextPreview { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + ) + } + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt new file mode 100644 index 00000000..f12beb72 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt @@ -0,0 +1,362 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialShapes +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.material3.toShape +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.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.Role +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.sp +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.rectangle +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.LocalSharedTransitionScope +import com.android.developers.androidify.theme.R +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.SharedElementKey +import com.android.developers.androidify.theme.components.ScaleIndicationNodeFactory +import com.android.developers.androidify.theme.components.SecondaryOutlinedButton +import com.android.developers.androidify.theme.sharedBoundsRevealWithShapeMorph +import com.android.developers.androidify.theme.sharedBoundsWithDefaults +import com.android.developers.androidify.util.dashedRoundedRectBorder +import com.android.developers.androidify.creation.R as CreationR + +@Composable +fun PhotoPrompt( + uiState: CreationState, + dropBehaviourFactory: DropBehaviourFactory, + onCameraPressed: () -> Unit, + onChooseImageClicked: () -> Unit, + onDropCallback: (Uri) -> Unit, + onUndoPressed: () -> Unit, +) { + val defaultDropAreaBackgroundColor = MaterialTheme.colorScheme.surface + val alternateDropAreaBackgroundColor = MaterialTheme.colorScheme.surfaceVariant + var background by remember { mutableStateOf(defaultDropAreaBackgroundColor) } + + val activity = LocalActivity.current as? ComponentActivity + val externalAppCallback = remember { + dropBehaviourFactory.createTargetCallback( + activity = activity ?: return@remember null, + onImageDropped = { uri -> onDropCallback(uri) }, + onDropStarted = { background = alternateDropAreaBackgroundColor }, + onDropEnded = { background = defaultDropAreaBackgroundColor }, + ) + } + + val imageUri = uiState.imageUri + if (imageUri == null) { + UploadEmptyState( + modifier = Modifier + .background( + color = background, + shape = MaterialTheme.shapes.large, + ) + .dashedRoundedRectBorder( + 2.dp, + MaterialTheme.colorScheme.outline, + cornerRadius = 28.dp, + ) + .apply { + if (externalAppCallback != null) { + dragAndDropTarget( + shouldStartDragAndDrop = { event -> + dropBehaviourFactory.shouldStartDragAndDrop( + event, + ) + }, + target = externalAppCallback, + ) + } + } + .fillMaxSize() + .padding(2.dp), + onCameraPressed = onCameraPressed, + onChooseImagePress = onChooseImageClicked, + ) + } else { + ImagePreviewUri( + uri = imageUri, + onUndoPressed = onUndoPressed, + onChooseImagePressed = onChooseImageClicked, + modifier = Modifier + .fillMaxSize() + .heightIn(min = 200.dp), + ) + } +} + +@Composable +private fun UploadEmptyState( + onCameraPressed: () -> Unit, + onChooseImagePress: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + stringResource(CreationR.string.photo_picker_title), + fontSize = 28.sp, + textAlign = TextAlign.Center, + lineHeight = 40.sp, + minLines = 2, + maxLines = 2, + ) + Spacer(modifier = Modifier.height(16.dp)) + TakePhotoButton(onCameraPressed) + Spacer(modifier = Modifier.height(32.dp)) + SecondaryOutlinedButton( + onClick = { + onChooseImagePress() + }, + leadingIcon = { + Image( + painterResource(CreationR.drawable.choose_picture_image), + contentDescription = null, + modifier = Modifier + .padding(end = 8.dp) + .size(24.dp), + ) + }, + buttonText = stringResource(CreationR.string.photo_picker_choose_photo_label), + ) + } +} + +@Composable +private fun TakePhotoButton(onCameraPressed: () -> Unit) { + val interactionSource = remember { MutableInteractionSource() } + val animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec() + val sharedElementScope = LocalSharedTransitionScope.current + with(sharedElementScope) { + Box( + modifier = Modifier + .defaultMinSize(minHeight = 48.dp, minWidth = 48.dp) + .sizeIn( + minHeight = 48.dp, + maxHeight = ButtonDefaults.ExtraLargeContainerHeight, + minWidth = 48.dp, + maxWidth = ButtonDefaults.ExtraLargeContainerHeight, + ) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .indication(interactionSource, ScaleIndicationNodeFactory(animationSpec)) + .background( + MaterialTheme.colorScheme.onSurface, + MaterialShapes.Cookie9Sided.toShape(), + ) + .clickable( + interactionSource = interactionSource, + indication = ripple(color = Color.White), + onClick = { + onCameraPressed() + }, + role = Role.Button, + enabled = true, + onClickLabel = stringResource(CreationR.string.take_picture_content_description), + ) + .sharedBoundsRevealWithShapeMorph( + rememberSharedContentState(SharedElementKey.CameraButtonToFullScreenCamera), + restingShape = MaterialShapes.Cookie9Sided, + targetShape = RoundedPolygon.rectangle().normalized(), + targetValueByState = { + when (it) { + EnterExitState.PreEnter -> 0f + EnterExitState.Visible -> 1f + EnterExitState.PostExit -> 1f + } + }, + ), + ) { + Image( + painterResource(R.drawable.photo_camera), + contentDescription = stringResource(CreationR.string.take_picture_content_description), + modifier = Modifier + .sizeIn(minHeight = 24.dp, maxHeight = 58.dp) + .padding(8.dp) + .aspectRatio(1f) + .align(Alignment.Center), + ) + } + } +} + +@Composable +private fun ImagePreviewUri( + modifier: Modifier = Modifier, + uri: Uri, + onUndoPressed: () -> Unit, + onChooseImagePressed: () -> Unit, +) { + val sharedElementScope = LocalSharedTransitionScope.current + with(sharedElementScope) { + ImagePreview(modifier, onUndoPressed, onChooseImagePressed) { + AsyncImage( + ImageRequest.Builder(LocalContext.current).data(uri).crossfade(false).build(), + placeholder = null, + contentDescription = stringResource(CreationR.string.cd_selected_image), + modifier = Modifier + .align(Alignment.Center) + .sharedBoundsWithDefaults(rememberSharedContentState(SharedElementKey.CaptureImageToDetails)) + .clip(MaterialTheme.shapes.large) + .fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } + } +} + +@Composable +private fun ImagePreview( + modifier: Modifier = Modifier, + onUndoPressed: () -> Unit, + onChooseImagePressed: () -> Unit, + content: @Composable BoxScope.() -> Unit, +) { + Box(modifier) { + content(this@Box) + + Row( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(8.dp), + ) { + SecondaryOutlinedButton( + onClick = { + onUndoPressed() + }, + leadingIcon = { + Icon( + ImageVector.vectorResource(CreationR.drawable.rounded_redo_24), + contentDescription = stringResource(CreationR.string.cd_retake_photo), + ) + }, + ) + Spacer(modifier = Modifier.width(8.dp)) + SecondaryOutlinedButton( + onClick = { + onChooseImagePressed() + }, + buttonText = stringResource(CreationR.string.photo_picker_choose_photo_label), // Reusing existing + leadingIcon = { + Icon( + ImageVector.vectorResource(CreationR.drawable.rounded_photo_24), + contentDescription = stringResource(CreationR.string.cd_choose_photo), + ) + }, + ) + } + } +} + +@Preview +@Composable +fun UploadEmptyPreview() { + AndroidifyTheme { + SharedElementContextPreview { + UploadEmptyState( + onCameraPressed = {}, + onChooseImagePress = {}, + ) + } + } +} + +@Preview +@Composable +fun ImagePreviewPreview() { + AndroidifyTheme { + SharedElementContextPreview { + ImagePreview( + onUndoPressed = {}, + onChooseImagePressed = {}, + ) { + val bitmap = ImageBitmap.imageResource(com.android.developers.androidify.results.R.drawable.placeholderbot) + Image(bitmap = bitmap, contentDescription = null) + } + } + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt new file mode 100644 index 00000000..b3b80922 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import android.net.Uri +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingToolbarColors +import androidx.compose.material3.HorizontalFloatingToolbar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.AndroidifyTheme +import kotlinx.coroutines.launch + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun MainCreationPane( + uiState: CreationState, + dropBehaviourFactory: DropBehaviourFactory, + modifier: Modifier = Modifier, + onCameraPressed: () -> Unit, + onChooseImageClicked: () -> Unit = {}, + onUndoPressed: () -> Unit = {}, + onPromptGenerationPressed: () -> Unit, + onSelectedPromptOptionChanged: (PromptType) -> Unit, + onDropCallback: (Uri) -> Unit, +) { + PromptTypePager(modifier, uiState, onSelectedPromptOptionChanged) { + when (it) { + PromptType.PHOTO -> { + PhotoPrompt( + uiState = uiState, + dropBehaviourFactory = dropBehaviourFactory, + onCameraPressed = onCameraPressed, + onChooseImageClicked = onChooseImageClicked, + onDropCallback = onDropCallback, + onUndoPressed = onUndoPressed, + ) + } + + PromptType.TEXT -> { + TextPrompt( + textFieldState = uiState.descriptionText, + promptGenerationInProgress = uiState.promptGenerationInProgress, + generatedPrompt = uiState.generatedPrompt, + onPromptGenerationPressed = onPromptGenerationPressed, + modifier = Modifier + .fillMaxSize() + .heightIn(min = 200.dp) + .padding(2.dp), + ) + } + } + } +} + +@Composable +private fun PromptTypePager( + modifier: Modifier = Modifier, + uiState: CreationState, + onSelectedPromptOptionChanged: (PromptType) -> Unit, + content: @Composable PagerScope.(PromptType) -> Unit, +) { + Box( + modifier = modifier, + ) { + val spatialSpec = MaterialTheme.motionScheme.slowSpatialSpec() + val pagerState = + rememberPagerState(uiState.selectedPromptOption.ordinal) { PromptType.entries.size } + val focusManager = LocalFocusManager.current + LaunchedEffect(uiState.selectedPromptOption) { + launch { + pagerState.animateScrollToPage( + uiState.selectedPromptOption.ordinal, + animationSpec = spatialSpec, + ) + }.invokeOnCompletion { + if (uiState.selectedPromptOption != PromptType.entries[pagerState.currentPage]) { + onSelectedPromptOptionChanged(PromptType.entries[pagerState.currentPage]) + } + } + } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + onSelectedPromptOptionChanged(PromptType.entries[page]) + } + } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.targetPage }.collect { + if (pagerState.targetPage != PromptType.TEXT.ordinal) { + focusManager.clearFocus() + } + } + } + HorizontalPager( + pagerState, + modifier.fillMaxSize(), + pageSpacing = 16.dp, + contentPadding = PaddingValues(16.dp), + pageContent = { + content(this, PromptType.entries[it]) + }, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun PromptTypeToolbar( + selectedOption: PromptType, + modifier: Modifier = Modifier, + onOptionSelected: (PromptType) -> Unit, +) { + val options = PromptType.entries + HorizontalFloatingToolbar( + modifier = modifier.border( + 2.dp, + color = MaterialTheme.colorScheme.outline, + shape = MaterialTheme.shapes.large, + ), + colors = FloatingToolbarColors( + toolbarContainerColor = MaterialTheme.colorScheme.surface, + toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + fabContainerColor = MaterialTheme.colorScheme.tertiary, + fabContentColor = MaterialTheme.colorScheme.onTertiary, + ), + expanded = true, + ) { + options.forEachIndexed { index, label -> + ToggleButton( + modifier = Modifier, + checked = selectedOption == label, + onCheckedChange = { onOptionSelected(label) }, + shapes = ToggleButtonDefaults.shapes(checkedShape = MaterialTheme.shapes.large), + colors = ToggleButtonDefaults.toggleButtonColors( + checkedContainerColor = MaterialTheme.colorScheme.onSurface, + containerColor = MaterialTheme.colorScheme.surface, + ), + ) { + Text(label.displayName, maxLines = 1) + } + if (index != options.size - 1) { + Spacer(Modifier.width(8.dp)) + } + } + } +} + +@Preview +@Composable +private fun PromptTypeToolbarPreview() { + AndroidifyTheme { + PromptTypeToolbar( + selectedOption = PromptType.PHOTO, + onOptionSelected = {}, + ) + } +} \ No newline at end of file diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/TextPrompt.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/TextPrompt.kt new file mode 100644 index 00000000..f2a8e757 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/TextPrompt.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2025 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 + * + * https://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. + */ +@file:OptIn( + ExperimentalLayoutApi::class, + ExperimentalSharedTransitionApi::class, + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalMaterial3Api::class, +) + +package com.android.developers.androidify.creation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.LimeGreen +import com.android.developers.androidify.theme.Primary90 +import com.android.developers.androidify.theme.Secondary +import com.android.developers.androidify.theme.components.GradientAssistElevatedChip +import com.android.developers.androidify.theme.components.gradientChipColorDefaults +import com.android.developers.androidify.theme.components.infinitelyAnimatingLinearGradient +import com.android.developers.androidify.util.AnimatedTextField +import com.android.developers.androidify.util.dashedRoundedRectBorder +import com.android.developers.androidify.creation.R as CreationR + +@Preview(showBackground = true) +@Composable +private fun TextPromptGenerationPreview() { + AndroidifyTheme { + TextPrompt( + TextFieldState(), + false, + generatedPrompt = "wearing a red sweater", + onPromptGenerationPressed = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun TextPromptGenerationInProgressPreview() { + AndroidifyTheme { + TextPrompt( + TextFieldState(), + true, + generatedPrompt = "wearing a red sweater", + onPromptGenerationPressed = {}, + ) + } +} + +@Composable +fun TextPrompt( + textFieldState: TextFieldState, + promptGenerationInProgress: Boolean, + modifier: Modifier = Modifier, + generatedPrompt: String? = null, + onPromptGenerationPressed: () -> Unit, +) { + Column(modifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + rememberVectorPainter(ImageVector.vectorResource(CreationR.drawable.rounded_draw_24)), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + stringResource(CreationR.string.headline_my_bot_is), + style = MaterialTheme.typography.headlineLarge, + fontSize = 24.sp, + ) + } + Spacer(modifier = Modifier.size(8.dp)) + Column( + modifier = Modifier + .dashedRoundedRectBorder( + 2.dp, + MaterialTheme.colorScheme.outline, + cornerRadius = 28.dp, + ) + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 16.dp) + .fillMaxSize(), + ) { + AnimatedTextField( + textFieldState, + targetEndState = generatedPrompt, + modifier = Modifier + .weight(1f) + .fillMaxSize(), + textStyle = TextStyle(fontSize = 24.sp), + decorator = { innerTextField -> + if (textFieldState.text.isEmpty()) { + Text( + stringResource(CreationR.string.prompt_text_hint).trimIndent(), + color = Color.Gray, + fontSize = 24.sp, + ) + } + innerTextField() + }, + ) + AnimatedVisibility( + !WindowInsets.isImeVisible, + enter = fadeIn(MaterialTheme.motionScheme.defaultEffectsSpec()), + exit = fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()), + ) { + HelpMeWriteButton(promptGenerationInProgress, onPromptGenerationPressed) + } + } + } +} + +@Composable +private fun HelpMeWriteButton( + promptGenerationInProgress: Boolean, + onPromptGenerationPressed: () -> Unit, +) { + val color = if (promptGenerationInProgress) { + Brush.infinitelyAnimatingLinearGradient( + listOf( + LimeGreen, + Primary90, + Secondary, + ), + ) + } else { + SolidColor(MaterialTheme.colorScheme.surfaceContainerLow) + } + GradientAssistElevatedChip( + onClick = { + onPromptGenerationPressed() + }, + label = { + if (promptGenerationInProgress) { + Text(stringResource(CreationR.string.writing)) + } else { + Text(stringResource(CreationR.string.write_me_a_prompt)) + } + }, + leadingIcon = { + Icon( + rememberVectorPainter(ImageVector.vectorResource(CreationR.drawable.pen_spark_24)), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + colors = gradientChipColorDefaults().copy( + containerColor = color, + disabledContainerColor = color, + ), + enabled = !promptGenerationInProgress, + ) +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/TransformButton.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/TransformButton.kt new file mode 100644 index 00000000..044eb1f0 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/TransformButton.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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 + * + * https://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.android.developers.androidify.creation + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.theme.components.PrimaryButton +import com.android.developers.androidify.creation.R as CreationR + +@Composable +@Preview +fun TransformButton( + modifier: Modifier = Modifier, + buttonText: String = stringResource(CreationR.string.transform_button), + onClicked: () -> Unit = {}, +) { + PrimaryButton( + modifier = modifier, + onClick = onClicked, + buttonText = buttonText, + trailingIcon = { + Row { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + ImageVector.vectorResource(com.android.developers.androidify.theme.R.drawable.rounded_arrow_forward_24), + contentDescription = null, + ) + } + }, + ) +} From bef67d035055540d374ff8298cd5da66ad4ee45d Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 16:11:23 +0200 Subject: [PATCH 2/7] Pull out the squiggle used in Home so it can be used in Create too --- .../androidify/xr/SpatialComponents.kt | 58 +++++++++++++++++++ .../androidify/home/xr/HomeScreenSpatial.kt | 58 +++++++++---------- 2 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 core/xr/src/main/java/com/android/developers/androidify/xr/SpatialComponents.kt diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialComponents.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialComponents.kt new file mode 100644 index 00000000..79b03da2 --- /dev/null +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialComponents.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 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 + * + * https://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.android.developers.androidify.xr + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialBox +import androidx.xr.compose.subspace.SpatialBoxScope +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.SubspaceComposable +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.aspectRatio +import androidx.xr.compose.subspace.layout.fillMaxWidth +import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.offset +import androidx.xr.compose.subspace.layout.resizable +import com.android.developers.androidify.theme.components.SquiggleBackgroundFull + +/** + * A composable for a Subspace with a Squiggle background. + * This Subspace is generally the top-level Subspace. It contains a full-sized squiggle background + * that is grabbable and movable, allowing all child components to move with the background. + */ +@Composable +fun SquiggleBackgroundSubspace( + content: + @SubspaceComposable @Composable + SpatialBoxScope.() -> Unit, +) { + Subspace { + SpatialPanel( + SubspaceModifier + .movable() + .resizable() + .fillMaxWidth(1f) + .aspectRatio(1.7f), + ) { + SquiggleBackgroundFull() + Subspace { + SpatialBox(SubspaceModifier.offset(z = 10.dp), content = content) + } + } + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt index 38a5c4bd..6e29dc2b 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt @@ -42,6 +42,7 @@ import androidx.xr.compose.subspace.layout.SpatialAlignment import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.aspectRatio import androidx.xr.compose.subspace.layout.fillMaxHeight +import androidx.xr.compose.subspace.layout.fillMaxSize import androidx.xr.compose.subspace.layout.fillMaxWidth import androidx.xr.compose.subspace.layout.movable import androidx.xr.compose.subspace.layout.offset @@ -58,6 +59,7 @@ import com.android.developers.androidify.util.TabletPreview import com.android.developers.androidify.xr.DisableSharedTransition import com.android.developers.androidify.xr.MainPanelWorkaround import com.android.developers.androidify.xr.RequestHomeSpaceIconButton +import com.android.developers.androidify.xr.SquiggleBackgroundSubspace @Composable fun HomeScreenContentsSpatial( @@ -67,43 +69,35 @@ fun HomeScreenContentsSpatial( onAboutClicked: () -> Unit, ) { DisableSharedTransition { - Subspace { + SquiggleBackgroundSubspace { MainPanelWorkaround() + Orbiter( + position = ContentEdge.Top, + offsetType = OrbiterOffsetType.OuterEdge, + offset = 32.dp, + alignment = Alignment.End, + ) { + RequestHomeSpaceIconButton( + modifier = Modifier + .size(64.dp, 64.dp) + .padding(8.dp), + ) + } + SpatialPanel(SubspaceModifier.fillMaxSize()) { + HomeScreenSpatialMainContent(dancingBotLink, onClickLetsGo, onAboutClicked) + } SpatialPanel( SubspaceModifier + .fillMaxWidth(0.2f) + .fillMaxHeight(0.8f) + .aspectRatio(0.77f) + .resizable(maintainAspectRatio = true) .movable() - .resizable() - .fillMaxWidth(1f) - .aspectRatio(1.7f), + .align(SpatialAlignment.CenterRight) + .offset(z = 10.dp) + .rotate(0f, 0f, 5f), ) { - Orbiter( - position = ContentEdge.Top, - offsetType = OrbiterOffsetType.OuterEdge, - offset = 32.dp, - alignment = Alignment.End, - ) { - RequestHomeSpaceIconButton( - modifier = Modifier - .size(64.dp, 64.dp) - .padding(8.dp), - ) - } - HomeScreenSpatialMainContent(dancingBotLink, onClickLetsGo, onAboutClicked) - Subspace { - SpatialPanel( - SubspaceModifier - .fillMaxWidth(0.2f) - .fillMaxHeight(0.8f) - .aspectRatio(0.77f) - .resizable(maintainAspectRatio = true) - .movable() - .align(SpatialAlignment.CenterRight) - .offset(z = 10.dp) - .rotate(0f, 0f, 5f), - ) { - VideoPlayer(videoLink) - } - } + VideoPlayer(videoLink) } } } From ff13fd0e85b5826755611ada6e4115f9a456d532 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 9 Sep 2025 17:08:08 +0200 Subject: [PATCH 3/7] Extract colors into RequestHomeSpaceIconButton parameters for better reuse --- .../developers/androidify/xr/SpatialUiModes.kt | 11 ++++++----- .../androidify/home/xr/HomeScreenSpatial.kt | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt index 1222357e..4b51981b 100644 --- a/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt @@ -19,8 +19,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -45,14 +45,15 @@ fun SpatialCapabilities.couldRequestHomeSpace(): Boolean { /** Default styling for an IconButton with a home space button and behavior. */ @Composable -fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { +fun RequestHomeSpaceIconButton( + modifier: Modifier = Modifier, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), +) { val session = LocalSession.current ?: return IconButton( modifier = modifier, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - ), + colors = colors, onClick = { session.scene.requestHomeSpaceMode() }, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt index 6e29dc2b..815c2001 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -81,6 +83,9 @@ fun HomeScreenContentsSpatial( modifier = Modifier .size(64.dp, 64.dp) .padding(8.dp), + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + ), ) } SpatialPanel(SubspaceModifier.fillMaxSize()) { From c2459f44efe36a6a954293c63794f5a837d471e5 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 16:01:45 +0200 Subject: [PATCH 4/7] Add XR libraries to Creation Screen --- feature/creation/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature/creation/build.gradle.kts b/feature/creation/build.gradle.kts index bfcbc42c..3a24a052 100644 --- a/feature/creation/build.gradle.kts +++ b/feature/creation/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui.compose) implementation(libs.androidx.media3.ui) // for string resources only + implementation(libs.androidx.xr.compose) ksp(libs.hilt.compiler) implementation(libs.androidx.ui.tooling) @@ -82,6 +83,7 @@ dependencies { implementation(projects.core.theme) implementation(projects.core.util) + implementation(projects.core.xr) implementation(projects.data) implementation(projects.feature.results) testImplementation(libs.hilt.android.testing) From aece4022fd07fae9f9546afa0ce6ec9d88b1966b Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 16:12:33 +0200 Subject: [PATCH 5/7] Create Spatial layout for Edit Screen --- feature/creation/build.gradle.kts | 1 + .../androidify/creation/CreationScreenTest.kt | 16 +- .../androidify/creation/CreationScreen.kt | 5 +- .../androidify/creation/CreationViewModel.kt | 5 +- .../androidify/creation/EditScreen.kt | 132 +++++++++---- .../androidify/creation/EditScreenCompact.kt | 2 +- .../creation/EditScreenLayoutType.kt | 35 ++++ .../androidify/creation/EditScreenMedium.kt | 4 +- .../androidify/creation/PromptTypePager.kt | 2 +- .../xr/EditScreenHomeSpaceModePreviews.kt | 80 ++++++++ .../creation/xr/EditScreenSpatial.kt | 181 ++++++++++++++++++ .../creation/CreationViewModelTest.kt | 11 +- 12 files changed, 421 insertions(+), 53 deletions(-) create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenLayoutType.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenHomeSpaceModePreviews.kt create mode 100644 feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenSpatial.kt diff --git a/feature/creation/build.gradle.kts b/feature/creation/build.gradle.kts index 3a24a052..7362cd90 100644 --- a/feature/creation/build.gradle.kts +++ b/feature/creation/build.gradle.kts @@ -98,6 +98,7 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(projects.core.testing) + implementation(projects.core.network) kspAndroidTest(libs.hilt.compiler) debugImplementation(libs.androidx.ui.test.manifest) diff --git a/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt b/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt index 3e804199..2f0fa383 100644 --- a/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt +++ b/feature/creation/src/androidTest/java/com/android/developers/androidify/creation/CreationScreenTest.kt @@ -47,7 +47,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -76,7 +76,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -106,7 +106,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -141,7 +141,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -175,7 +175,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -210,7 +210,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -244,7 +244,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, @@ -274,7 +274,7 @@ class CreationScreenTest { EditScreen( snackbarHostState = SnackbarHostState(), dropBehaviourFactory = FakeDropImageFactory(), - isExpanded = true, // Expanded mode + layoutType = EditScreenLayoutType.Medium, onCameraPressed = {}, onBackPressed = {}, onAboutPressed = {}, diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt index 37b6258c..751ed692 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt @@ -39,18 +39,17 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.developers.androidify.customize.CustomizeAndExportScreen import com.android.developers.androidify.customize.CustomizeExportViewModel import com.android.developers.androidify.results.ResultsScreen -import com.android.developers.androidify.util.isAtLeastMedium @Composable fun CreationScreen( fileName: String? = null, creationViewModel: CreationViewModel = hiltViewModel(), - isMedium: Boolean = isAtLeastMedium(), onCameraPressed: () -> Unit = {}, onBackPressed: () -> Unit, onAboutPressed: () -> Unit, ) { val uiState by creationViewModel.uiState.collectAsStateWithLifecycle() + val layoutType = calculateLayoutType(uiState.xrEnabled) BackHandler( enabled = uiState.screenState != ScreenState.EDIT, ) { @@ -74,7 +73,7 @@ fun CreationScreen( EditScreen( snackbarHostState = snackbarHostState, dropBehaviourFactory = creationViewModel.dropBehaviourFactory, - isExpanded = isMedium, + layoutType = layoutType, onCameraPressed = onCameraPressed, onBackPressed = onBackPressed, onAboutPressed = onAboutPressed, diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index b9e34558..1d50984e 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.developers.androidify.data.ConfigProvider import com.android.developers.androidify.data.DropBehaviourFactory import com.android.developers.androidify.data.ImageDescriptionFailedGenerationException import com.android.developers.androidify.data.ImageGenerationRepository @@ -52,6 +53,7 @@ class CreationViewModel @Inject constructor( val dropBehaviourFactory: DropBehaviourFactory, @ApplicationContext val context: Context, + configProvider: ConfigProvider, ) : ViewModel() { init { @@ -61,7 +63,7 @@ class CreationViewModel @Inject constructor( } } - private var _uiState = MutableStateFlow(CreationState()) + private var _uiState = MutableStateFlow(CreationState(xrEnabled = configProvider.isXrEnabled())) val uiState: StateFlow get() = _uiState @@ -253,6 +255,7 @@ data class CreationState( val promptGenerationInProgress: Boolean = false, val screenState: ScreenState = ScreenState.EDIT, val resultBitmap: Bitmap? = null, + val xrEnabled: Boolean = false, ) enum class ScreenState { diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt index febce604..333caee1 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreen.kt @@ -46,20 +46,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.unit.dp +import com.android.developers.androidify.creation.xr.EditScreenSpatial import com.android.developers.androidify.data.DropBehaviourFactory import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AboutButton import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.SquiggleBackground -import com.android.developers.androidify.util.AdaptivePreview -import com.android.developers.androidify.util.isAtLeastMedium +import com.android.developers.androidify.util.LargeScreensPreview +import com.android.developers.androidify.util.PhonePreview +import com.android.developers.androidify.xr.RequestFullSpaceIconButton +import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun EditScreen( snackbarHostState: SnackbarHostState, dropBehaviourFactory: DropBehaviourFactory, - isExpanded: Boolean = isAtLeastMedium(), + layoutType: EditScreenLayoutType, onCameraPressed: () -> Unit, onBackPressed: () -> Unit, onAboutPressed: () -> Unit, @@ -71,13 +74,73 @@ fun EditScreen( onBotColorSelected: (BotColor) -> Unit, onStartClicked: () -> Unit, onDropCallback: (Uri) -> Unit = {}, +) { + when (layoutType) { + EditScreenLayoutType.Compact -> + EditScreenScaffoldWithAppBar(snackbarHostState, layoutType, onBackPressed, onAboutPressed, uiState, onPromptOptionSelected) { + EditScreenContentsCompact( + dropBehaviourFactory, + onCameraPressed, + uiState, + onChooseImageClicked, + onPromptOptionSelected, + onUndoPressed, + onPromptGenerationPressed, + onBotColorSelected, + onStartClicked, + onDropCallback, + ) + } + EditScreenLayoutType.Medium -> + EditScreenScaffoldWithAppBar(snackbarHostState, layoutType, onBackPressed, onAboutPressed, uiState, onPromptOptionSelected) { + EditScreenContentsMedium( + dropBehaviourFactory, + onCameraPressed, + uiState, + onChooseImageClicked, + onPromptOptionSelected, + onUndoPressed, + onPromptGenerationPressed, + onBotColorSelected, + onStartClicked, + onDropCallback, + ) + } + EditScreenLayoutType.Spatial -> + EditScreenSpatial( + dropBehaviourFactory, + onCameraPressed, + onBackPressed, + onAboutPressed, + uiState, + snackbarHostState, + onChooseImageClicked, + onPromptOptionSelected, + onUndoPressed, + onPromptGenerationPressed, + onBotColorSelected, + onStartClicked, + onDropCallback, + ) + } +} + +@Composable +fun EditScreenScaffoldWithAppBar( + snackbarHostState: SnackbarHostState, + layoutType: EditScreenLayoutType, + onBackPressed: () -> Unit, + onAboutPressed: () -> Unit, + uiState: CreationState, + onPromptOptionSelected: (PromptType) -> Unit, + contents: @Composable () -> Unit, ) { EditScreenScaffold( snackbarHostState, topBar = { AndroidifyTopAppBar( backEnabled = true, - isMediumWindowSize = isExpanded, + isMediumWindowSize = layoutType == EditScreenLayoutType.Medium, onBackPressed = onBackPressed, expandedCenterButtons = { PromptTypeToolbar( @@ -88,7 +151,10 @@ fun EditScreen( }, actions = { AboutButton { onAboutPressed() } - } + if (couldRequestFullSpace()) { + RequestFullSpaceIconButton() + } + }, ) }, ) { contentPadding -> @@ -99,33 +165,7 @@ fun EditScreen( .padding(contentPadding) .imePadding(), ) { - if (isExpanded) { - EditScreenContentsMedium( - dropBehaviourFactory, - onCameraPressed, - uiState, - onChooseImageClicked, - onPromptOptionSelected, - onUndoPressed, - onPromptGenerationPressed, - onBotColorSelected, - onStartClicked, - onDropCallback, - ) - } else { - EditScreenContentsCompact( - dropBehaviourFactory, - onCameraPressed, - uiState, - onChooseImageClicked, - onPromptOptionSelected, - onUndoPressed, - onPromptGenerationPressed, - onBotColorSelected, - onStartClicked, - onDropCallback, - ) - } + contents() } } } @@ -156,9 +196,9 @@ fun EditScreenScaffold( ) } +@PhonePreview @Composable -@AdaptivePreview -private fun EditScreenPreview() { +private fun HomeScreenPhonePreview() { AndroidifyTheme { SharedElementContextPreview { EditScreen( @@ -175,11 +215,35 @@ private fun EditScreenPreview() { onDropCallback = {}, onBackPressed = {}, onAboutPressed = {}, + layoutType = EditScreenLayoutType.Compact, ) } } } +@LargeScreensPreview +@Composable +private fun HomeScreenLargeScreensPreview() { + SharedElementContextPreview { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + layoutType = EditScreenLayoutType.Medium, + ) + } +} + val fakeDropBehaviourFactory = object : DropBehaviourFactory { override fun shouldStartDragAndDrop(event: DragAndDropEvent): Boolean { TODO("Stub") diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt index 6635a99b..dd11a2d8 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenCompact.kt @@ -237,7 +237,7 @@ private fun EditScreenPreview() { onDropCallback = {}, onBackPressed = {}, onAboutPressed = {}, - isExpanded = false, + layoutType = EditScreenLayoutType.Compact, ) } } diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenLayoutType.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenLayoutType.kt new file mode 100644 index 00000000..60e697b8 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenLayoutType.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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 + * + * https://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.android.developers.androidify.creation + +import androidx.compose.runtime.Composable +import com.android.developers.androidify.util.isAtLeastMedium +import com.android.developers.androidify.xr.LocalSpatialCapabilities + +enum class EditScreenLayoutType { + Compact, + Medium, + Spatial, +} + +@Composable +fun calculateLayoutType(enableXr: Boolean = false): EditScreenLayoutType { + return when { + LocalSpatialCapabilities.current.isSpatialUiEnabled && enableXr -> EditScreenLayoutType.Spatial + isAtLeastMedium() -> EditScreenLayoutType.Medium + else -> EditScreenLayoutType.Compact + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt index 9587e567..6c6e9c64 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/EditScreenMedium.kt @@ -36,14 +36,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -148,6 +145,7 @@ private fun EditScreenPreview() { onDropCallback = {}, onBackPressed = {}, onAboutPressed = {}, + layoutType = EditScreenLayoutType.Medium, ) } } diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt index b3b80922..71e4f4d6 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt @@ -198,4 +198,4 @@ private fun PromptTypeToolbarPreview() { onOptionSelected = {}, ) } -} \ No newline at end of file +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenHomeSpaceModePreviews.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenHomeSpaceModePreviews.kt new file mode 100644 index 00000000..feabcc16 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenHomeSpaceModePreviews.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2025 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 + * + * https://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.android.developers.androidify.creation.xr + +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import com.android.developers.androidify.creation.CreationState +import com.android.developers.androidify.creation.EditScreen +import com.android.developers.androidify.creation.EditScreenLayoutType +import com.android.developers.androidify.creation.fakeDropBehaviourFactory +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider +import com.android.developers.androidify.xr.XrHomeSpaceCompactPreview +import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview + +@XrHomeSpaceMediumPreview +@ExperimentalMaterial3ExpressiveApi +@Composable +private fun EditScreenMediumXrHomeSpaceModePreview() { + SharedElementContextPreview { + SupportsFullSpaceModeRequestProvider { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + layoutType = EditScreenLayoutType.Medium, + ) + } + } +} + +@ExperimentalMaterial3ExpressiveApi +@XrHomeSpaceCompactPreview +@Composable +private fun EditScreenCompactXrHomeSpaceModePreview() { + SharedElementContextPreview { + SupportsFullSpaceModeRequestProvider { + EditScreen( + snackbarHostState = SnackbarHostState(), + dropBehaviourFactory = fakeDropBehaviourFactory, + onCameraPressed = { }, + uiState = CreationState(), + onChooseImageClicked = {}, + onPromptOptionSelected = {}, + onUndoPressed = {}, + onPromptGenerationPressed = {}, + onBotColorSelected = {}, + onStartClicked = {}, + onDropCallback = {}, + onBackPressed = {}, + onAboutPressed = {}, + layoutType = EditScreenLayoutType.Compact, + ) + } + } +} diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenSpatial.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenSpatial.kt new file mode 100644 index 00000000..8f7632d1 --- /dev/null +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/xr/EditScreenSpatial.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2025 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 + * + * https://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.android.developers.androidify.creation.xr + +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.xr.compose.spatial.ContentEdge +import androidx.xr.compose.spatial.Orbiter +import androidx.xr.compose.subspace.SpatialColumn +import androidx.xr.compose.subspace.SpatialLayoutSpacer +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.SpatialRow +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.fillMaxHeight +import androidx.xr.compose.subspace.layout.fillMaxWidth +import androidx.xr.compose.subspace.layout.offset +import androidx.xr.compose.subspace.layout.width +import com.android.developers.androidify.creation.AndroidBotColorPicker +import com.android.developers.androidify.creation.BotColor +import com.android.developers.androidify.creation.CreationState +import com.android.developers.androidify.creation.EditScreenScaffold +import com.android.developers.androidify.creation.MainCreationPane +import com.android.developers.androidify.creation.PromptType +import com.android.developers.androidify.creation.PromptTypeToolbar +import com.android.developers.androidify.creation.TransformButton +import com.android.developers.androidify.data.DropBehaviourFactory +import com.android.developers.androidify.theme.components.AboutButton +import com.android.developers.androidify.theme.components.AndroidifyTopAppBar +import com.android.developers.androidify.xr.DisableSharedTransition +import com.android.developers.androidify.xr.MainPanelWorkaround +import com.android.developers.androidify.xr.RequestHomeSpaceIconButton +import com.android.developers.androidify.xr.SquiggleBackgroundSubspace +import com.android.developers.androidify.creation.R as CreationR + +@Composable +fun EditScreenSpatial( + dropBehaviourFactory: DropBehaviourFactory, + onCameraPressed: () -> Unit, + onBackPressed: () -> Unit, + onAboutPressed: () -> Unit, + uiState: CreationState, + snackbarHostState: SnackbarHostState, + onChooseImageClicked: (PickVisualMedia.VisualMediaType) -> Unit, + onPromptOptionSelected: (PromptType) -> Unit, + onUndoPressed: () -> Unit, + onPromptGenerationPressed: () -> Unit, + onBotColorSelected: (BotColor) -> Unit, + onStartClicked: () -> Unit, + onDropCallback: (Uri) -> Unit = {}, +) { + DisableSharedTransition { + SquiggleBackgroundSubspace { + MainPanelWorkaround() + SpatialColumn(SubspaceModifier.fillMaxWidth()) { + SpatialPanel( + SubspaceModifier.offset(z = 10.dp) + .fillMaxWidth(0.5f), + ) { + Column( + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = MaterialTheme.shapes.large, + ), + ) { + AndroidifyTopAppBar( + backEnabled = true, + isMediumWindowSize = true, + onBackPressed = onBackPressed, + actions = { + AboutButton { + onAboutPressed() + } + RequestHomeSpaceIconButton() + }, + expandedCenterButtons = { + PromptTypeToolbar( + uiState.selectedPromptOption, + modifier = Modifier.padding(start = 16.dp, end = 16.dp), + onOptionSelected = onPromptOptionSelected, + ) + }, + ) + Spacer(Modifier.height(16.dp)) + } + } + + SpatialRow(SubspaceModifier.fillMaxWidth(0.7f)) { + SpatialPanel( + modifier = SubspaceModifier + .offset(z = 10.dp) + .weight(1.3f) + .fillMaxHeight(0.8f), + ) { + Box( + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = MaterialTheme.shapes.large, + ), + ) { + EditScreenScaffold( + snackbarHostState = snackbarHostState, + topBar = {}, + ) { + MainCreationPane( + uiState = uiState, + dropBehaviourFactory = dropBehaviourFactory, + onCameraPressed = onCameraPressed, + onChooseImageClicked = { + onChooseImageClicked(PickVisualMedia.ImageOnly) + }, + onUndoPressed = onUndoPressed, + onPromptGenerationPressed = onPromptGenerationPressed, + onSelectedPromptOptionChanged = onPromptOptionSelected, + onDropCallback = onDropCallback, + ) + } + } + } + SpatialLayoutSpacer(SubspaceModifier.width(48.dp)) + SpatialPanel( + modifier = SubspaceModifier + .offset(z = 10.dp) + .weight(1f) + .fillMaxHeight(0.8f), + ) { + Box( + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = MaterialTheme.shapes.large, + ), + ) { + AndroidBotColorPicker( + selectedBotColor = uiState.botColor, + modifier = Modifier.padding(16.dp), + onBotColorSelected = onBotColorSelected, + listBotColor = uiState.listBotColors, + ) + } + } + + Orbiter( + position = ContentEdge.Bottom, + alignment = Alignment.End, + offset = 16.dp, + ) { + TransformButton( + buttonText = stringResource(CreationR.string.start_transformation_button), + onClicked = onStartClicked, + ) + } + } + } + } + } +} diff --git a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt index 574fda1e..bc7450eb 100644 --- a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt +++ b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt @@ -17,9 +17,11 @@ package com.android.developers.androidify.creation import android.net.Uri import androidx.compose.material3.SnackbarHostState +import com.android.developers.androidify.data.ConfigProvider import com.android.developers.androidify.data.InsufficientInformationException import com.android.developers.testing.data.TestFileProvider import com.android.developers.testing.data.TestInternetConnectivityManager +import com.android.developers.testing.network.TestRemoteConfigDataSource import com.android.developers.testing.repository.FakeDropImageFactory import com.android.developers.testing.repository.FakeImageGenerationRepository import com.android.developers.testing.repository.TestTextGenerationRepository @@ -52,6 +54,7 @@ class CreationViewModelTest { private val imageGenerationRepository = FakeImageGenerationRepository() private val fakeUri = Uri.parse("test.jpeg") + @Before fun setup() { viewModel = CreationViewModel( @@ -61,8 +64,8 @@ class CreationViewModelTest { TestFileProvider(), FakeDropImageFactory(), context = RuntimeEnvironment.getApplication(), + configProvider = ConfigProvider(TestRemoteConfigDataSource(false)), ) - } @Test @@ -85,6 +88,7 @@ class CreationViewModelTest { TestFileProvider(), FakeDropImageFactory(), context = RuntimeEnvironment.getApplication(), + configProvider = ConfigProvider(TestRemoteConfigDataSource(false)), ) assertEquals( ScreenState.EDIT, @@ -153,7 +157,10 @@ class CreationViewModelTest { viewModel.onSelectedPromptOptionChanged(PromptType.PHOTO) viewModel.startClicked() assertEquals(ScreenState.EDIT, viewModel.uiState.value.screenState) - assertNotNull("Choose an image or use a prompt instead.", values.last().currentSnackbarData?.visuals?.message) + assertNotNull( + "Choose an image or use a prompt instead.", + values.last().currentSnackbarData?.visuals?.message, + ) } @Test From f5b1c6be3ea3024f4412e0912911cf82231868e4 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Wed, 10 Sep 2025 15:38:59 +0200 Subject: [PATCH 6/7] Fix dragAndDropTarget modifier being discarded --- .../androidify/creation/PhotoPrompt.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt index f12beb72..75487846 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/PhotoPrompt.kt @@ -136,17 +136,16 @@ fun PhotoPrompt( MaterialTheme.colorScheme.outline, cornerRadius = 28.dp, ) - .apply { - if (externalAppCallback != null) { - dragAndDropTarget( - shouldStartDragAndDrop = { event -> - dropBehaviourFactory.shouldStartDragAndDrop( - event, - ) - }, - target = externalAppCallback, - ) - } + .run { + if (externalAppCallback == null) this + else dragAndDropTarget( + shouldStartDragAndDrop = { event -> + dropBehaviourFactory.shouldStartDragAndDrop( + event, + ) + }, + target = externalAppCallback, + ) } .fillMaxSize() .padding(2.dp), @@ -354,7 +353,8 @@ fun ImagePreviewPreview() { onUndoPressed = {}, onChooseImagePressed = {}, ) { - val bitmap = ImageBitmap.imageResource(com.android.developers.androidify.results.R.drawable.placeholderbot) + val bitmap = + ImageBitmap.imageResource(com.android.developers.androidify.results.R.drawable.placeholderbot) Image(bitmap = bitmap, contentDescription = null) } } From f98fce3fe9cc64556a8e1bb20967ee836014a376 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 12 Sep 2025 14:13:33 +0200 Subject: [PATCH 7/7] Re-integrate TextPrompt workaround from #112. --- .../androidify/creation/PromptTypePager.kt | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt index 71e4f4d6..2f9920e2 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/PromptTypePager.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -46,6 +47,9 @@ import androidx.compose.material3.ToggleButton import androidx.compose.material3.ToggleButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager @@ -68,8 +72,8 @@ fun MainCreationPane( onSelectedPromptOptionChanged: (PromptType) -> Unit, onDropCallback: (Uri) -> Unit, ) { - PromptTypePager(modifier, uiState, onSelectedPromptOptionChanged) { - when (it) { + PromptTypePager(modifier, uiState, onSelectedPromptOptionChanged) { promptType, pagerState -> + when (promptType) { PromptType.PHOTO -> { PhotoPrompt( uiState = uiState, @@ -82,16 +86,25 @@ fun MainCreationPane( } PromptType.TEXT -> { - TextPrompt( - textFieldState = uiState.descriptionText, - promptGenerationInProgress = uiState.promptGenerationInProgress, - generatedPrompt = uiState.generatedPrompt, - onPromptGenerationPressed = onPromptGenerationPressed, - modifier = Modifier - .fillMaxSize() - .heightIn(min = 200.dp) - .padding(2.dp), - ) + // Workaround for https://issuetracker.google.com/432431393 + val showTextPrompt by remember { + derivedStateOf { + pagerState.currentPage == PromptType.TEXT.ordinal + && pagerState.targetPage == pagerState.currentPage + } + } + if (showTextPrompt) { + TextPrompt( + textFieldState = uiState.descriptionText, + promptGenerationInProgress = uiState.promptGenerationInProgress, + generatedPrompt = uiState.generatedPrompt, + onPromptGenerationPressed = onPromptGenerationPressed, + modifier = Modifier + .fillMaxSize() + .heightIn(min = 200.dp) + .padding(2.dp), + ) + } } } } @@ -102,7 +115,7 @@ private fun PromptTypePager( modifier: Modifier = Modifier, uiState: CreationState, onSelectedPromptOptionChanged: (PromptType) -> Unit, - content: @Composable PagerScope.(PromptType) -> Unit, + content: @Composable PagerScope.(PromptType, PagerState) -> Unit, ) { Box( modifier = modifier, @@ -141,7 +154,7 @@ private fun PromptTypePager( pageSpacing = 16.dp, contentPadding = PaddingValues(16.dp), pageContent = { - content(this, PromptType.entries[it]) + content(this, PromptType.entries[it], pagerState) }, ) }