diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportLayoutType.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportLayoutType.kt new file mode 100644 index 00000000..31ad2ed5 --- /dev/null +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportLayoutType.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.customize + +import androidx.compose.runtime.Composable +import androidx.xr.compose.platform.LocalSpatialCapabilities +import com.android.developers.androidify.util.isAtLeastMedium + +enum class CustomizeExportLayoutType { + Compact, + Medium, + Spatial, +} + +@Composable +fun calculateLayoutType(enableXr: Boolean = false): CustomizeExportLayoutType { + return when { + LocalSpatialCapabilities.current.isSpatialUiEnabled && enableXr -> CustomizeExportLayoutType.Spatial + isAtLeastMedium() -> CustomizeExportLayoutType.Medium + else -> CustomizeExportLayoutType.Compact + } +} diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt index 5983d985..51a58c2a 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -59,6 +60,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.LookaheadScope @@ -70,6 +72,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.android.developers.androidify.customize.watchface.WatchFaceModalSheet +import com.android.developers.androidify.customize.xr.CustomizeExportLayoutSpatial import com.android.developers.androidify.results.PermissionRationaleDialog import com.android.developers.androidify.results.R import com.android.developers.androidify.results.shareImage @@ -83,9 +86,12 @@ import com.android.developers.androidify.theme.transitions.loadingShimmerOverlay import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview import com.android.developers.androidify.util.allowsFullContent -import com.android.developers.androidify.util.isAtLeastMedium import com.android.developers.androidify.watchface.WatchFaceAsset import com.android.developers.androidify.wear.common.ConnectedWatch +import com.android.developers.androidify.xr.RequestFullSpaceIconButton +import com.android.developers.androidify.xr.RequestHomeSpaceIconButton +import com.android.developers.androidify.xr.couldRequestFullSpace +import com.android.developers.androidify.xr.couldRequestHomeSpace import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -97,7 +103,6 @@ import com.android.developers.androidify.theme.R as ThemeR fun CustomizeAndExportScreen( onBackPress: () -> Unit, onInfoPress: () -> Unit, - isMediumWindowSize: Boolean = isAtLeastMedium(), viewModel: CustomizeExportViewModel, ) { val state = viewModel.state.collectAsStateWithLifecycle() @@ -110,6 +115,8 @@ fun CustomizeAndExportScreen( viewModel.onSavedUriConsumed() } } + + val layoutType = calculateLayoutType(enableXr = state.value.xrEnabled) CustomizeExportContents( state.value, onBackPress, @@ -126,7 +133,7 @@ fun CustomizeAndExportScreen( onResetWatchFaceSend = { viewModel.resetWatchFaceSend() }, - isMediumWindowSize = isMediumWindowSize, + layoutType = layoutType, snackbarHostState = viewModel.snackbarHostState.collectAsStateWithLifecycle().value, loadWatchFaces = viewModel::loadWatchFaces, onWatchFaceSelect = viewModel::onWatchFaceSelected, @@ -145,198 +152,261 @@ private fun CustomizeExportContents( onSelectedToolStateChanged: (ToolState) -> Unit, onInstallWatchFaceClicked: () -> Unit, onResetWatchFaceSend: () -> Unit, - isMediumWindowSize: Boolean, + layoutType: CustomizeExportLayoutType, snackbarHostState: SnackbarHostState, loadWatchFaces: () -> Unit, onWatchFaceSelect: (WatchFaceAsset) -> Unit, ) { - Scaffold( - snackbarHost = { - SnackbarHost( - hostState = snackbarHostState, - snackbar = { snackbarData -> - Snackbar(snackbarData, shape = SnackbarDefaults.shape) - }, - ) - }, - topBar = { - AndroidifyTopAppBar( - backEnabled = true, - titleText = stringResource(R.string.customize_and_export), - isMediumWindowSize = isMediumWindowSize, - onBackPressed = onBackPress, - actions = { - AboutButton { onInfoPress() } - }, - ) - }, - containerColor = MaterialTheme.colorScheme.surface, - ) { paddingValues -> - var showWatchFaceBottomSheet by remember { mutableStateOf(false) } - val watchFaceSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ) - val imageResult = remember(state.showImageEditProgress) { - movableContentWithReceiverOf { - val chromeModifier = if (this.showSticker) { - Modifier - .clip(RoundedCornerShape(6)) - } else { - Modifier.dropShadow( + var showWatchFaceBottomSheet by remember { mutableStateOf(false) } + val watchFaceSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + val imageResult = remember(state.showImageEditProgress) { + movableContentWithReceiverOf { + val chromeModifier = if (this.showSticker) { + Modifier + .clip(RoundedCornerShape(6)) + } else { + Modifier + .dropShadow( RoundedCornerShape(6), shadow = Shadow( radius = 26.dp, spread = 10.dp, color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.2f), ), - ).clip(RoundedCornerShape(6)) - } - Box( - Modifier - .padding(16.dp), - ) { - ImageResult( - this@movableContentWithReceiverOf, - modifier = Modifier, - outerChromeModifier = Modifier - .then(chromeModifier) - .loadingShimmerOverlay( - visible = state.showImageEditProgress, - clipShape = RoundedCornerShape(percent = 6), - ), ) - } + .clip(RoundedCornerShape(6)) + } + Box( + Modifier + .padding(16.dp), + ) { + ImageResult( + this@movableContentWithReceiverOf, + modifier = Modifier, + outerChromeModifier = Modifier + .then(chromeModifier) + .loadingShimmerOverlay( + visible = state.showImageEditProgress, + clipShape = RoundedCornerShape(percent = 6), + ), + ) } } - val toolSelector = @Composable { modifier: Modifier, horizontal: Boolean -> - ToolSelector( - tools = state.tools, - selectedOption = state.selectedTool, - modifier = modifier, - horizontal = horizontal, - onToolSelected = { tool -> - onToolSelected(tool) + } + val topBar = @Composable { + AndroidifyTopAppBar( + backEnabled = true, + titleText = stringResource(R.string.customize_and_export), + isMediumWindowSize = layoutType != CustomizeExportLayoutType.Compact, + onBackPressed = onBackPress, + actions = { + AboutButton { onInfoPress() } + if (state.xrEnabled) { + if (couldRequestFullSpace()) { + RequestFullSpaceIconButton() + } + if (couldRequestHomeSpace()) { + RequestHomeSpaceIconButton() + } + } + }, + ) + } + val toolSelector: ToolSelectorComposable = @Composable { modifier, horizontal -> + ToolSelector( + tools = state.tools, + selectedOption = state.selectedTool, + modifier = modifier, + horizontal = horizontal, + onToolSelected = { tool -> + onToolSelected(tool) + }, + ) + } + val toolDetail: ToolDetailComposable = @Composable { modifier, singleLine -> + SelectedToolDetail( + state, + onSelectedToolStateChanged = { toolState -> + onSelectedToolStateChanged(toolState) + }, + singleLine = singleLine, + modifier = modifier, + ) + } + val actionButtons = @Composable { modifier: Modifier -> + BotActionsButtonRow( + onShareClicked = { + onShareClicked() + }, + onDownloadClicked = { + onDownloadClicked() + }, + onWearDeviceClick = { + showWatchFaceBottomSheet = true + }, + hasWearDevice = state.connectedWatch != null, + modifier = modifier, + ) + } + state.connectedWatch?.let { device -> + if (showWatchFaceBottomSheet) { + WatchFaceModalSheet( + sheetState = watchFaceSheetState, + onDismiss = { + onResetWatchFaceSend() + showWatchFaceBottomSheet = false + }, + connectedWatch = device, + installationStatus = state.watchFaceInstallationStatus, + onWatchFaceInstallClick = { + onInstallWatchFaceClicked() }, + onLoad = loadWatchFaces, + watchFaceSelectionState = state.watchFaceSelectionState, + onWatchFaceSelect = onWatchFaceSelect, ) } - val toolDetail = @Composable { modifier: Modifier, singleLine: Boolean -> - SelectedToolDetail( - state, - onSelectedToolStateChanged = { toolState -> - onSelectedToolStateChanged(toolState) - }, - singleLine = singleLine, - modifier = modifier, + } + + when (layoutType) { + CustomizeExportLayoutType.Medium -> CustomizeExportScreenScaffold( + snackbarHostState, + topBar = topBar, + containerColor = MaterialTheme.colorScheme.surface, + ) { paddingValues -> + CustomizeExportLayoutMedium( + paddingValues = paddingValues, + imageResult = imageResult, + state = state, + toolDetail = toolDetail, + toolSelector = toolSelector, + actionButtons = actionButtons, ) } - val actionButtons = @Composable { modifier: Modifier -> - BotActionsButtonRow( - onShareClicked = { - onShareClicked() - }, - onDownloadClicked = { - onDownloadClicked() - }, - onWearDeviceClick = { - showWatchFaceBottomSheet = true - }, - hasWearDevice = state.connectedWatch != null, - modifier = modifier, + + CustomizeExportLayoutType.Compact -> CustomizeExportScreenScaffold( + snackbarHostState, + topBar = topBar, + containerColor = MaterialTheme.colorScheme.surface, + ) { paddingValues -> + CustomizeExportLayoutCompact( + paddingValues = paddingValues, + imageResult = imageResult, + state = state, + toolSelector = toolSelector, + toolDetail = toolDetail, + actionButtons = actionButtons, ) } - state.connectedWatch?.let { device -> - if (showWatchFaceBottomSheet) { - WatchFaceModalSheet( - sheetState = watchFaceSheetState, - onDismiss = { - onResetWatchFaceSend() - showWatchFaceBottomSheet = false - }, - connectedWatch = device, - installationStatus = state.watchFaceInstallationStatus, - onWatchFaceInstallClick = { - onInstallWatchFaceClicked() - }, - onLoad = loadWatchFaces, - watchFaceSelectionState = state.watchFaceSelectionState, - onWatchFaceSelect = onWatchFaceSelect, - ) + + CustomizeExportLayoutType.Spatial -> CustomizeExportLayoutSpatial( + imageResult = imageResult, + state = state, + toolSelector = toolSelector, + toolDetail = toolDetail, + actionButtons = actionButtons, + snackbarHostState = snackbarHostState, + topBar = topBar, + ) + } +} + +@Composable +private fun CustomizeExportLayoutCompact( + paddingValues: PaddingValues, + imageResult: @Composable (ExportImageCanvas.() -> Unit), + state: CustomizeExportState, + toolDetail: ToolDetailComposable, + toolSelector: ToolSelectorComposable, + actionButtons: @Composable (Modifier) -> Unit, +) { + LookaheadScope { + CompositionLocalProvider(LocalAnimateBoundsScope provides this) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box( + modifier = Modifier + .weight(1f, fill = true), + contentAlignment = Alignment.Center, + ) { + imageResult( + state.exportImageCanvas, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + toolSelector(Modifier, true) + Spacer(modifier = Modifier.height(16.dp)) + toolDetail(Modifier, true) + Spacer(modifier = Modifier.height(16.dp)) + actionButtons(Modifier) + Spacer(modifier = Modifier.height(24.dp)) } } - LookaheadScope { - CompositionLocalProvider(LocalAnimateBoundsScope provides this) { - if (isMediumWindowSize) { + } +} + +@Composable +private fun CustomizeExportLayoutMedium( + paddingValues: PaddingValues, + imageResult: @Composable (ExportImageCanvas.() -> Unit), + state: CustomizeExportState, + toolDetail: ToolDetailComposable, + toolSelector: ToolSelectorComposable, + actionButtons: @Composable (Modifier) -> Unit, +) { + LookaheadScope { + CompositionLocalProvider(LocalAnimateBoundsScope provides this) { + Row( + Modifier + .fillMaxSize() + .padding(paddingValues), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Box( + modifier = Modifier.weight(0.6f), + contentAlignment = Alignment.Center, + ) { + imageResult( + state.exportImageCanvas, + ) + } + Column( + Modifier + .weight(0.4f) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { Row( Modifier - .fillMaxSize() - .padding(paddingValues), + .weight(1f) + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Box( - modifier = Modifier.weight(0.6f), - contentAlignment = Alignment.Center, - ) { - imageResult( - state.exportImageCanvas, - ) - } - Column( - Modifier - .weight(0.4f) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Row( - Modifier - .weight(1f) - .fillMaxSize(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - ) { - Box(modifier = Modifier.weight(1f)) { - toolDetail(Modifier.align(Alignment.CenterEnd), false) - } - Spacer(modifier = Modifier.size(16.dp)) - toolSelector(Modifier.requiredSizeIn(minWidth = 56.dp), false) - Spacer(modifier = Modifier.size(16.dp)) - } - Spacer(modifier = Modifier.size(16.dp)) - actionButtons( - Modifier - .align(Alignment.End) - .padding(end = 16.dp), - ) - Spacer(modifier = Modifier.size(24.dp)) - } - } - } else { - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, ) { - Box( - modifier = Modifier - .weight(1f, fill = true), - contentAlignment = Alignment.Center, - ) { - imageResult( - state.exportImageCanvas, - ) + Box(modifier = Modifier.weight(1f)) { + toolDetail(Modifier.align(Alignment.CenterEnd), false) } - Spacer(modifier = Modifier.height(16.dp)) - toolSelector(Modifier, true) - Spacer(modifier = Modifier.height(16.dp)) - toolDetail(Modifier, true) - Spacer(modifier = Modifier.height(16.dp)) - actionButtons(Modifier) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.size(16.dp)) + toolSelector(Modifier.requiredSizeIn(minWidth = 56.dp), false) + Spacer(modifier = Modifier.size(16.dp)) } + Spacer(modifier = Modifier.size(16.dp)) + actionButtons( + Modifier + .align(Alignment.End) + .padding(end = 16.dp), + ) + Spacer(modifier = Modifier.size(24.dp)) } } } @@ -473,6 +543,33 @@ private fun BotActionsButtonRow( } } +typealias ToolSelectorComposable = @Composable (modifier: Modifier, horizontal: Boolean) -> Unit +typealias ToolDetailComposable = @Composable (modifier: Modifier, singleLine: Boolean) -> Unit + +@Composable +fun CustomizeExportScreenScaffold( + snackbarHostState: SnackbarHostState, + topBar: @Composable () -> Unit, + modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.background, + content: @Composable (PaddingValues) -> Unit, +) { + Scaffold( + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { snackbarData -> + Snackbar(snackbarData, shape = SnackbarDefaults.shape) + }, + ) + }, + topBar = topBar, + containerColor = containerColor, + modifier = modifier, + content = content, + ) +} + @Preview(showBackground = true) @PhonePreview @Composable @@ -499,7 +596,7 @@ fun CustomizeExportPreview() { onInfoPress = {}, onToolSelected = {}, snackbarHostState = SnackbarHostState(), - isMediumWindowSize = false, + layoutType = CustomizeExportLayoutType.Compact, onSelectedToolStateChanged = {}, onInstallWatchFaceClicked = {}, onResetWatchFaceSend = {}, @@ -540,7 +637,7 @@ fun CustomizeExportPreviewLarge() { onInfoPress = {}, onToolSelected = {}, snackbarHostState = SnackbarHostState(), - isMediumWindowSize = true, + layoutType = CustomizeExportLayoutType.Medium, onSelectedToolStateChanged = {}, onInstallWatchFaceClicked = {}, onResetWatchFaceSend = {}, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt index cddf7056..3112cca8 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt @@ -67,7 +67,7 @@ class CustomizeExportViewModel @AssistedInject constructor( ): CustomizeExportViewModel } - private val _state = MutableStateFlow(CustomizeExportState()) + private val _state = MutableStateFlow(CustomizeExportState(xrEnabled = remoteConfigDataSource.isXrEnabled())) val state: StateFlow = combine( _state, watchfaceInstallationRepository.connectedWatch, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt index 780388de..c3e1e70b 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeState.kt @@ -41,6 +41,7 @@ data class CustomizeExportState( val connectedWatch: ConnectedWatch? = null, val watchFaceInstallationStatus: WatchFaceInstallationStatus = WatchFaceInstallationStatus.NotStarted, val watchFaceSelectionState: WatchFaceSelectionState = WatchFaceSelectionState(), + val xrEnabled: Boolean = false, ) interface ToolState { diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/ToolSelector.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/ToolSelector.kt index a29aaf70..0df7b612 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/ToolSelector.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/ToolSelector.kt @@ -13,12 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.developers.androidify.customize +import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarColors import androidx.compose.material3.HorizontalFloatingToolbar @@ -33,8 +36,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.Primary -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ToolSelector( tools: List, @@ -43,91 +46,92 @@ fun ToolSelector( horizontal: Boolean, modifier: Modifier = Modifier, ) { + val buttons = @Composable { + tools.forEachIndexed { index, tool -> + ToolSelectorToggleButton( + modifier = Modifier, + tool = tool, + checked = selectedOption == tool, + onCheckedChange = { onToolSelected(tool) }, + ) + if (index != tools.size - 1) { + Spacer(Modifier.size(8.dp)) + } + } + } + val toolbarColors = FloatingToolbarColors( + toolbarContainerColor = MaterialTheme.colorScheme.surface, + toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + fabContainerColor = MaterialTheme.colorScheme.tertiary, + fabContentColor = MaterialTheme.colorScheme.onTertiary, + ) + if (horizontal) { HorizontalFloatingToolbar( - modifier = modifier.border( - 2.dp, - color = MaterialTheme.colorScheme.outline, - shape = MaterialTheme.shapes.large, - ).padding(2.dp), - colors = FloatingToolbarColors( - toolbarContainerColor = MaterialTheme.colorScheme.surface, - toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - fabContainerColor = MaterialTheme.colorScheme.tertiary, - fabContentColor = MaterialTheme.colorScheme.onTertiary, - ), + modifier = modifier.toolbarBorder(), + shape = MaterialTheme.shapes.large, + colors = toolbarColors, expanded = true, ) { - tools.forEachIndexed { index, tool -> - ToggleButton( - modifier = Modifier, - checked = selectedOption == tool, - onCheckedChange = { onToolSelected(tool) }, - shapes = ToggleButtonDefaults.shapes(checkedShape = MaterialTheme.shapes.large), - colors = ToggleButtonDefaults.toggleButtonColors( - checkedContainerColor = MaterialTheme.colorScheme.onSurface, - containerColor = MaterialTheme.colorScheme.surface, - ), - ) { - Icon( - painterResource(tool.icon), - contentDescription = tool.displayName, - ) - } - if (index != tools.size - 1) { - Spacer(Modifier.width(8.dp)) - } - } + buttons() } } else { VerticalFloatingToolbar( - modifier = modifier.border( - 2.dp, - color = MaterialTheme.colorScheme.outline, - shape = MaterialTheme.shapes.large, - ).padding(2.dp), - colors = FloatingToolbarColors( - toolbarContainerColor = MaterialTheme.colorScheme.surface, - toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - fabContainerColor = MaterialTheme.colorScheme.tertiary, - fabContentColor = MaterialTheme.colorScheme.onTertiary, - ), + modifier = modifier.toolbarBorder(), + shape = MaterialTheme.shapes.large, + colors = toolbarColors, expanded = true, ) { - tools.forEachIndexed { index, tool -> - ToggleButton( - modifier = Modifier, - checked = selectedOption == tool, - onCheckedChange = { onToolSelected(tool) }, - shapes = ToggleButtonDefaults.shapes(checkedShape = MaterialTheme.shapes.large), - colors = ToggleButtonDefaults.toggleButtonColors( - checkedContainerColor = MaterialTheme.colorScheme.onSurface, - containerColor = MaterialTheme.colorScheme.surface, - ), - ) { - Icon( - painterResource(tool.icon), - contentDescription = tool.displayName, - ) - } - if (index != tools.size - 1) { - Spacer(Modifier.width(8.dp)) - } - } + buttons() } } } +@Composable +private fun ToolSelectorToggleButton( + tool: CustomizeTool, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + ToggleButton( + modifier = modifier, + checked = checked, + onCheckedChange = onCheckedChange, + shapes = ToggleButtonDefaults.shapes( + checkedShape = MaterialTheme.shapes.large, + ), + colors = ToggleButtonDefaults.toggleButtonColors( + checkedContainerColor = MaterialTheme.colorScheme.onSurface, + containerColor = MaterialTheme.colorScheme.surface, + ), + ) { + Icon( + painterResource(tool.icon), + contentDescription = tool.displayName, + ) + } +} + +@Composable +private fun Modifier.toolbarBorder() = this.border( + 2.dp, + color = MaterialTheme.colorScheme.outline, + shape = MaterialTheme.shapes.large, +) + @Preview @Composable private fun ToolsPreviewHorizontal() { AndroidifyTheme { - ToolSelector( - tools = listOf(CustomizeTool.Size, CustomizeTool.Background), - selectedOption = CustomizeTool.Size, - horizontal = true, - onToolSelected = {}, - ) + Box(Modifier.background(Primary)) { + ToolSelector( + tools = listOf(CustomizeTool.Size, CustomizeTool.Background), + selectedOption = CustomizeTool.Size, + horizontal = true, + onToolSelected = {}, + ) + } } } @@ -135,11 +139,13 @@ private fun ToolsPreviewHorizontal() { @Composable private fun ToolsPreviewVertical() { AndroidifyTheme { - ToolSelector( - tools = listOf(CustomizeTool.Size, CustomizeTool.Background), - selectedOption = CustomizeTool.Size, - horizontal = false, - onToolSelected = {}, - ) + Box(Modifier.background(Primary)) { + ToolSelector( + tools = listOf(CustomizeTool.Size, CustomizeTool.Background), + selectedOption = CustomizeTool.Size, + horizontal = false, + onToolSelected = {}, + ) + } } } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/xr/CustomizeExportScreenSpatial.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/xr/CustomizeExportScreenSpatial.kt new file mode 100644 index 00000000..fffaee62 --- /dev/null +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/xr/CustomizeExportScreenSpatial.kt @@ -0,0 +1,130 @@ +/* + * 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.customize.xr + +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.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LookaheadScope +import androidx.compose.ui.unit.dp +import androidx.xr.compose.spatial.ContentEdge +import androidx.xr.compose.spatial.Orbiter +import androidx.xr.compose.spatial.OrbiterOffsetType +import androidx.xr.compose.subspace.SpatialBox +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.customize.CustomizeExportScreenScaffold +import com.android.developers.androidify.customize.CustomizeExportState +import com.android.developers.androidify.customize.ExportImageCanvas +import com.android.developers.androidify.customize.ToolDetailComposable +import com.android.developers.androidify.customize.ToolSelectorComposable +import com.android.developers.androidify.theme.LocalAnimateBoundsScope +import com.android.developers.androidify.xr.DisableSharedTransition +import com.android.developers.androidify.xr.MainPanelWorkaround +import com.android.developers.androidify.xr.SquiggleBackgroundSubspace + +@Composable +fun CustomizeExportLayoutSpatial( + state: CustomizeExportState, + snackbarHostState: SnackbarHostState, + imageResult: @Composable (ExportImageCanvas.() -> Unit), + toolDetail: ToolDetailComposable, + toolSelector: ToolSelectorComposable, + actionButtons: @Composable (Modifier) -> Unit, + topBar: @Composable () -> Unit, +) { + DisableSharedTransition { + SquiggleBackgroundSubspace(minimumHeight = 600.dp) { + MainPanelWorkaround() + SpatialColumn(SubspaceModifier.fillMaxWidth()) { + Orbiter(position = ContentEdge.Bottom, alignment = Alignment.End) { + actionButtons(Modifier) + } + SpatialPanel( + SubspaceModifier.offset(z = 10.dp) + .fillMaxWidth(0.5f), + ) { + Column( + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = MaterialTheme.shapes.large, + ), + ) { + topBar() + Spacer(Modifier.size(16.dp)) + } + } + + SpatialRow(SubspaceModifier.fillMaxWidth(0.7f)) { + SpatialPanel( + modifier = SubspaceModifier + .offset(z = 10.dp) + .weight(1f, fill = true) + .fillMaxHeight(0.8f), + ) { + CustomizeExportScreenScaffold( + snackbarHostState = snackbarHostState, + topBar = {}, + containerColor = Color.Transparent, + ) { + LookaheadScope { + CompositionLocalProvider(LocalAnimateBoundsScope provides this) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + imageResult(state.exportImageCanvas) + } + } + } + } + } + SpatialLayoutSpacer(SubspaceModifier.width(48.dp)) + SpatialBox(SubspaceModifier.fillMaxHeight(0.7f)) { + SpatialPanel( + modifier = SubspaceModifier.offset(z = 10.dp).fillMaxWidth(0.3f), + ) { + toolDetail(Modifier, false) + Orbiter( + position = ContentEdge.End, + offsetType = OrbiterOffsetType.InnerEdge, + ) { + toolSelector(Modifier, false) + } + } + } + } + } + } + } +}