From 11a08e923ad44da93296db18fe0c5669f7cccd77 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 28 Aug 2025 14:36:12 +0200 Subject: [PATCH 01/22] Extract compact layout, medium layout, and shared components into their own files. --- .../developers/androidify/home/HomeScreen.kt | 489 +----------------- .../androidify/home/HomeScreenCompact.kt | 157 ++++++ .../androidify/home/HomeScreenComponents.kt | 228 ++++++++ .../androidify/home/HomeScreenMedium.kt | 101 ++++ .../androidify/home/HomeScreenVideoPlayer.kt | 166 ++++++ 5 files changed, 655 insertions(+), 486 deletions(-) create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index b42e157e..fddc0d0c 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -16,104 +16,22 @@ package com.android.developers.androidify.home import androidx.annotation.OptIn -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.focusable -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.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio 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.safeDrawingPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedIconButton -import androidx.compose.material3.Text 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.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onLayoutRectChanged -import androidx.compose.ui.layout.onVisibilityChanged -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.ui.compose.PlayerSurface -import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW -import androidx.media3.ui.compose.state.rememberPlayPauseButtonState -import coil3.compose.AsyncImage -import com.android.developers.androidify.theme.Blue import com.android.developers.androidify.theme.SharedElementContextPreview -import com.android.developers.androidify.theme.components.AndroidifyTopAppBar -import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.util.LargeScreensPreview -import com.android.developers.androidify.util.LocalOcclusion import com.android.developers.androidify.util.PhonePreview import com.android.developers.androidify.util.isAtLeastMedium -import com.android.developers.androidify.theme.R as ThemeR @ExperimentalMaterial3ExpressiveApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -149,56 +67,16 @@ fun HomeScreenContents( ) { Box { SquiggleBackground() - var positionButtonClick by remember { - mutableStateOf(IntOffset.Zero) - } + Column( modifier = Modifier .fillMaxSize() .safeDrawingPadding(), ) { if (isMediumWindowSize) { - AndroidifyTranslucentTopAppBar(isMediumSizeLayout = true) - - Row( - modifier = Modifier - .weight(1f) - .padding(horizontal = 32.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier.weight(0.8f), - ) { - VideoPlayerRotatedCard( - videoLink, - modifier = Modifier - .padding(32.dp) - .align(Alignment.Center), - ) - } - Box( - modifier = Modifier - .weight(1.2f) - .align(Alignment.CenterVertically), - ) { - MainHomeContent(dancingBotLink) - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - positionButtonClick = it.boundsInWindow.center - } - .align(Alignment.BottomCenter) - .padding(bottom = 16.dp) - .height(64.dp) - .width(220.dp), - onClick = { - onClickLetsGo(positionButtonClick) - }, - ) - } - } + HomeScreenMediumContents(Modifier.weight(1f), videoLink, dancingBotLink, onClickLetsGo) } else { - CompactPager( + HomeScreenCompactPager( videoLink, dancingBotLink, onClickLetsGo, @@ -209,125 +87,6 @@ fun HomeScreenContents( } } -@Composable -private fun CompactPager( - videoLink: String?, - dancingBotLink: String?, - onClick: (IntOffset) -> Unit, - onAboutClicked: () -> Unit, -) { - val pagerState = rememberPagerState(pageCount = { 2 }) - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AndroidifyTopAppBar( - aboutEnabled = true, - onAboutClicked = onAboutClicked, - ) - HorizontalPager( - state = pagerState, - modifier = Modifier.weight(.8f), - beyondViewportPageCount = 1, - ) { page -> - if (page == 0) { - MainHomeContent( - dancingBotLink = dancingBotLink, - modifier = Modifier - .padding(horizontal = 16.dp) - .align(Alignment.CenterHorizontally), - ) - } else { - Box(modifier = Modifier.fillMaxSize()) { - VideoPlayerRotatedCard( - videoLink = videoLink, - modifier = Modifier - .padding(horizontal = 32.dp) - .align(Alignment.Center), - ) - } - } - } - Row( - modifier = Modifier - .weight(.1f) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - repeat(pagerState.pageCount) { iteration -> - val isCurrent by remember { derivedStateOf { pagerState.currentPage == iteration } } - val animatedColor by animateColorAsState( - if (isCurrent) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onTertiary, - label = "animatedFirstColor", - ) - - Box( - modifier = Modifier - .padding(2.dp) - .clip(RoundedCornerShape(size = 16.dp)) - .animateContentSize() - .background( - color = animatedColor, - shape = RoundedCornerShape(size = 16.dp), - ) - .height(16.dp) - .width(if (isCurrent) 40.dp else 16.dp), - ) - } - } - Spacer(modifier = Modifier.size(12.dp)) - var buttonPosition by remember { - mutableStateOf(IntOffset.Zero) - } - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - buttonPosition = it.boundsInWindow.center - } - .padding(bottom = 16.dp) - .height(64.dp) - .width(220.dp), - colors = ButtonDefaults.buttonColors() - .copy(containerColor = Blue), - onClick = { - onClick(buttonPosition) - }, - ) - } -} - -@Composable -private fun VideoPlayerRotatedCard( - videoLink: String?, - modifier: Modifier = Modifier, -) { - val aspectRatio = 280f / 380f - val videoInstructionText = stringResource(R.string.instruction_video_transcript) - Box( - modifier = modifier - .focusable() - .semantics { contentDescription = videoInstructionText } - .aspectRatio(aspectRatio) - .rotate(-3f) - .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large) - .background( - color = Color.White, - shape = MaterialTheme.shapes.large, - ), - ) { - VideoPlayer( - videoLink, - modifier = Modifier - .aspectRatio(aspectRatio) - .align(Alignment.Center) - .clip(MaterialTheme.shapes.large) - .clipToBounds(), - ) - } -} - @ExperimentalMaterial3ExpressiveApi @PhonePreview @Composable @@ -357,245 +116,3 @@ private fun HomeScreenLargeScreensPreview() { ) } } - -@Composable -private fun MainHomeContent( - dancingBotLink: String?, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - ) { - DecorativeSquiggleLimeGreen() - DancingBotHeadlineText( - dancingBotLink, - modifier = Modifier.weight(1f), - ) - DecorativeSquiggleLightGreen() - } -} - -@Composable -private fun ColumnScope.DecorativeSquiggleLightGreen() { - val infiniteAnimation = rememberInfiniteTransition() - val rotationAnimation = infiniteAnimation.animateFloat( - 0f, - 720f, - animationSpec = infiniteRepeatable( - tween(12000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, - ), - ) - Image( - painter = rememberVectorPainter( - ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle_2), - ), - contentDescription = null, // decorative element - modifier = Modifier - .padding(start = 60.dp) - .size(60.dp) - .align(Alignment.Start) - .graphicsLayer { - rotationZ = rotationAnimation.value - }, - ) -} - -@Composable -private fun ColumnScope.DecorativeSquiggleLimeGreen() { - val infiniteAnimation = rememberInfiniteTransition() - val rotationAnimation = infiniteAnimation.animateFloat( - 0f, - -720f, - animationSpec = infiniteRepeatable( - tween(24000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, - ), - ) - Image( - painter = rememberVectorPainter( - ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle), - ), - contentDescription = null, // decorative element - modifier = Modifier - .padding(end = 80.dp) - .size(60.dp) - .align(Alignment.End) - .graphicsLayer { - rotationZ = rotationAnimation.value - }, - - ) -} - -@Preview -@Composable -private fun HomePageButton( - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), - onClick: () -> Unit = {}, -) { - val style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight(700), - letterSpacing = .15f.sp, - ) - - Button( - onClick = onClick, - modifier = modifier, - colors = colors, - ) { - Text( - stringResource(R.string.home_button_label), - style = style, - ) - } -} - -@Composable -private fun DancingBot( - dancingBotLink: String?, - modifier: Modifier, -) { - if (LocalInspectionMode.current) { - Image( - painter = painterResource(id = R.drawable.dancing_droid_gif_placeholder), - contentDescription = null, - modifier = modifier, - ) - } else { - AsyncImage( - model = dancingBotLink, - modifier = modifier, - contentDescription = null, - ) - } -} - -@Composable -private fun DancingBotHeadlineText( - dancingBotLink: String?, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier) { - val animatedBot = "animatedBot" - val text = buildAnnotatedString { - append(stringResource(R.string.customize_your_own)) - // Attach "animatedBot" annotation on the placeholder - appendInlineContent(animatedBot) - append(stringResource(R.string.into_an_android_bot)) - } - var placeHolderSize by remember { - mutableStateOf(220.sp) - } - val inlineContent = mapOf( - Pair( - animatedBot, - InlineTextContent( - Placeholder( - width = placeHolderSize, - height = placeHolderSize, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, - ), - ) { - DancingBot( - dancingBotLink, - modifier = Modifier - .padding(top = 32.dp) - .fillMaxSize(), - ) - }, - ), - ) - BasicText( - text, - modifier = Modifier - .align(Alignment.Center) - .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), - style = MaterialTheme.typography.titleLarge, - autoSize = TextAutoSize.StepBased(maxFontSize = 220.sp), - maxLines = 5, - onTextLayout = { result -> - placeHolderSize = result.layoutInput.style.fontSize * 3.5f - }, - inlineContent = inlineContent, - ) - } -} - -@OptIn(UnstableApi::class) // New Media3 Compose artifact is currently experimental -@Composable -private fun VideoPlayer( - videoLink: String?, - modifier: Modifier = Modifier, -) { - if (LocalInspectionMode.current) { - Image( - painter = painterResource(id = R.drawable.promo_video_placeholder), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = modifier, - ) - return - } else { - val context = LocalContext.current - var player by remember { mutableStateOf(null) } - LifecycleStartEffect(videoLink) { - if (videoLink != null) { - player = ExoPlayer.Builder(context).build().apply { - setMediaItem(MediaItem.fromUri(videoLink)) - repeatMode = Player.REPEAT_MODE_ONE - prepare() - } - } - onStopOrDispose { - player?.release() - player = null - } - } - - var videoFullyOnScreen by remember { mutableStateOf(false) } - val isWindowOccluded = LocalOcclusion.current - Box( - Modifier - .background(MaterialTheme.colorScheme.surfaceContainerLowest) - .onVisibilityChanged( - minDurationMs = 100, - minFractionVisible = 1f, - ) { fullyVisible -> videoFullyOnScreen = fullyVisible } - .then(modifier), - ) { - player?.let { currentPlayer -> - LaunchedEffect(videoFullyOnScreen, LocalOcclusion.current.value) { - if (videoFullyOnScreen && !isWindowOccluded.value) currentPlayer.play() else currentPlayer.pause() - } - - // Render the video - PlayerSurface(currentPlayer, surfaceType = SURFACE_TYPE_TEXTURE_VIEW) - - // Show a play / pause button - val playPauseButtonState = rememberPlayPauseButtonState(currentPlayer) - OutlinedIconButton( - onClick = playPauseButtonState::onClick, - enabled = playPauseButtonState.isEnabled, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - colors = IconButtonDefaults.outlinedIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - ) { - val icon = - if (playPauseButtonState.showPlay) R.drawable.rounded_play_arrow_24 else R.drawable.rounded_pause_24 - val contentDescription = - if (playPauseButtonState.showPlay) R.string.play else R.string.pause - Icon( - painterResource(icon), - stringResource(contentDescription), - ) - } - } - } - } -} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt new file mode 100644 index 00000000..02fcb1f9 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -0,0 +1,157 @@ +/* + * 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.home + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +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.layout.onLayoutRectChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.theme.Blue +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.components.AndroidifyTopAppBar +import com.android.developers.androidify.util.PhonePreview + +@Composable +fun HomeScreenCompactPager( + videoLink: String?, + dancingBotLink: String?, + onClick: (IntOffset) -> Unit, + onAboutClicked: () -> Unit, +) { + val pagerState = rememberPagerState(pageCount = { 2 }) + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AndroidifyTopAppBar( + aboutEnabled = true, + onAboutClicked = onAboutClicked, + ) + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(.8f), + beyondViewportPageCount = 1, + ) { page -> + if (page == 0) { + MainHomeContent( + dancingBotLink = dancingBotLink, + modifier = Modifier + .padding(horizontal = 16.dp) + .align(Alignment.CenterHorizontally), + ) + } else { + Box(modifier = Modifier.fillMaxSize()) { + VideoPlayerRotatedCard( + videoLink = videoLink, + modifier = Modifier + .padding(horizontal = 32.dp) + .align(Alignment.Center), + ) + } + } + } + Row( + modifier = Modifier + .weight(.1f) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(pagerState.pageCount) { iteration -> + val isCurrent by remember { derivedStateOf { pagerState.currentPage == iteration } } + val animatedColor by animateColorAsState( + if (isCurrent) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onTertiary, + label = "animatedFirstColor", + ) + + Box( + modifier = Modifier + .padding(2.dp) + .clip(RoundedCornerShape(size = 16.dp)) + .animateContentSize() + .background( + color = animatedColor, + shape = RoundedCornerShape(size = 16.dp), + ) + .height(16.dp) + .width(if (isCurrent) 40.dp else 16.dp), + ) + } + } + Spacer(modifier = Modifier.size(12.dp)) + var buttonPosition by remember { + mutableStateOf(IntOffset.Zero) + } + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + buttonPosition = it.boundsInWindow.center + } + .padding(bottom = 16.dp) + .height(64.dp) + .width(220.dp), + colors = ButtonDefaults.buttonColors() + .copy(containerColor = Blue), + onClick = { + onClick(buttonPosition) + }, + ) + } +} + +@ExperimentalMaterial3ExpressiveApi +@PhonePreview +@Composable +private fun HomeScreenPhonePreview() { + SharedElementContextPreview { + HomeScreenContents( + isMediumWindowSize = false, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt new file mode 100644 index 00000000..678600e5 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt @@ -0,0 +1,228 @@ +/* + * 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.home + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.android.developers.androidify.theme.Blue +import com.android.developers.androidify.theme.R as ThemeR + +@Composable +fun MainHomeContent( + dancingBotLink: String?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + DecorativeSquiggleLimeGreen() + DancingBotHeadlineText( + dancingBotLink, + modifier = Modifier.weight(1f), + ) + DecorativeSquiggleLightGreen() + } +} + +@Composable +fun ColumnScope.DecorativeSquiggleLightGreen() { + val infiniteAnimation = rememberInfiniteTransition() + val rotationAnimation = infiniteAnimation.animateFloat( + 0f, + 720f, + animationSpec = infiniteRepeatable( + tween(12000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + ) + Image( + painter = rememberVectorPainter( + ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle_2), + ), + contentDescription = null, // decorative element + modifier = Modifier + .padding(start = 60.dp) + .size(60.dp) + .align(Alignment.Start) + .graphicsLayer { + rotationZ = rotationAnimation.value + }, + ) +} + +@Composable +fun ColumnScope.DecorativeSquiggleLimeGreen() { + val infiniteAnimation = rememberInfiniteTransition() + val rotationAnimation = infiniteAnimation.animateFloat( + 0f, + -720f, + animationSpec = infiniteRepeatable( + tween(24000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + ) + Image( + painter = rememberVectorPainter( + ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle), + ), + contentDescription = null, // decorative element + modifier = Modifier + .padding(end = 80.dp) + .size(60.dp) + .align(Alignment.End) + .graphicsLayer { + rotationZ = rotationAnimation.value + }, + + ) +} + +@Preview +@Composable +fun HomePageButton( + modifier: Modifier = Modifier, + colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), + onClick: () -> Unit = {}, +) { + val style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight(700), + letterSpacing = .15f.sp, + ) + + Button( + onClick = onClick, + modifier = modifier, + colors = colors, + ) { + Text( + stringResource(R.string.home_button_label), + style = style, + ) + } +} + +@Composable +private fun DancingBot( + dancingBotLink: String?, + modifier: Modifier, +) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(id = R.drawable.dancing_droid_gif_placeholder), + contentDescription = null, + modifier = modifier, + ) + } else { + AsyncImage( + model = dancingBotLink, + modifier = modifier, + contentDescription = null, + ) + } +} + +@Composable +fun DancingBotHeadlineText( + dancingBotLink: String?, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val animatedBot = "animatedBot" + val text = buildAnnotatedString { + append(stringResource(R.string.customize_your_own)) + // Attach "animatedBot" annotation on the placeholder + appendInlineContent(animatedBot) + append(stringResource(R.string.into_an_android_bot)) + } + var placeHolderSize by remember { + mutableStateOf(220.sp) + } + val inlineContent = mapOf( + Pair( + animatedBot, + InlineTextContent( + Placeholder( + width = placeHolderSize, + height = placeHolderSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + ) { + DancingBot( + dancingBotLink, + modifier = Modifier + .padding(top = 32.dp) + .fillMaxSize(), + ) + }, + ), + ) + BasicText( + text, + modifier = Modifier + .align(Alignment.Center) + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + style = MaterialTheme.typography.titleLarge, + autoSize = TextAutoSize.StepBased(maxFontSize = 220.sp), + maxLines = 5, + onTextLayout = { result -> + placeHolderSize = result.layoutInput.style.fontSize * 3.5f + }, + inlineContent = inlineContent, + ) + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt new file mode 100644 index 00000000..03dc4d1c --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -0,0 +1,101 @@ +/* + * 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.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +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.layout.onLayoutRectChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar +import com.android.developers.androidify.util.LargeScreensPreview + +@Composable +fun HomeScreenMediumContents( + modifier: Modifier, + videoLink: String?, + dancingBotLink: String?, + onClickLetsGo: (IntOffset) -> Unit, +) { + var positionButtonClick by remember { + mutableStateOf(IntOffset.Zero) + } + AndroidifyTranslucentTopAppBar(isMediumSizeLayout = true) + + Row( + modifier = modifier + .padding(horizontal = 32.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.weight(0.8f), + ) { + VideoPlayerRotatedCard( + videoLink, + modifier = Modifier + .padding(32.dp) + .align(Alignment.Center), + ) + } + Box( + modifier = Modifier + .weight(1.2f) + .align(Alignment.CenterVertically), + ) { + MainHomeContent(dancingBotLink) + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + positionButtonClick = it.boundsInWindow.center + } + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + .height(64.dp) + .width(220.dp), + onClick = { + onClickLetsGo(positionButtonClick) + }, + ) + } + } +} + +@ExperimentalMaterial3ExpressiveApi +@LargeScreensPreview +@Composable +private fun HomeScreenLargeScreensPreview() { + SharedElementContextPreview { + HomeScreenContents( + isMediumWindowSize = true, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt new file mode 100644 index 00000000..eb7c17ac --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt @@ -0,0 +1,166 @@ +/* + * 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.home + +import androidx.annotation.OptIn +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.draw.clipToBounds +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onVisibilityChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.compose.PlayerSurface +import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW +import androidx.media3.ui.compose.state.rememberPlayPauseButtonState +import com.android.developers.androidify.util.LocalOcclusion + +@OptIn(UnstableApi::class) // New Media3 Compose artifact is currently experimental +@Composable +fun VideoPlayer( + videoLink: String?, + modifier: Modifier = Modifier, +) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(id = R.drawable.promo_video_placeholder), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier, + ) + return + } else { + val context = LocalContext.current + var player by remember { mutableStateOf(null) } + LifecycleStartEffect(videoLink) { + if (videoLink != null) { + player = ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri(videoLink)) + repeatMode = Player.REPEAT_MODE_ONE + prepare() + } + } + onStopOrDispose { + player?.release() + player = null + } + } + + var videoFullyOnScreen by remember { mutableStateOf(false) } + val isWindowOccluded = LocalOcclusion.current + Box( + Modifier + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .onVisibilityChanged( + minDurationMs = 100, + minFractionVisible = 1f, + ) { fullyVisible -> videoFullyOnScreen = fullyVisible } + .then(modifier), + ) { + player?.let { currentPlayer -> + LaunchedEffect(videoFullyOnScreen, LocalOcclusion.current.value) { + if (videoFullyOnScreen && !isWindowOccluded.value) currentPlayer.play() else currentPlayer.pause() + } + + // Render the video + PlayerSurface(currentPlayer, surfaceType = SURFACE_TYPE_TEXTURE_VIEW) + + // Show a play / pause button + val playPauseButtonState = rememberPlayPauseButtonState(currentPlayer) + OutlinedIconButton( + onClick = playPauseButtonState::onClick, + enabled = playPauseButtonState.isEnabled, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + colors = IconButtonDefaults.outlinedIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + ), + ) { + val icon = + if (playPauseButtonState.showPlay) R.drawable.rounded_play_arrow_24 else R.drawable.rounded_pause_24 + val contentDescription = + if (playPauseButtonState.showPlay) R.string.play else R.string.pause + Icon( + painterResource(icon), + stringResource(contentDescription), + ) + } + } + } + } +} + +@Composable +fun VideoPlayerRotatedCard( + videoLink: String?, + modifier: Modifier = Modifier, +) { + val aspectRatio = 280f / 380f + val videoInstructionText = stringResource(R.string.instruction_video_transcript) + Box( + modifier = modifier + .focusable() + .semantics { contentDescription = videoInstructionText } + .aspectRatio(aspectRatio) + .rotate(-3f) + .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large) + .background( + color = Color.White, + shape = MaterialTheme.shapes.large, + ), + ) { + VideoPlayer( + videoLink, + modifier = Modifier + .aspectRatio(aspectRatio) + .align(Alignment.Center) + .clip(MaterialTheme.shapes.large) + .clipToBounds(), + ) + } +} From 0498d27d264c5b2a930f40ef7a320887f1c03611 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 28 Aug 2025 14:40:19 +0200 Subject: [PATCH 02/22] Add remote configuration for disabling XR layouts --- .../android/developers/androidify/RemoteConfigDataSource.kt | 6 ++++++ .../testing/network/TestRemoteConfigDataSource.kt | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt index f8a38caf..5f16603d 100644 --- a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt +++ b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt @@ -42,6 +42,8 @@ interface RemoteConfigDataSource { fun getImageGenerationEditsModelName(): String fun getBotBackgroundInstructionPrompt(): String + + fun isXrDisabled(): Boolean } @Singleton @@ -106,4 +108,8 @@ class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource override fun getBotBackgroundInstructionPrompt(): String { return remoteConfig.getString("bot_background_instruction_prompt") } + + override fun isXrDisabled(): Boolean { + return remoteConfig.getBoolean("is_xr_disabled") + } } diff --git a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt index 8f5379b6..52251654 100644 --- a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt +++ b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt @@ -75,4 +75,8 @@ class TestRemoteConfigDataSource(private val useGeminiNano: Boolean) : RemoteCon override fun getBotBackgroundInstructionPrompt(): String { return "bot_background_instruction_prompt" } + + override fun isXrDisabled(): Boolean { + return false + } } From b0a3b066d4f30612924e926ec4cfc0d2f46daa7a Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 28 Aug 2025 18:48:56 +0200 Subject: [PATCH 03/22] Add XR libraries and a new core/xr project with XR utils --- core/xr/.gitignore | 1 + core/xr/build.gradle.kts | 51 +++++++++ core/xr/proguard-rules.pro | 21 ++++ core/xr/src/main/AndroidManifest.xml | 19 ++++ .../androidify/xr/SpatialUiModes.kt | 77 +++++++++++++ .../developers/androidify/xr/XrPreview.kt | 102 ++++++++++++++++++ .../res/drawable/collapse_content_24px.xml | 26 +++++ .../main/res/drawable/expand_content_24px.xml | 26 +++++ gradle/libs.versions.toml | 4 + settings.gradle.kts | 1 + 10 files changed, 328 insertions(+) create mode 100644 core/xr/.gitignore create mode 100644 core/xr/build.gradle.kts create mode 100644 core/xr/proguard-rules.pro create mode 100644 core/xr/src/main/AndroidManifest.xml create mode 100644 core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt create mode 100644 core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt create mode 100644 core/xr/src/main/res/drawable/collapse_content_24px.xml create mode 100644 core/xr/src/main/res/drawable/expand_content_24px.xml diff --git a/core/xr/.gitignore b/core/xr/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/xr/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/xr/build.gradle.kts b/core/xr/build.gradle.kts new file mode 100644 index 00000000..58b89183 --- /dev/null +++ b/core/xr/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} +android { + namespace = "com.android.developers.androidify.xr" + compileSdk = libs.versions.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + } + + buildFeatures { + compose = true + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get()) + } + kotlinOptions { + jvmTarget = libs.versions.jvmTarget.get() + } + +} + +dependencies { + implementation(libs.androidx.xr.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(projects.core.util) + implementation(projects.core.theme) +} diff --git a/core/xr/proguard-rules.pro b/core/xr/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/xr/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/xr/src/main/AndroidManifest.xml b/core/xr/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b47d651b --- /dev/null +++ b/core/xr/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file 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 new file mode 100644 index 00000000..7a606f72 --- /dev/null +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt @@ -0,0 +1,77 @@ +/* + * 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.foundation.layout.fillMaxSize +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.xr.compose.platform.LocalSession +import androidx.xr.compose.platform.LocalSpatialCapabilities +import androidx.xr.compose.platform.LocalSpatialConfiguration +import androidx.xr.compose.platform.SpatialCapabilities +import androidx.xr.scenecore.scene + +/** Check if the device is XR-enabled, but is not yet rendering spatial UI. */ +@Composable +fun couldRequestFullSpace(): Boolean { + return LocalSpatialConfiguration.current.hasXrSpatialFeature && !LocalSpatialCapabilities.current.isSpatialUiEnabled +} + +/** Check if the device is XR-enabled and is rendering spatial UI. */ +@Composable +fun SpatialCapabilities.couldRequestHomeSpace(): Boolean { + if (!LocalSpatialConfiguration.current.hasXrSpatialFeature) return false + return isSpatialUiEnabled +} + +/** Default styling for an IconButton with a home space button and behavior. */ +@Composable +fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { + val session = LocalSession.current ?: return + + OutlinedIconButton( + modifier = modifier, + colors = IconButtonDefaults.outlinedIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + ), + onClick = { + session.scene.requestHomeSpaceMode() + }, + ) { + Icon( + modifier = Modifier + .fillMaxSize(), + imageVector = ImageVector.vectorResource(R.drawable.collapse_content_24px), + contentDescription = "To Home Space Mode", + ) + } +} + +@Composable +fun FullSpaceIcon(modifier: Modifier = Modifier) { + Icon( + modifier = modifier, + imageVector = ImageVector.vectorResource(R.drawable.expand_content_24px), + contentDescription = "To Full Space Mode", + ) +} diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt new file mode 100644 index 00000000..333c4c5c --- /dev/null +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt @@ -0,0 +1,102 @@ +/* + * 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.runtime.CompositionLocalProvider +import androidx.compose.ui.tooling.preview.Devices.PIXEL_7_PRO +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET +import androidx.compose.ui.tooling.preview.Preview +import androidx.xr.compose.platform.LocalSession +import androidx.xr.compose.platform.LocalSpatialCapabilities +import androidx.xr.compose.platform.LocalSpatialConfiguration +import androidx.xr.compose.platform.SpatialCapabilities +import androidx.xr.compose.platform.SpatialConfiguration + +/** + * Preview for a layout that could go into Full Space Mode. + */ +@Composable +fun SupportsFullSpaceModeRequestProvider(contents: @Composable () -> Unit) { + CompositionLocalProvider(LocalSpatialConfiguration provides HasSpatialFeatureSpatialConfiguration) { + CompositionLocalProvider(LocalSpatialCapabilities provides SupportsFullSpaceModeRequestCapabilities) { + CompositionLocalProvider(LocalSession provides null) { + contents() + } + } + } +} + +/** + * Workaround for b/441901724. + * Any composable referencing LocalSpatialConfiguration or LocalSpatialCapabilities will + * fail to preview instead of gracefully degrading due to failing to resolve XR capabilities. + * + * This can be removed when the default for XR capabilities under the preview is no capabilities + * instead of throwing an exception. + * */ +@Composable +fun NoXrSupportPreview(contents: @Composable () -> Unit) { + CompositionLocalProvider(LocalSpatialConfiguration provides LacksSpatialFeatureSpatialConfiguration) { + CompositionLocalProvider(LocalSpatialCapabilities provides NoSpatialCapabilities) { + CompositionLocalProvider(LocalSession provides null) { + contents() + } + } + } +} + +private object SupportsFullSpaceModeRequestCapabilities : SpatialCapabilities { + override val isSpatialUiEnabled: Boolean + get() = false + override val isContent3dEnabled: Boolean + get() = true + override val isAppEnvironmentEnabled: Boolean + get() = true + override val isPassthroughControlEnabled: Boolean + get() = true + override val isSpatialAudioEnabled: Boolean + get() = true +} + +private object NoSpatialCapabilities : SpatialCapabilities { + override val isSpatialUiEnabled: Boolean + get() = false + override val isContent3dEnabled: Boolean + get() = false + override val isAppEnvironmentEnabled: Boolean + get() = false + override val isPassthroughControlEnabled: Boolean + get() = false + override val isSpatialAudioEnabled: Boolean + get() = false +} + +private object HasSpatialFeatureSpatialConfiguration : SpatialConfiguration { + override val hasXrSpatialFeature: Boolean + get() = true +} + +private object LacksSpatialFeatureSpatialConfiguration : SpatialConfiguration { + override val hasXrSpatialFeature: Boolean + get() = false +} + +@Preview(device = PIXEL_TABLET, name = "Android XR (Home Space Mode)") +annotation class XrHomeSpaceMediumPreview + +@Preview(device = PIXEL_7_PRO, name = "Android XR (Home Space Mode)") +annotation class XrHomeSpaceCompactPreview diff --git a/core/xr/src/main/res/drawable/collapse_content_24px.xml b/core/xr/src/main/res/drawable/collapse_content_24px.xml new file mode 100644 index 00000000..f832f639 --- /dev/null +++ b/core/xr/src/main/res/drawable/collapse_content_24px.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/core/xr/src/main/res/drawable/expand_content_24px.xml b/core/xr/src/main/res/drawable/expand_content_24px.xml new file mode 100644 index 00000000..ba4da15e --- /dev/null +++ b/core/xr/src/main/res/drawable/expand_content_24px.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86e07b22..6b60e045 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,6 +65,8 @@ lifecycleProcess = "2.9.1" mlkitCommon = "18.11.0" mlkitSegmentation = "16.0.0-beta1" playServicesBase = "18.4.0" +xr-compose = "1.0.0-alpha06" + [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } @@ -143,6 +145,8 @@ google-oss-licenses = { group = "com.google.android.gms", name = "play-services- google-oss-licenses-plugin = { group = "com.google.android.gms", name = "oss-licenses-plugin", version.ref = "googleOssPlugin" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } play-services-base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "playServicesBase" } +androidx-xr-compose = { group = "androidx.xr.compose", name="compose", version.ref = "xr-compose"} + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9cf587c9..ad389d43 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,4 +32,5 @@ include(":core:network") include(":core:util") include(":core:theme") include(":core:testing") +include(":core:xr") include(":benchmark") From 92a8b89f3020f97575ade7acbda7087d9b50b176 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 28 Aug 2025 18:51:48 +0200 Subject: [PATCH 04/22] Add Spatial layout type and a spatial layout for the Home screen --- .../androidify/data/ConfigProvider.kt | 4 + feature/home/build.gradle.kts | 3 + .../developers/androidify/home/HomeScreen.kt | 58 +++++-- .../androidify/home/HomeScreenCompact.kt | 76 +++++--- .../androidify/home/HomeScreenComponents.kt | 34 ++++ .../androidify/home/HomeScreenLayoutType.kt | 35 ++++ .../androidify/home/HomeScreenMedium.kt | 66 +++++-- .../androidify/home/HomeScreenSpatial.kt | 163 ++++++++++++++++++ .../androidify/home/HomeViewModel.kt | 2 + feature/home/src/main/res/values/strings.xml | 1 + 10 files changed, 385 insertions(+), 57 deletions(-) create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt diff --git a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt index 5c0a36d7..b0da04da 100644 --- a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt +++ b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt @@ -33,4 +33,8 @@ class ConfigProvider @Inject constructor(val remoteConfigDataSource: RemoteConfi fun getDancingDroidLink(): String { return remoteConfigDataSource.getDancingDroidLink() } + + fun isXrDisabled(): Boolean { + return remoteConfigDataSource.isXrDisabled() + } } diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 61ff0d12..0c6d98b5 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -68,6 +68,9 @@ dependencies { exclude(group = "com.google.guava") } + implementation(libs.androidx.xr.compose) + implementation(projects.core.xr) + implementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling.preview) diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index fddc0d0c..b8c0e67c 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -17,7 +17,6 @@ package com.android.developers.androidify.home import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material3.ExperimentalMaterial3Api @@ -31,18 +30,17 @@ import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview -import com.android.developers.androidify.util.isAtLeastMedium @ExperimentalMaterial3ExpressiveApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun HomeScreen( homeScreenViewModel: HomeViewModel = hiltViewModel(), - isMediumWindowSize: Boolean = isAtLeastMedium(), onClickLetsGo: (IntOffset) -> Unit = {}, onAboutClicked: () -> Unit = {}, ) { val state = homeScreenViewModel.state.collectAsStateWithLifecycle() + val layoutType = calculateLayoutType(state.value.isXrDisabled) if (!state.value.isAppActive) { AppInactiveScreen() @@ -50,7 +48,7 @@ fun HomeScreen( HomeScreenContents( state.value.videoLink, state.value.dancingDroidLink, - isMediumWindowSize, + layoutType, onClickLetsGo, onAboutClicked, ) @@ -61,21 +59,13 @@ fun HomeScreen( fun HomeScreenContents( videoLink: String?, dancingBotLink: String?, - isMediumWindowSize: Boolean, + layoutType: HomeScreenLayoutType, onClickLetsGo: (IntOffset) -> Unit, onAboutClicked: () -> Unit, ) { - Box { - SquiggleBackground() - - Column( - modifier = Modifier - .fillMaxSize() - .safeDrawingPadding(), - ) { - if (isMediumWindowSize) { - HomeScreenMediumContents(Modifier.weight(1f), videoLink, dancingBotLink, onClickLetsGo) - } else { + when (layoutType) { + HomeScreenLayoutType.Compact -> + SquiggleBackgroundBox { HomeScreenCompactPager( videoLink, dancingBotLink, @@ -83,6 +73,38 @@ fun HomeScreenContents( onAboutClicked, ) } + + HomeScreenLayoutType.Medium -> + SquiggleBackgroundBox { + HomeScreenMediumContents( + Modifier, + videoLink, + dancingBotLink, + onClickLetsGo, + ) + } + + HomeScreenLayoutType.Spatial -> + HomeScreenContentsSpatial( + videoLink, + dancingBotLink, + onClickLetsGo, + onAboutClicked, + ) + } +} + +@Composable +private fun SquiggleBackgroundBox(contents: @Composable () -> Unit) { + Box { + SquiggleBackground() + + Box( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + contents() } } } @@ -93,7 +115,7 @@ fun HomeScreenContents( private fun HomeScreenPhonePreview() { SharedElementContextPreview { HomeScreenContents( - isMediumWindowSize = false, + layoutType = HomeScreenLayoutType.Compact, onClickLetsGo = {}, videoLink = "", dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", @@ -108,7 +130,7 @@ private fun HomeScreenPhonePreview() { private fun HomeScreenLargeScreensPreview() { SharedElementContextPreview { HomeScreenContents( - isMediumWindowSize = true, + layoutType = HomeScreenLayoutType.Medium, onClickLetsGo = { }, videoLink = "", dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index 02fcb1f9..4ca6b4a2 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -51,6 +51,10 @@ import com.android.developers.androidify.theme.Blue import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.util.PhonePreview +import com.android.developers.androidify.xr.NoXrSupportPreview +import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider +import com.android.developers.androidify.xr.XrHomeSpaceCompactPreview +import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenCompactPager( @@ -124,20 +128,29 @@ fun HomeScreenCompactPager( var buttonPosition by remember { mutableStateOf(IntOffset.Zero) } - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - buttonPosition = it.boundsInWindow.center - } - .padding(bottom = 16.dp) - .height(64.dp) - .width(220.dp), - colors = ButtonDefaults.buttonColors() - .copy(containerColor = Blue), - onClick = { - onClick(buttonPosition) - }, - ) + Column { + if (couldRequestFullSpace()) { + ViewInFullSpaceButton( + modifier = Modifier + .height(64.dp), + ) + Spacer(modifier = Modifier.size(12.dp)) + } + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + buttonPosition = it.boundsInWindow.center + } + .padding(bottom = 16.dp) + .height(64.dp) + .width(220.dp), + colors = ButtonDefaults.buttonColors() + .copy(containerColor = Blue), + onClick = { + onClick(buttonPosition) + }, + ) + } } } @@ -145,13 +158,32 @@ fun HomeScreenCompactPager( @PhonePreview @Composable private fun HomeScreenPhonePreview() { - SharedElementContextPreview { - HomeScreenContents( - isMediumWindowSize = false, - onClickLetsGo = {}, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) + NoXrSupportPreview { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } + } +} + +@ExperimentalMaterial3ExpressiveApi +@XrHomeSpaceCompactPreview +@Composable +private fun HomeScreenCompactXrHomeSpacePreview() { + SupportsFullSpaceModeRequestProvider { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt index 678600e5..b99c18a5 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt @@ -25,9 +25,11 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.TextAutoSize @@ -35,6 +37,7 @@ import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -58,8 +61,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.xr.compose.platform.LocalSession +import androidx.xr.scenecore.scene import coil3.compose.AsyncImage import com.android.developers.androidify.theme.Blue +import com.android.developers.androidify.xr.FullSpaceIcon import com.android.developers.androidify.theme.R as ThemeR @Composable @@ -156,6 +162,34 @@ fun HomePageButton( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ViewInFullSpaceButton( + modifier: Modifier = Modifier, + colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), +) { + val style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight(700), + letterSpacing = .15f.sp, + ) + val session = LocalSession.current + + Button( + onClick = { + session?.scene?.requestFullSpaceMode() + }, + modifier = modifier, + colors = colors, + ) { + FullSpaceIcon(Modifier.size(ButtonDefaults.LargeIconSize)) + Spacer(Modifier.width(ButtonDefaults.LargeIconSpacing)) + Text( + stringResource(R.string.full_space_button_label), + style = style, + ) + } +} + @Composable private fun DancingBot( dancingBotLink: String?, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt new file mode 100644 index 00000000..c0344d38 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.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.home + +import androidx.compose.runtime.Composable +import androidx.xr.compose.platform.LocalSpatialCapabilities +import com.android.developers.androidify.util.isAtLeastMedium + +enum class HomeScreenLayoutType { + Compact, + Medium, + Spatial, +} + +@Composable +fun calculateLayoutType(disableXr: Boolean = false): HomeScreenLayoutType { + return when { + LocalSpatialCapabilities.current.isSpatialUiEnabled && !disableXr -> HomeScreenLayoutType.Spatial + isAtLeastMedium() -> HomeScreenLayoutType.Medium + else -> HomeScreenLayoutType.Compact + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index 03dc4d1c..c60a2c0e 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -17,6 +17,7 @@ package com.android.developers.androidify.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -34,6 +35,10 @@ import androidx.compose.ui.unit.dp import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar import com.android.developers.androidify.util.LargeScreensPreview +import com.android.developers.androidify.xr.NoXrSupportPreview +import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider +import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview +import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenMediumContents( @@ -68,19 +73,27 @@ fun HomeScreenMediumContents( .align(Alignment.CenterVertically), ) { MainHomeContent(dancingBotLink) - HomePageButton( + Row( modifier = Modifier - .onLayoutRectChanged { - positionButtonClick = it.boundsInWindow.center - } .align(Alignment.BottomCenter) .padding(bottom = 16.dp) - .height(64.dp) - .width(220.dp), - onClick = { - onClickLetsGo(positionButtonClick) - }, - ) + .height(64.dp), + ) { + if (couldRequestFullSpace()) { + ViewInFullSpaceButton() + Spacer(Modifier.width(16.dp)) + } + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + positionButtonClick = it.boundsInWindow.center + } + .width(220.dp), + onClick = { + onClickLetsGo(positionButtonClick) + }, + ) + } } } } @@ -89,13 +102,32 @@ fun HomeScreenMediumContents( @LargeScreensPreview @Composable private fun HomeScreenLargeScreensPreview() { + NoXrSupportPreview { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } + } +} + +@XrHomeSpaceMediumPreview +@ExperimentalMaterial3ExpressiveApi +@Composable +private fun HomeScreenXrHomeSpacePreview() { SharedElementContextPreview { - HomeScreenContents( - isMediumWindowSize = true, - onClickLetsGo = { }, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) + SupportsFullSpaceModeRequestProvider { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt new file mode 100644 index 00000000..817b3200 --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt @@ -0,0 +1,163 @@ +/* + * 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.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +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.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.layout.onLayoutRectChanged +import androidx.compose.ui.unit.IntOffset +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.spatial.Subspace +import androidx.xr.compose.subspace.SpatialPanel +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.fillMaxWidth +import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.offset +import androidx.xr.compose.subspace.layout.resizable +import androidx.xr.compose.subspace.layout.rotate +import com.android.developers.androidify.theme.SharedElementContextPreview +import com.android.developers.androidify.theme.components.AndroidifyTopAppBar +import com.android.developers.androidify.theme.components.SquiggleBackground +import com.android.developers.androidify.util.TabletPreview +import com.android.developers.androidify.xr.RequestHomeSpaceIconButton + +@Composable +fun HomeScreenContentsSpatial( + videoLink: String?, + dancingBotLink: String?, + onClickLetsGo: (IntOffset) -> Unit, + onAboutClicked: () -> Unit, +) { + Subspace { + SpatialPanel( + SubspaceModifier + .movable() + .resizable() + .fillMaxWidth(1f) + .aspectRatio(1.7f), + ) { + Orbiter( + position = ContentEdge.Top, + offsetType = OrbiterOffsetType.InnerEdge, + 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) + } + } + } + } +} + +@Composable +private fun HomeScreenSpatialMainContent( + dancingBotLink: String?, + onClickLetsGo: (IntOffset) -> Unit, + onAboutClicked: () -> Unit, +) { + var positionButtonClick by remember { + mutableStateOf(IntOffset.Zero) + } + Box { + SquiggleBackground() + Box( + modifier = Modifier + .fillMaxWidth(0.55f) + .fillMaxHeight(1f) + .align(Alignment.Center), + ) { + MainHomeContent( + modifier = Modifier + .fillMaxHeight(0.6f) + .align(Alignment.Center), + dancingBotLink = dancingBotLink, + ) + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp), + ) { + AndroidifyTopAppBar( + modifier = Modifier + .width(220.dp) + .padding(bottom = 16.dp), + aboutEnabled = true, + onAboutClicked = onAboutClicked, + ) + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + positionButtonClick = it.boundsInWindow.center + } + .height(64.dp) + .width(220.dp), + onClick = { + onClickLetsGo(positionButtonClick) + }, + ) + } + } + } +} + +@TabletPreview +@Composable +private fun HomeScreenSpatialMainContentPreview() { + SharedElementContextPreview { + HomeScreenSpatialMainContent( + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onClickLetsGo = {}, + onAboutClicked = {}, + ) + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt index 40d3ab14..889f4dff 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt @@ -29,6 +29,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo isAppActive = !configProvider.isAppInactive(), dancingDroidLink = configProvider.getDancingDroidLink(), videoLink = configProvider.getPromoVideoLink(), + isXrDisabled = configProvider.isXrDisabled(), ), ) val state = _state.asStateFlow() @@ -36,6 +37,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo data class HomeState( val isAppActive: Boolean = true, + val isXrDisabled: Boolean = false, val videoLink: String? = null, val dancingDroidLink: String? = null, ) diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index cdb99eed..9412592f 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Customize your own\n \n\nAndroid bot Let\'s Go + To Full Space Check back later. App is under construction. How it works From 45300c1fb1ccdc181feb78f777a6d02167b01b56 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 14:16:09 +0200 Subject: [PATCH 05/22] Add transparent squiggle to match design spec --- .../theme/components/Backgrounds.kt | 27 ++++++++++++++ .../src/main/res/drawable/squiggle_full.xml | 35 +++++++++++++++++++ .../androidify/home/HomeScreenSpatial.kt | 3 +- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 core/theme/src/main/res/drawable/squiggle_full.xml diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt index 976546d2..9a1a2266 100644 --- a/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt +++ b/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt @@ -36,6 +36,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.android.developers.androidify.theme.AndroidifyTheme @@ -78,6 +80,31 @@ fun SquiggleBackground( } } +/** + * Background squiggle that tries to fit in its parent. + */ +@Composable +fun SquiggleBackgroundFull() { + val vectorBackground = + rememberVectorPainter(ImageVector.vectorResource(R.drawable.squiggle_full)) + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = vectorBackground, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + ) + } +} + +@Preview(device = PIXEL_TABLET) +@Composable +fun SquiggleFullImagePreview() { + AndroidifyTheme { + SquiggleBackgroundFull() + } +} + @LargeScreensPreview @Composable private fun SquiggleBackgroundLargePreview() { diff --git a/core/theme/src/main/res/drawable/squiggle_full.xml b/core/theme/src/main/res/drawable/squiggle_full.xml new file mode 100644 index 00000000..4fc308ca --- /dev/null +++ b/core/theme/src/main/res/drawable/squiggle_full.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt index 817b3200..1b5007ec 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt @@ -50,6 +50,7 @@ import androidx.xr.compose.subspace.layout.rotate import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.SquiggleBackground +import com.android.developers.androidify.theme.components.SquiggleBackgroundFull import com.android.developers.androidify.util.TabletPreview import com.android.developers.androidify.xr.RequestHomeSpaceIconButton @@ -109,7 +110,7 @@ private fun HomeScreenSpatialMainContent( mutableStateOf(IntOffset.Zero) } Box { - SquiggleBackground() + SquiggleBackgroundFull() Box( modifier = Modifier .fillMaxWidth(0.55f) From 5191b97bd9b8a13fb2cf881395cbc7e15bad22f1 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 16:13:54 +0200 Subject: [PATCH 06/22] Separate XR components into xr namespace --- .../developers/androidify/home/HomeScreen.kt | 1 + .../androidify/home/HomeScreenCompact.kt | 22 +------ .../androidify/home/HomeScreenComponents.kt | 34 ---------- .../androidify/home/HomeScreenMedium.kt | 22 +------ .../xr/HomeScreenHomeSpaceModePreviews.kt | 59 +++++++++++++++++ .../home/{ => xr}/HomeScreenSpatial.kt | 6 +- .../home/xr/ViewInFullSpaceModeButton.kt | 64 +++++++++++++++++++ feature/home/src/main/res/values/strings.xml | 2 +- 8 files changed, 133 insertions(+), 77 deletions(-) create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt rename feature/home/src/main/java/com/android/developers/androidify/home/{ => xr}/HomeScreenSpatial.kt (96%) create mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index b8c0e67c..dfd8bf85 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.developers.androidify.home.xr.HomeScreenContentsSpatial import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.util.LargeScreensPreview diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index 4ca6b4a2..6ad9c9ed 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -47,13 +47,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onLayoutRectChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.android.developers.androidify.home.xr.ViewInFullSpaceModeButton import com.android.developers.androidify.theme.Blue import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.util.PhonePreview import com.android.developers.androidify.xr.NoXrSupportPreview -import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider -import com.android.developers.androidify.xr.XrHomeSpaceCompactPreview import com.android.developers.androidify.xr.couldRequestFullSpace @Composable @@ -130,7 +129,7 @@ fun HomeScreenCompactPager( } Column { if (couldRequestFullSpace()) { - ViewInFullSpaceButton( + ViewInFullSpaceModeButton( modifier = Modifier .height(64.dp), ) @@ -170,20 +169,3 @@ private fun HomeScreenPhonePreview() { } } } - -@ExperimentalMaterial3ExpressiveApi -@XrHomeSpaceCompactPreview -@Composable -private fun HomeScreenCompactXrHomeSpacePreview() { - SupportsFullSpaceModeRequestProvider { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Compact, - onClickLetsGo = {}, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } - } -} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt index b99c18a5..678600e5 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt @@ -25,11 +25,9 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.TextAutoSize @@ -37,7 +35,6 @@ import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -61,11 +58,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.xr.compose.platform.LocalSession -import androidx.xr.scenecore.scene import coil3.compose.AsyncImage import com.android.developers.androidify.theme.Blue -import com.android.developers.androidify.xr.FullSpaceIcon import com.android.developers.androidify.theme.R as ThemeR @Composable @@ -162,34 +156,6 @@ fun HomePageButton( } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ViewInFullSpaceButton( - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), -) { - val style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight(700), - letterSpacing = .15f.sp, - ) - val session = LocalSession.current - - Button( - onClick = { - session?.scene?.requestFullSpaceMode() - }, - modifier = modifier, - colors = colors, - ) { - FullSpaceIcon(Modifier.size(ButtonDefaults.LargeIconSize)) - Spacer(Modifier.width(ButtonDefaults.LargeIconSpacing)) - Text( - stringResource(R.string.full_space_button_label), - style = style, - ) - } -} - @Composable private fun DancingBot( dancingBotLink: String?, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index c60a2c0e..8d0a8716 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -32,12 +32,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onLayoutRectChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.android.developers.androidify.home.xr.ViewInFullSpaceModeButton import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.xr.NoXrSupportPreview -import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider -import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview import com.android.developers.androidify.xr.couldRequestFullSpace @Composable @@ -80,7 +79,7 @@ fun HomeScreenMediumContents( .height(64.dp), ) { if (couldRequestFullSpace()) { - ViewInFullSpaceButton() + ViewInFullSpaceModeButton() Spacer(Modifier.width(16.dp)) } HomePageButton( @@ -114,20 +113,3 @@ private fun HomeScreenLargeScreensPreview() { } } } - -@XrHomeSpaceMediumPreview -@ExperimentalMaterial3ExpressiveApi -@Composable -private fun HomeScreenXrHomeSpacePreview() { - SharedElementContextPreview { - SupportsFullSpaceModeRequestProvider { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Medium, - onClickLetsGo = { }, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } - } -} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt new file mode 100644 index 00000000..22df109a --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt @@ -0,0 +1,59 @@ +/* + * 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.home.xr + +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.runtime.Composable +import com.android.developers.androidify.home.HomeScreenContents +import com.android.developers.androidify.home.HomeScreenLayoutType +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 HomeScreenMediumXrHomeSpaceModePreview() { + SharedElementContextPreview { + SupportsFullSpaceModeRequestProvider { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } + } +} + +@ExperimentalMaterial3ExpressiveApi +@XrHomeSpaceCompactPreview +@Composable +private fun HomeScreenCompactXrHomeSpaceModePreview() { + SupportsFullSpaceModeRequestProvider { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } + } +} diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt similarity index 96% rename from feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt rename to feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt index 1b5007ec..ca2492ae 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenSpatial.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.developers.androidify.home +package com.android.developers.androidify.home.xr import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -47,9 +47,11 @@ import androidx.xr.compose.subspace.layout.movable import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.resizable import androidx.xr.compose.subspace.layout.rotate +import com.android.developers.androidify.home.HomePageButton +import com.android.developers.androidify.home.MainHomeContent +import com.android.developers.androidify.home.VideoPlayer import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar -import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.theme.components.SquiggleBackgroundFull import com.android.developers.androidify.util.TabletPreview import com.android.developers.androidify.xr.RequestHomeSpaceIconButton diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt new file mode 100644 index 00000000..1f22f46a --- /dev/null +++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt @@ -0,0 +1,64 @@ +/* + * 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.home.xr + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import androidx.xr.compose.platform.LocalSession +import androidx.xr.scenecore.scene +import com.android.developers.androidify.home.R +import com.android.developers.androidify.theme.Blue +import com.android.developers.androidify.xr.FullSpaceIcon + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ViewInFullSpaceModeButton( + modifier: Modifier = Modifier, + colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), +) { + val style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight(700), + letterSpacing = .15f.sp, + ) + val session = LocalSession.current + + Button( + onClick = { + session?.scene?.requestFullSpaceMode() + }, + modifier = modifier, + colors = colors, + ) { + FullSpaceIcon(Modifier.size(ButtonDefaults.LargeIconSize)) + Spacer(Modifier.width(ButtonDefaults.LargeIconSpacing)) + Text( + stringResource(R.string.xr_full_space_button_label), + style = style, + ) + } +} diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index 9412592f..2bf7e7ff 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -28,7 +28,7 @@ Customize your own\n \n\nAndroid bot Let\'s Go - To Full Space + To Full Space Check back later. App is under construction. How it works From 512f78a12dac74f9a00432aa08400149d61c417b Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 16:15:23 +0200 Subject: [PATCH 07/22] Fix broken previews in HomeScreen.kt --- .../developers/androidify/home/HomeScreen.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index dfd8bf85..c7b7d5dc 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -31,6 +31,7 @@ import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview +import com.android.developers.androidify.xr.NoXrSupportPreview @ExperimentalMaterial3ExpressiveApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -114,14 +115,16 @@ private fun SquiggleBackgroundBox(contents: @Composable () -> Unit) { @PhonePreview @Composable private fun HomeScreenPhonePreview() { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Compact, - onClickLetsGo = {}, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) + NoXrSupportPreview { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } } } @@ -129,13 +132,15 @@ private fun HomeScreenPhonePreview() { @LargeScreensPreview @Composable private fun HomeScreenLargeScreensPreview() { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Medium, - onClickLetsGo = { }, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) + NoXrSupportPreview { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } } } From 4ce5edb49eed1c40682358c67d9130d84837468b Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 29 Aug 2025 16:28:40 +0200 Subject: [PATCH 08/22] Create XR composition locals that will never throw to work around b/441901724. --- .../androidify/xr/SpatialUiModes.kt | 3 - .../developers/androidify/xr/XrPreview.kt | 76 +++++++++++++------ .../developers/androidify/home/HomeScreen.kt | 37 ++++----- .../androidify/home/HomeScreenCompact.kt | 19 ++--- .../androidify/home/HomeScreenMedium.kt | 19 ++--- 5 files changed, 83 insertions(+), 71 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 7a606f72..8eed9579 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 @@ -24,9 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource -import androidx.xr.compose.platform.LocalSession -import androidx.xr.compose.platform.LocalSpatialCapabilities -import androidx.xr.compose.platform.LocalSpatialConfiguration import androidx.xr.compose.platform.SpatialCapabilities import androidx.xr.scenecore.scene diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt index 333c4c5c..b06d5e65 100644 --- a/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt @@ -17,14 +17,14 @@ package com.android.developers.androidify.xr import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalWithComputedDefaultOf import androidx.compose.ui.tooling.preview.Devices.PIXEL_7_PRO import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview -import androidx.xr.compose.platform.LocalSession -import androidx.xr.compose.platform.LocalSpatialCapabilities -import androidx.xr.compose.platform.LocalSpatialConfiguration import androidx.xr.compose.platform.SpatialCapabilities import androidx.xr.compose.platform.SpatialConfiguration +import androidx.xr.runtime.Session /** * Preview for a layout that could go into Full Space Mode. @@ -40,23 +40,9 @@ fun SupportsFullSpaceModeRequestProvider(contents: @Composable () -> Unit) { } } -/** - * Workaround for b/441901724. - * Any composable referencing LocalSpatialConfiguration or LocalSpatialCapabilities will - * fail to preview instead of gracefully degrading due to failing to resolve XR capabilities. - * - * This can be removed when the default for XR capabilities under the preview is no capabilities - * instead of throwing an exception. - * */ -@Composable -fun NoXrSupportPreview(contents: @Composable () -> Unit) { - CompositionLocalProvider(LocalSpatialConfiguration provides LacksSpatialFeatureSpatialConfiguration) { - CompositionLocalProvider(LocalSpatialCapabilities provides NoSpatialCapabilities) { - CompositionLocalProvider(LocalSession provides null) { - contents() - } - } - } +private object HasSpatialFeatureSpatialConfiguration : SpatialConfiguration { + override val hasXrSpatialFeature: Boolean + get() = true } private object SupportsFullSpaceModeRequestCapabilities : SpatialCapabilities { @@ -72,6 +58,51 @@ private object SupportsFullSpaceModeRequestCapabilities : SpatialCapabilities { get() = true } +/** + * Workaround composition locals for b/441901724. + * Any composable referencing XR composition locals will fail to preview instead of gracefully + * degrading due to failing to resolve XR capabilities. + * + * This can be removed when the default for XR capabilities under the preview is no capabilities + * instead of throwing an exception. + * */ +val LocalSpatialCapabilities: ProvidableCompositionLocal = + compositionLocalWithComputedDefaultOf { + runCatching { + androidx.xr.compose.platform.LocalSpatialCapabilities.currentValue + }.getOrDefault(NoSpatialCapabilities) + } + +/** + * Workaround composition locals for b/441901724. + * Any composable referencing XR composition locals will fail to preview instead of gracefully + * degrading due to failing to resolve XR capabilities. + * + * This can be removed when the default for XR capabilities under the preview is no capabilities + * instead of throwing an exception. + * */ +val LocalSpatialConfiguration: ProvidableCompositionLocal = + compositionLocalWithComputedDefaultOf { + runCatching { + androidx.xr.compose.platform.LocalSpatialConfiguration.currentValue + }.getOrDefault(LacksSpatialFeatureSpatialConfiguration) + } + +/** + * Workaround composition locals for b/441901724. + * Any composable referencing XR composition locals will fail to preview instead of gracefully + * degrading due to failing to resolve XR capabilities. + * + * This can be removed when the default for XR capabilities under the preview is no capabilities + * instead of throwing an exception. + * */ +val LocalSession: ProvidableCompositionLocal = + compositionLocalWithComputedDefaultOf { + runCatching { + androidx.xr.compose.platform.LocalSession.currentValue + }.getOrNull() + } + private object NoSpatialCapabilities : SpatialCapabilities { override val isSpatialUiEnabled: Boolean get() = false @@ -85,11 +116,6 @@ private object NoSpatialCapabilities : SpatialCapabilities { get() = false } -private object HasSpatialFeatureSpatialConfiguration : SpatialConfiguration { - override val hasXrSpatialFeature: Boolean - get() = true -} - private object LacksSpatialFeatureSpatialConfiguration : SpatialConfiguration { override val hasXrSpatialFeature: Boolean get() = false diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index c7b7d5dc..dfd8bf85 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -31,7 +31,6 @@ import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.SquiggleBackground import com.android.developers.androidify.util.LargeScreensPreview import com.android.developers.androidify.util.PhonePreview -import com.android.developers.androidify.xr.NoXrSupportPreview @ExperimentalMaterial3ExpressiveApi @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -115,16 +114,14 @@ private fun SquiggleBackgroundBox(contents: @Composable () -> Unit) { @PhonePreview @Composable private fun HomeScreenPhonePreview() { - NoXrSupportPreview { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Compact, - onClickLetsGo = {}, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) } } @@ -132,15 +129,13 @@ private fun HomeScreenPhonePreview() { @LargeScreensPreview @Composable private fun HomeScreenLargeScreensPreview() { - NoXrSupportPreview { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Medium, - onClickLetsGo = { }, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index 6ad9c9ed..fba9ac5b 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -52,7 +52,6 @@ import com.android.developers.androidify.theme.Blue import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.util.PhonePreview -import com.android.developers.androidify.xr.NoXrSupportPreview import com.android.developers.androidify.xr.couldRequestFullSpace @Composable @@ -157,15 +156,13 @@ fun HomeScreenCompactPager( @PhonePreview @Composable private fun HomeScreenPhonePreview() { - NoXrSupportPreview { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Compact, - onClickLetsGo = {}, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Compact, + onClickLetsGo = {}, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index 8d0a8716..ee606f87 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -36,7 +36,6 @@ import com.android.developers.androidify.home.xr.ViewInFullSpaceModeButton import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar import com.android.developers.androidify.util.LargeScreensPreview -import com.android.developers.androidify.xr.NoXrSupportPreview import com.android.developers.androidify.xr.couldRequestFullSpace @Composable @@ -101,15 +100,13 @@ fun HomeScreenMediumContents( @LargeScreensPreview @Composable private fun HomeScreenLargeScreensPreview() { - NoXrSupportPreview { - SharedElementContextPreview { - HomeScreenContents( - layoutType = HomeScreenLayoutType.Medium, - onClickLetsGo = { }, - videoLink = "", - dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", - onAboutClicked = {}, - ) - } + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) } } From 3a2b2b078014857cdcf030943a05e238eaa2a4fe Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Mon, 1 Sep 2025 16:08:21 +0200 Subject: [PATCH 09/22] Refactor the XR disable flag to XR enable flag --- .../android/developers/androidify/RemoteConfigDataSource.kt | 6 +++--- .../testing/network/TestRemoteConfigDataSource.kt | 2 +- .../android/developers/androidify/data/ConfigProvider.kt | 4 ++-- .../com/android/developers/androidify/home/HomeScreen.kt | 2 +- .../developers/androidify/home/HomeScreenLayoutType.kt | 4 ++-- .../com/android/developers/androidify/home/HomeViewModel.kt | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt index 5f16603d..77ba6ea0 100644 --- a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt +++ b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt @@ -43,7 +43,7 @@ interface RemoteConfigDataSource { fun getBotBackgroundInstructionPrompt(): String - fun isXrDisabled(): Boolean + fun isXrEnabled(): Boolean } @Singleton @@ -109,7 +109,7 @@ class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource return remoteConfig.getString("bot_background_instruction_prompt") } - override fun isXrDisabled(): Boolean { - return remoteConfig.getBoolean("is_xr_disabled") + override fun isXrEnabled(): Boolean { + return remoteConfig.getBoolean("xr_feature_enabled") } } diff --git a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt index 52251654..2178975c 100644 --- a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt +++ b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt @@ -76,7 +76,7 @@ class TestRemoteConfigDataSource(private val useGeminiNano: Boolean) : RemoteCon return "bot_background_instruction_prompt" } - override fun isXrDisabled(): Boolean { + override fun isXrEnabled(): Boolean { return false } } diff --git a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt index b0da04da..8dda915f 100644 --- a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt +++ b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt @@ -34,7 +34,7 @@ class ConfigProvider @Inject constructor(val remoteConfigDataSource: RemoteConfi return remoteConfigDataSource.getDancingDroidLink() } - fun isXrDisabled(): Boolean { - return remoteConfigDataSource.isXrDisabled() + fun isXrEnabled(): Boolean { + return remoteConfigDataSource.isXrEnabled() } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index dfd8bf85..e3ba715a 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -41,7 +41,7 @@ fun HomeScreen( onAboutClicked: () -> Unit = {}, ) { val state = homeScreenViewModel.state.collectAsStateWithLifecycle() - val layoutType = calculateLayoutType(state.value.isXrDisabled) + val layoutType = calculateLayoutType(state.value.isXrEnabled) if (!state.value.isAppActive) { AppInactiveScreen() diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt index c0344d38..2828f9e2 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt @@ -26,9 +26,9 @@ enum class HomeScreenLayoutType { } @Composable -fun calculateLayoutType(disableXr: Boolean = false): HomeScreenLayoutType { +fun calculateLayoutType(enableXr: Boolean = false): HomeScreenLayoutType { return when { - LocalSpatialCapabilities.current.isSpatialUiEnabled && !disableXr -> HomeScreenLayoutType.Spatial + LocalSpatialCapabilities.current.isSpatialUiEnabled && enableXr -> HomeScreenLayoutType.Spatial isAtLeastMedium() -> HomeScreenLayoutType.Medium else -> HomeScreenLayoutType.Compact } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt index 889f4dff..51475ff5 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt @@ -29,7 +29,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo isAppActive = !configProvider.isAppInactive(), dancingDroidLink = configProvider.getDancingDroidLink(), videoLink = configProvider.getPromoVideoLink(), - isXrDisabled = configProvider.isXrDisabled(), + isXrEnabled = configProvider.isXrEnabled(), ), ) val state = _state.asStateFlow() @@ -37,7 +37,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo data class HomeState( val isAppActive: Boolean = true, - val isXrDisabled: Boolean = false, + val isXrEnabled: Boolean = false, val videoLink: String? = null, val dancingDroidLink: String? = null, ) From 6d1b5988950d6f9b416bfacbd959ac3535765060 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 11:20:59 +0200 Subject: [PATCH 10/22] Match new design spec. This commit also adds the About button to the Medium layouts by removing the Translucent top bar. This matches the new spec and also makes the Compact layout and Medium layout more similar in functionality. --- .../androidify/theme/components/TopAppBar.kt | 35 ---------- .../androidify/xr/SpatialUiModes.kt | 14 ++-- .../developers/androidify/home/HomeScreen.kt | 1 + .../androidify/home/HomeScreenCompact.kt | 39 ++++------- .../androidify/home/HomeScreenMedium.kt | 41 ++++++------ .../androidify/home/xr/HomeScreenSpatial.kt | 53 ++++++++------- .../home/xr/ViewInFullSpaceModeButton.kt | 64 ------------------- 7 files changed, 67 insertions(+), 180 deletions(-) delete mode 100644 feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt index 41d40f7e..8a1a13dd 100644 --- a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt +++ b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt @@ -26,20 +26,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable 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.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -143,38 +140,6 @@ private fun BackButton(onBackPressed: () -> Unit) { } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AndroidifyTranslucentTopAppBar( - modifier: Modifier = Modifier, - titleText: String = stringResource(R.string.androidify_title), - isMediumSizeLayout: Boolean = false, -) { - if (isMediumSizeLayout) { - TopAppBar( - title = { - Spacer(Modifier.statusBarsPadding()) - AndroidifyTitle(titleText) - }, - modifier = modifier.clip( - MaterialTheme.shapes.large.copy(topStart = CornerSize(0f), topEnd = CornerSize(0f)), - ), - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - ) - } else { - CenterAlignedTopAppBar( - title = { - Spacer(Modifier.statusBarsPadding()) - AndroidifyTitle(titleText) - }, - modifier = modifier.clip( - MaterialTheme.shapes.large.copy(topStart = CornerSize(0f), topEnd = CornerSize(0f)), - ), - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - ) - } -} - @Composable private fun AndroidifyTitle(text: String) { Text(text, fontWeight = FontWeight.Bold) 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 8eed9579..f9aee545 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 @@ -16,14 +16,16 @@ package com.android.developers.androidify.xr 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.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp import androidx.xr.compose.platform.SpatialCapabilities import androidx.xr.scenecore.scene @@ -45,11 +47,10 @@ fun SpatialCapabilities.couldRequestHomeSpace(): Boolean { fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { val session = LocalSession.current ?: return - OutlinedIconButton( + IconButton( modifier = modifier, - colors = IconButtonDefaults.outlinedIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, ), onClick = { session.scene.requestHomeSpaceMode() @@ -57,7 +58,8 @@ fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { ) { Icon( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .padding(8.dp), imageVector = ImageVector.vectorResource(R.drawable.collapse_content_24px), contentDescription = "To Home Space Mode", ) diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index e3ba715a..60d9661b 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -82,6 +82,7 @@ fun HomeScreenContents( videoLink, dancingBotLink, onClickLetsGo, + onAboutClicked, ) } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index fba9ac5b..f5e9d3ba 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -47,12 +47,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onLayoutRectChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import com.android.developers.androidify.home.xr.ViewInFullSpaceModeButton import com.android.developers.androidify.theme.Blue import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.util.PhonePreview -import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenCompactPager( @@ -126,29 +124,20 @@ fun HomeScreenCompactPager( var buttonPosition by remember { mutableStateOf(IntOffset.Zero) } - Column { - if (couldRequestFullSpace()) { - ViewInFullSpaceModeButton( - modifier = Modifier - .height(64.dp), - ) - Spacer(modifier = Modifier.size(12.dp)) - } - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - buttonPosition = it.boundsInWindow.center - } - .padding(bottom = 16.dp) - .height(64.dp) - .width(220.dp), - colors = ButtonDefaults.buttonColors() - .copy(containerColor = Blue), - onClick = { - onClick(buttonPosition) - }, - ) - } + HomePageButton( + modifier = Modifier + .onLayoutRectChanged { + buttonPosition = it.boundsInWindow.center + } + .padding(bottom = 16.dp) + .height(64.dp) + .width(220.dp), + colors = ButtonDefaults.buttonColors() + .copy(containerColor = Blue), + onClick = { + onClick(buttonPosition) + }, + ) } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index ee606f87..f4b3e2db 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -17,7 +17,6 @@ package com.android.developers.androidify.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -32,11 +31,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onLayoutRectChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import com.android.developers.androidify.home.xr.ViewInFullSpaceModeButton import com.android.developers.androidify.theme.SharedElementContextPreview -import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar +import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.util.LargeScreensPreview -import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenMediumContents( @@ -44,11 +41,16 @@ fun HomeScreenMediumContents( videoLink: String?, dancingBotLink: String?, onClickLetsGo: (IntOffset) -> Unit, + onAboutClicked: () -> Unit, ) { var positionButtonClick by remember { mutableStateOf(IntOffset.Zero) } - AndroidifyTranslucentTopAppBar(isMediumSizeLayout = true) + AndroidifyTopAppBar( + isMediumWindowSize = true, + aboutEnabled = true, + onAboutClicked = onAboutClicked, + ) Row( modifier = modifier @@ -71,27 +73,20 @@ fun HomeScreenMediumContents( .align(Alignment.CenterVertically), ) { MainHomeContent(dancingBotLink) - Row( + + HomePageButton( modifier = Modifier + .onLayoutRectChanged { + positionButtonClick = it.boundsInWindow.center + } .align(Alignment.BottomCenter) .padding(bottom = 16.dp) - .height(64.dp), - ) { - if (couldRequestFullSpace()) { - ViewInFullSpaceModeButton() - Spacer(Modifier.width(16.dp)) - } - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - positionButtonClick = it.boundsInWindow.center - } - .width(220.dp), - onClick = { - onClickLetsGo(positionButtonClick) - }, - ) - } + .height(64.dp) + .width(220.dp), + onClick = { + onClickLetsGo(positionButtonClick) + }, + ) } } } 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 ca2492ae..90d9f23b 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 @@ -73,7 +73,8 @@ fun HomeScreenContentsSpatial( ) { Orbiter( position = ContentEdge.Top, - offsetType = OrbiterOffsetType.InnerEdge, + offsetType = OrbiterOffsetType.OuterEdge, + offset = 32.dp, alignment = Alignment.End, ) { RequestHomeSpaceIconButton( @@ -113,42 +114,40 @@ private fun HomeScreenSpatialMainContent( } Box { SquiggleBackgroundFull() - Box( + Column( modifier = Modifier .fillMaxWidth(0.55f) .fillMaxHeight(1f) .align(Alignment.Center), ) { + AndroidifyTopAppBar( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(220.dp) + .padding(bottom = 16.dp, top = 48.dp), + aboutEnabled = true, + onAboutClicked = onAboutClicked, + ) MainHomeContent( modifier = Modifier - .fillMaxHeight(0.6f) - .align(Alignment.Center), + .align(Alignment.CenterHorizontally) + .fillMaxHeight(0.8f), dancingBotLink = dancingBotLink, ) - Column( + + HomePageButton( modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 16.dp), - ) { - AndroidifyTopAppBar( - modifier = Modifier - .width(220.dp) - .padding(bottom = 16.dp), - aboutEnabled = true, - onAboutClicked = onAboutClicked, - ) - HomePageButton( - modifier = Modifier - .onLayoutRectChanged { - positionButtonClick = it.boundsInWindow.center - } - .height(64.dp) - .width(220.dp), - onClick = { - onClickLetsGo(positionButtonClick) - }, - ) - } + .align(Alignment.CenterHorizontally) + .padding(bottom = 48.dp) + .onLayoutRectChanged { + positionButtonClick = it.boundsInWindow.center + } + .height(64.dp) + .width(220.dp), + onClick = { + onClickLetsGo(positionButtonClick) + }, + ) } } } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt deleted file mode 100644 index 1f22f46a..00000000 --- a/feature/home/src/main/java/com/android/developers/androidify/home/xr/ViewInFullSpaceModeButton.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.home.xr - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import androidx.xr.compose.platform.LocalSession -import androidx.xr.scenecore.scene -import com.android.developers.androidify.home.R -import com.android.developers.androidify.theme.Blue -import com.android.developers.androidify.xr.FullSpaceIcon - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ViewInFullSpaceModeButton( - modifier: Modifier = Modifier, - colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue), -) { - val style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight(700), - letterSpacing = .15f.sp, - ) - val session = LocalSession.current - - Button( - onClick = { - session?.scene?.requestFullSpaceMode() - }, - modifier = modifier, - colors = colors, - ) { - FullSpaceIcon(Modifier.size(ButtonDefaults.LargeIconSize)) - Spacer(Modifier.width(ButtonDefaults.LargeIconSpacing)) - Text( - stringResource(R.string.xr_full_space_button_label), - style = style, - ) - } -} From 80e97bd8f421ac65e078bc8358c9069434e8e763 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 12:11:47 +0200 Subject: [PATCH 11/22] Refactor TopAppBar to accept multiple actions. --- .../androidify/theme/components/TopAppBar.kt | 29 +++++++++---------- .../androidify/creation/CreationScreen.kt | 6 ++-- .../androidify/creation/LoadingScreen.kt | 2 +- .../androidify/home/HomeScreenCompact.kt | 6 ++-- .../androidify/home/HomeScreenMedium.kt | 6 ++-- .../androidify/home/xr/HomeScreenSpatial.kt | 7 +++-- .../customize/CustomizeExportScreen.kt | 5 +++- .../androidify/results/ResultsScreen.kt | 5 +++- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt index 8a1a13dd..2fca24a9 100644 --- a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt +++ b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt @@ -54,10 +54,9 @@ fun AndroidifyTopAppBar( titleText: String = stringResource(R.string.androidify_title), isMediumWindowSize: Boolean = false, backEnabled: Boolean = false, - aboutEnabled: Boolean = true, expandedCenterButtons: @Composable () -> Unit = {}, onBackPressed: () -> Unit = {}, - onAboutClicked: () -> Unit = {}, + actions: (@Composable () -> Unit)? = null, ) { if (isMediumWindowSize) { Box( @@ -92,16 +91,16 @@ fun AndroidifyTopAppBar( expandedCenterButtons() } - if (aboutEnabled) { - AboutButton( - modifier = Modifier - .align(Alignment.CenterEnd) - .background( - color = MaterialTheme.colorScheme.surfaceContainerLowest, - shape = CircleShape, - ), - onAboutClicked = onAboutClicked, - ) + Row( + modifier = Modifier + .align(Alignment.CenterEnd) + .background( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shape = CircleShape, + ), + + ) { + actions?.invoke() } } } else { @@ -121,9 +120,7 @@ fun AndroidifyTopAppBar( } }, actions = { - if (aboutEnabled) { - AboutButton(onAboutClicked = onAboutClicked) - } + actions?.invoke() }, colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest), ) @@ -147,7 +144,7 @@ private fun AndroidifyTitle(text: String) { @OptIn(ExperimentalSharedTransitionApi::class) @Composable -private fun AboutButton(modifier: Modifier = Modifier, onAboutClicked: () -> Unit = {}) { +fun AboutButton(modifier: Modifier = Modifier, onAboutClicked: () -> Unit = {}) { val sharedTransitionScope = LocalSharedTransitionScope.current with(sharedTransitionScope) { IconButton( 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 40767c56..cdbe618b 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 @@ -138,6 +138,7 @@ 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 @@ -289,9 +290,10 @@ fun EditScreen( AndroidifyTopAppBar( backEnabled = true, isMediumWindowSize = isExpanded, - aboutEnabled = true, + actions = { + AboutButton { onAboutPressed() } + }, onBackPressed = onBackPressed, - onAboutClicked = onAboutPressed, expandedCenterButtons = { PromptTypeToolbar( uiState.selectedPromptOption, diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt index 0085e8a2..ccf39eef 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt @@ -87,7 +87,7 @@ fun LoadingScreen( ) { Scaffold( topBar = { - AndroidifyTopAppBar(isMediumWindowSize = isMediumScreen, aboutEnabled = false) + AndroidifyTopAppBar(isMediumWindowSize = isMediumScreen) }, bottomBar = { Box( diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index f5e9d3ba..8a15026a 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.android.developers.androidify.theme.Blue 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.util.PhonePreview @@ -66,8 +67,9 @@ fun HomeScreenCompactPager( horizontalAlignment = Alignment.CenterHorizontally, ) { AndroidifyTopAppBar( - aboutEnabled = true, - onAboutClicked = onAboutClicked, + actions = { + AboutButton { onAboutClicked() } + }, ) HorizontalPager( state = pagerState, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index f4b3e2db..e05ac3c7 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.layout.onLayoutRectChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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.util.LargeScreensPreview @@ -48,8 +49,9 @@ fun HomeScreenMediumContents( } AndroidifyTopAppBar( isMediumWindowSize = true, - aboutEnabled = true, - onAboutClicked = onAboutClicked, + actions = { + AboutButton(onAboutClicked = onAboutClicked) + }, ) Row( 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 90d9f23b..4d7e97c0 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 @@ -51,6 +51,7 @@ import com.android.developers.androidify.home.HomePageButton import com.android.developers.androidify.home.MainHomeContent import com.android.developers.androidify.home.VideoPlayer 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.SquiggleBackgroundFull import com.android.developers.androidify.util.TabletPreview @@ -125,8 +126,10 @@ private fun HomeScreenSpatialMainContent( .align(Alignment.CenterHorizontally) .width(220.dp) .padding(bottom = 16.dp, top = 48.dp), - aboutEnabled = true, - onAboutClicked = onAboutClicked, + + actions = { + AboutButton { onAboutClicked() } + }, ) MainHomeContent( modifier = Modifier 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 0778e228..91e7404f 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 @@ -80,6 +80,7 @@ import com.android.developers.androidify.results.R import com.android.developers.androidify.results.shareImage import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.LocalAnimateBoundsScope +import com.android.developers.androidify.theme.components.AboutButton import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.PrimaryButton import com.android.developers.androidify.theme.components.SecondaryOutlinedButton @@ -159,7 +160,9 @@ private fun CustomizeExportContents( titleText = stringResource(R.string.customize_and_export), isMediumWindowSize = isMediumWindowSize, onBackPressed = onBackPress, - onAboutClicked = onInfoPress, + actions = { + AboutButton { onInfoPress() } + }, ) }, containerColor = MaterialTheme.colorScheme.surface, diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt index a6c2036c..96a685b6 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt @@ -67,6 +67,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.developers.androidify.theme.AndroidifyTheme +import com.android.developers.androidify.theme.components.AboutButton import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.PrimaryButton import com.android.developers.androidify.theme.components.ResultsBackground @@ -109,7 +110,9 @@ fun ResultsScreen( onBackPressed = { onBackPress() }, - onAboutClicked = onAboutPress, + actions = { + AboutButton { onAboutPress() } + }, ) }, modifier = modifier From 3bccd2d70d9da471b8bc54eadaf00ed549fd3bc3 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 12:12:05 +0200 Subject: [PATCH 12/22] Add To Full Space button to TopAppBar --- .../androidify/xr/SpatialUiModes.kt | 18 ++++++++++++++ .../androidify/home/HomeScreenCompact.kt | 5 ++++ .../androidify/home/HomeScreenMedium.kt | 24 +++++++++++++++++++ 3 files changed, 47 insertions(+) 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 f9aee545..3b0b5308 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 @@ -66,6 +66,24 @@ fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { } } +/** Default styling for an TopAppBar Button with a full space button and behavior. */ +@Composable +fun RequestFullSpaceIconButton(modifier: Modifier = Modifier) { + val session = LocalSession.current ?: return + + IconButton( + modifier = modifier, + onClick = { + session.scene.requestFullSpaceMode() + }, + ) { + Icon( + ImageVector.vectorResource(R.drawable.expand_content_24px), + contentDescription = "To Full Space Mode", + ) + } +} + @Composable fun FullSpaceIcon(modifier: Modifier = Modifier) { Icon( diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index 8a15026a..8b226fe9 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -52,6 +52,8 @@ 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.util.PhonePreview +import com.android.developers.androidify.xr.RequestFullSpaceIconButton +import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenCompactPager( @@ -69,6 +71,9 @@ fun HomeScreenCompactPager( AndroidifyTopAppBar( actions = { AboutButton { onAboutClicked() } + if (couldRequestFullSpace()) { + RequestFullSpaceIconButton() + } }, ) HorizontalPager( diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index e05ac3c7..e2289c11 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -35,6 +35,10 @@ 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.util.LargeScreensPreview +import com.android.developers.androidify.xr.RequestFullSpaceIconButton +import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider +import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview +import com.android.developers.androidify.xr.couldRequestFullSpace @Composable fun HomeScreenMediumContents( @@ -51,6 +55,9 @@ fun HomeScreenMediumContents( isMediumWindowSize = true, actions = { AboutButton(onAboutClicked = onAboutClicked) + if (couldRequestFullSpace()) { + RequestFullSpaceIconButton() + } }, ) @@ -107,3 +114,20 @@ private fun HomeScreenLargeScreensPreview() { ) } } + +@ExperimentalMaterial3ExpressiveApi +@XrHomeSpaceMediumPreview +@Composable +private fun HomeScreenLargeScreensHomeSpaceModePreview() { + SupportsFullSpaceModeRequestProvider { + SharedElementContextPreview { + HomeScreenContents( + layoutType = HomeScreenLayoutType.Medium, + onClickLetsGo = { }, + videoLink = "", + dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif", + onAboutClicked = {}, + ) + } + } +} From cea059e1d7fa087dbfcb98518a283d97aef5cc9e Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 16:27:13 +0200 Subject: [PATCH 13/22] Move To Full Space string to :core:xr. --- .../androidify/xr/SpatialUiModes.kt | 12 ++---------- core/xr/src/main/res/values/strings.xml | 19 +++++++++++++++++++ feature/home/src/main/res/values/strings.xml | 1 - 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 core/xr/src/main/res/values/strings.xml 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 3b0b5308..7cb57d33 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 @@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme 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.unit.dp import androidx.xr.compose.platform.SpatialCapabilities @@ -79,16 +80,7 @@ fun RequestFullSpaceIconButton(modifier: Modifier = Modifier) { ) { Icon( ImageVector.vectorResource(R.drawable.expand_content_24px), - contentDescription = "To Full Space Mode", + contentDescription = stringResource(R.string.xr_to_full_space_mode), ) } } - -@Composable -fun FullSpaceIcon(modifier: Modifier = Modifier) { - Icon( - modifier = modifier, - imageVector = ImageVector.vectorResource(R.drawable.expand_content_24px), - contentDescription = "To Full Space Mode", - ) -} diff --git a/core/xr/src/main/res/values/strings.xml b/core/xr/src/main/res/values/strings.xml new file mode 100644 index 00000000..01aec86f --- /dev/null +++ b/core/xr/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + To Full Space Mode + diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index 2bf7e7ff..cdb99eed 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -28,7 +28,6 @@ Customize your own\n \n\nAndroid bot Let\'s Go - To Full Space Check back later. App is under construction. How it works From 91210a2e4f9a4d116918ea7eaff12b3c0966d1e1 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 16:28:54 +0200 Subject: [PATCH 14/22] Refactor To Home Space Mode description to stringResource --- .../java/com/android/developers/androidify/xr/SpatialUiModes.kt | 2 +- core/xr/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 7cb57d33..1222357e 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 @@ -62,7 +62,7 @@ fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) { .fillMaxSize() .padding(8.dp), imageVector = ImageVector.vectorResource(R.drawable.collapse_content_24px), - contentDescription = "To Home Space Mode", + contentDescription = stringResource(R.string.xr_to_home_space_mode), ) } } diff --git a/core/xr/src/main/res/values/strings.xml b/core/xr/src/main/res/values/strings.xml index 01aec86f..9f061fa7 100644 --- a/core/xr/src/main/res/values/strings.xml +++ b/core/xr/src/main/res/values/strings.xml @@ -16,4 +16,5 @@ --> To Full Space Mode + To Home Space Mode From a4d6be7f2889b38044fc42a0a6e27783d6e837aa Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Tue, 2 Sep 2025 17:32:04 +0200 Subject: [PATCH 15/22] Hide To Full Space Mode button when XR is not enabled --- .../com/android/developers/androidify/home/HomeScreen.kt | 7 ++++++- .../developers/androidify/home/HomeScreenCompact.kt | 3 ++- .../android/developers/androidify/home/HomeScreenMedium.kt | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt index 60d9661b..370a849f 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt @@ -41,7 +41,8 @@ fun HomeScreen( onAboutClicked: () -> Unit = {}, ) { val state = homeScreenViewModel.state.collectAsStateWithLifecycle() - val layoutType = calculateLayoutType(state.value.isXrEnabled) + val xrEnabled = state.value.isXrEnabled + val layoutType = calculateLayoutType(xrEnabled) if (!state.value.isAppActive) { AppInactiveScreen() @@ -52,6 +53,7 @@ fun HomeScreen( layoutType, onClickLetsGo, onAboutClicked, + xrEnabled, ) } } @@ -63,6 +65,7 @@ fun HomeScreenContents( layoutType: HomeScreenLayoutType, onClickLetsGo: (IntOffset) -> Unit, onAboutClicked: () -> Unit, + xrEnabled: Boolean = false, ) { when (layoutType) { HomeScreenLayoutType.Compact -> @@ -72,6 +75,7 @@ fun HomeScreenContents( dancingBotLink, onClickLetsGo, onAboutClicked, + xrEnabled, ) } @@ -83,6 +87,7 @@ fun HomeScreenContents( dancingBotLink, onClickLetsGo, onAboutClicked, + xrEnabled, ) } diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt index 8b226fe9..ec64d73e 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt @@ -61,6 +61,7 @@ fun HomeScreenCompactPager( dancingBotLink: String?, onClick: (IntOffset) -> Unit, onAboutClicked: () -> Unit, + xrEnabled: Boolean = false, ) { val pagerState = rememberPagerState(pageCount = { 2 }) @@ -71,7 +72,7 @@ fun HomeScreenCompactPager( AndroidifyTopAppBar( actions = { AboutButton { onAboutClicked() } - if (couldRequestFullSpace()) { + if (xrEnabled && couldRequestFullSpace()) { RequestFullSpaceIconButton() } }, diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt index e2289c11..6d4cecbb 100644 --- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt +++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt @@ -47,6 +47,7 @@ fun HomeScreenMediumContents( dancingBotLink: String?, onClickLetsGo: (IntOffset) -> Unit, onAboutClicked: () -> Unit, + xrEnabled: Boolean = false, ) { var positionButtonClick by remember { mutableStateOf(IntOffset.Zero) @@ -55,7 +56,7 @@ fun HomeScreenMediumContents( isMediumWindowSize = true, actions = { AboutButton(onAboutClicked = onAboutClicked) - if (couldRequestFullSpace()) { + if (xrEnabled && couldRequestFullSpace()) { RequestFullSpaceIconButton() } }, From e100dcea67adaf19bd488256c4cb82630c5eb352 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 21 Aug 2025 17:41:13 +0200 Subject: [PATCH 16/22] Workaround for mainPanelEntity appearing when you have transitioning ApplicationSubspaces --- .../androidify/xr/MainPanelWorkaround.kt | 40 +++++++++++++++++++ .../androidify/home/xr/HomeScreenSpatial.kt | 2 + 2 files changed, 42 insertions(+) create mode 100644 core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt new file mode 100644 index 00000000..a587f465 --- /dev/null +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt @@ -0,0 +1,40 @@ +/* + * 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.runtime.LaunchedEffect +import androidx.xr.compose.platform.LocalSession +import androidx.xr.scenecore.scene +import kotlinx.coroutines.delay + +/* + * A composable that attempts to continually hide the mainPanel. + * + * This is a temporary workaround for b/440325404, that causes the mainPanelEntity when an + * ApplicationSubspace transitions out of the composition due to a race condition when transitioning + * the two hierarchies. + */ +@Composable +fun MainPanelWorkaround() { + val session = LocalSession.current + LaunchedEffect(null) { + while (true) { + delay(100L) + session?.scene?.mainPanelEntity?.setEnabled(false) + } + } +} \ No newline at end of file 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 4d7e97c0..9ecb1570 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 @@ -55,6 +55,7 @@ import com.android.developers.androidify.theme.components.AboutButton import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.SquiggleBackgroundFull import com.android.developers.androidify.util.TabletPreview +import com.android.developers.androidify.xr.MainPanelWorkaround import com.android.developers.androidify.xr.RequestHomeSpaceIconButton @Composable @@ -65,6 +66,7 @@ fun HomeScreenContentsSpatial( onAboutClicked: () -> Unit, ) { Subspace { + MainPanelWorkaround() SpatialPanel( SubspaceModifier .movable() From e1899f6c7b92d7f13d1965b717bcff3ec50a68ce Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 4 Sep 2025 16:13:08 +0200 Subject: [PATCH 17/22] Disable shared transitions in the Spatial layout Adds a shim SharedTransitionScope that does nothing when motion is disabled. --- .../androidify/xr/SharedTransitions.kt | 110 ++++++++++++++++++ .../androidify/home/xr/HomeScreenSpatial.kt | 69 +++++------ 2 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt new file mode 100644 index 00000000..119d0b19 --- /dev/null +++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt @@ -0,0 +1,110 @@ +/* + * 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.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.Placeable +import com.android.developers.androidify.theme.LocalSharedTransitionScope + +/** + * On Android XR in Full Space Mode, spatial panels inflate into a different Window. + * A layout using a SharedTransitionScope will fail to find shared elements across different + * spatial panels. + * + * This composable replaces the LocalSharedTransitionScope with a no-op SharedTransitionScope, + * effectively disabling the shared transitions. + */ +@Composable +fun DisableSharedTransition(content: @Composable (() -> Unit)) { + @OptIn(ExperimentalSharedTransitionApi::class) + SharedTransitionLayout { + CompositionLocalProvider(LocalSharedTransitionScope provides shimSharedTransitionScope(this)) { + content() + } + } +} + +/** A fake implementation of SharedTransitionScope that disables registering shared transitions. */ +@ExperimentalSharedTransitionApi +private fun shimSharedTransitionScope(original: SharedTransitionScope): SharedTransitionScope { + return object : SharedTransitionScope { + override val isTransitionActive: Boolean + get() = false + + override fun Modifier.skipToLookaheadSize(): Modifier = this + + override fun Modifier.renderInSharedTransitionScopeOverlay( + zIndexInOverlay: Float, + renderInOverlay: (SharedTransitionScope) -> Boolean, + ) = this + + override fun Modifier.sharedElement( + sharedContentState: SharedTransitionScope.SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + boundsTransform: BoundsTransform, + placeHolderSize: SharedTransitionScope.PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ) = this + + override fun Modifier.sharedBounds( + sharedContentState: SharedTransitionScope.SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + enter: EnterTransition, + exit: ExitTransition, + boundsTransform: BoundsTransform, + resizeMode: SharedTransitionScope.ResizeMode, + placeHolderSize: SharedTransitionScope.PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ) = this + + override fun Modifier.sharedElementWithCallerManagedVisibility( + sharedContentState: SharedTransitionScope.SharedContentState, + visible: Boolean, + boundsTransform: BoundsTransform, + placeHolderSize: SharedTransitionScope.PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + ) = this + + override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip = + original.OverlayClip(clipShape) + + override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates + get() = with(original) { lookaheadScopeCoordinates } + + override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates { + with(original) { + return toLookaheadCoordinates() + } + } + } +} 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 9ecb1570..38a5c4bd 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 @@ -55,6 +55,7 @@ import com.android.developers.androidify.theme.components.AboutButton import com.android.developers.androidify.theme.components.AndroidifyTopAppBar import com.android.developers.androidify.theme.components.SquiggleBackgroundFull 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 @@ -65,41 +66,43 @@ fun HomeScreenContentsSpatial( onClickLetsGo: (IntOffset) -> Unit, onAboutClicked: () -> Unit, ) { - Subspace { - MainPanelWorkaround() - SpatialPanel( - SubspaceModifier - .movable() - .resizable() - .fillMaxWidth(1f) - .aspectRatio(1.7f), - ) { - Orbiter( - position = ContentEdge.Top, - offsetType = OrbiterOffsetType.OuterEdge, - offset = 32.dp, - alignment = Alignment.End, + DisableSharedTransition { + Subspace { + MainPanelWorkaround() + SpatialPanel( + SubspaceModifier + .movable() + .resizable() + .fillMaxWidth(1f) + .aspectRatio(1.7f), ) { - 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), + Orbiter( + position = ContentEdge.Top, + offsetType = OrbiterOffsetType.OuterEdge, + offset = 32.dp, + alignment = Alignment.End, ) { - VideoPlayer(videoLink) + 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) + } } } } From 791d36fe90eda79e8ce5157b86292d3d84cc8dc7 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 4 Sep 2025 15:43:04 +0200 Subject: [PATCH 18/22] Fix HomeScreenScreenshotTest --- .../developers/androidify/home/HomeScreenScreenshotTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt index 5cdf0e62..49439c55 100644 --- a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt +++ b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt @@ -31,7 +31,7 @@ class HomeScreenScreenshotTest { AndroidifyTheme { SharedElementContextPreview { HomeScreenContents( - isMediumWindowSize = isAtLeastMedium(), + layoutType = calculateLayoutType(enableXr = false), onClickLetsGo = { }, onAboutClicked = {}, videoLink = "", From 3754280f8a3e8c875bfeba6408afd8fdb46b0707 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Mon, 8 Sep 2025 14:52:49 +0200 Subject: [PATCH 19/22] Add xr_feature_enabled in remote_config_defaults.xml --- core/network/src/main/res/xml/remote_config_defaults.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/network/src/main/res/xml/remote_config_defaults.xml b/core/network/src/main/res/xml/remote_config_defaults.xml index 6b6f6c63..eb8daa7e 100644 --- a/core/network/src/main/res/xml/remote_config_defaults.xml +++ b/core/network/src/main/res/xml/remote_config_defaults.xml @@ -23,6 +23,10 @@ background_vibes_feature_enabled false + + xr_feature_enabled + false + bot_background_instruction_prompt Add the input image android bot as the main subject to the result, From 6ed3da4699f4771cabd8483461e89aeb932198e1 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Mon, 8 Sep 2025 19:20:43 +0200 Subject: [PATCH 20/22] Fix HomeScreenTest --- .../com/android/developers/androidify/home/HomeScreenTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt b/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt index 450f2581..5e0b077e 100644 --- a/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt +++ b/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt @@ -48,7 +48,7 @@ class HomeScreenTest { SharedElementContextPreview { HomeScreenContents( - isMediumWindowSize = false, // Provide a default or mock value + layoutType = HomeScreenLayoutType.Compact, // Provide a default or mock value onClickLetsGo = { offset: IntOffset -> // Match the lambda signature wasClicked = true }, @@ -74,7 +74,7 @@ class HomeScreenTest { composeTestRule.setContent { SharedElementContextPreview { HomeScreenContents( - isMediumWindowSize = false, // Ensure compact mode for pager + layoutType = HomeScreenLayoutType.Compact, // Ensure compact mode for pager onClickLetsGo = { }, onAboutClicked = {}, videoLink = "", From 3a712dd9bc6010ca22160923cdf50d6ea13b96a5 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Mon, 8 Sep 2025 19:33:05 +0200 Subject: [PATCH 21/22] Fix incorrect color namespace --- core/xr/src/main/res/drawable/collapse_content_24px.xml | 2 +- core/xr/src/main/res/drawable/expand_content_24px.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/xr/src/main/res/drawable/collapse_content_24px.xml b/core/xr/src/main/res/drawable/collapse_content_24px.xml index f832f639..f11b7743 100644 --- a/core/xr/src/main/res/drawable/collapse_content_24px.xml +++ b/core/xr/src/main/res/drawable/collapse_content_24px.xml @@ -19,7 +19,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:tint="?android:attr/colorControlNormal"> diff --git a/core/xr/src/main/res/drawable/expand_content_24px.xml b/core/xr/src/main/res/drawable/expand_content_24px.xml index ba4da15e..e2a6035c 100644 --- a/core/xr/src/main/res/drawable/expand_content_24px.xml +++ b/core/xr/src/main/res/drawable/expand_content_24px.xml @@ -19,7 +19,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:tint="?android:attr/colorControlNormal"> From a1f5582f950d6c83720deb1ce8244c7710cde2cf Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Mon, 8 Sep 2025 20:00:17 +0200 Subject: [PATCH 22/22] Fix release variant compilation --- app/build.gradle.kts | 4 ++++ gradle/libs.versions.toml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b44b8a5..100540af 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,6 +155,10 @@ dependencies { implementation(projects.core.theme) implementation(projects.core.util) + // library must be compileOnly, see + // https://developer.android.com/develop/xr/jetpack-xr-sdk/getting-started#enable-minification + compileOnly(libs.androidx.xr.extensions) + baselineProfile(projects.benchmark) // Android Instrumented Tests diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4af0ca03..1ab2f4f3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ agp = "8.11.1" bcpkixJdk18on = "1.81" compileSdk = "36" core = "1.5.0" +extensionsXr = "1.0.0" leakcanaryAndroid = "2.14" minSdk = "26" javaVersion = "17" @@ -178,6 +179,7 @@ watchface-push = { group = "androidx.wear.watchfacepush", name="watchfacepush", play-services-base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "playServicesBase" } google-firebase-appcheck-debug = { group = "com.google.firebase", name = "firebase-appcheck-debug"} androidx-xr-compose = { group = "androidx.xr.compose", name="compose", version.ref = "xr-compose"} +androidx-xr-extensions = { module = "com.android.extensions.xr:extensions-xr", version.ref = "extensionsXr" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }