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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AdaptiveJetStream/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ android-test-plugin = "8.13.0"
androidx-baselineprofile = "1.4.1"
benchmark-macro-junit4 = "1.4.1"
coil-compose = "2.7.0"
compose-bom = "2025.09.01"
compose-bom = "2026.01.01"
concurrent-futures-ktx = "1.3.0"
tv-material = "1.0.1"
core-ktx = "1.17.0"
Expand All @@ -29,7 +29,7 @@ rules = "1.7.0"
window = "1.5.0"
xr = "1.0.0-alpha07"
xr-material3 = "1.0.0-alpha11"
screenshot = "0.0.1-alpha12"
screenshot = "0.0.1-alpha13"
ui-tooling-preview = "1.10.0"
ui-tooling = "1.10.0"
robolectric = "4.16"
Expand Down
4 changes: 2 additions & 2 deletions AdaptiveJetStream/jetstream/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,6 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)

"screenshotTestImplementation"(libs.screenshot.validation.api)
"screenshotTestImplementation"(libs.androidx.compose.ui.tooling)
screenshotTestImplementation(libs.screenshot.validation.api)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class MainActivity : ComponentActivity() {
LocalContentColor provides MaterialTheme.colorScheme.onSurface
) {
App(
// TODO: Figure out why this is being used instead of a BackHandler
onActivityBackPressed = onBackPressedDispatcher::onBackPressed,
modifier = Modifier
.safeDrawingPadding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,30 @@

package com.google.jetstream.presentation

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import com.google.jetstream.presentation.app.AppState
import com.google.jetstream.presentation.app.NavigationComponentType
import com.google.jetstream.presentation.app.NavigationTree
import com.google.jetstream.presentation.app.rememberAppState
import com.google.jetstream.presentation.app.rememberNavigationComponentType
import com.google.jetstream.presentation.app.updateTopBarVisibility
import com.google.jetstream.presentation.app.withNavigationSuiteScaffold.AdaptiveAppNavigationItems
import com.google.jetstream.presentation.app.withNavigationSuiteScaffold.AppWithNavigationSuiteScaffold
import com.google.jetstream.presentation.app.withNavigationSuiteScaffold.EnableProminentMovieListOverride
import com.google.jetstream.presentation.app.withNavigationSuiteScaffold.RequestFullSpaceModeItem
import com.google.jetstream.presentation.app.withSpatialNavigation.AppWithSpatialNavigation
import com.google.jetstream.presentation.app.withTopBarNavigation.AppWithTopBarNavigation
import com.google.jetstream.presentation.components.KeyboardShortcut
import com.google.jetstream.presentation.components.ModifierKey
import com.google.jetstream.presentation.components.feature.hasXrSpatialFeature
import com.google.jetstream.presentation.components.handleKeyboardShortcuts
import com.google.jetstream.presentation.screens.Screens

Expand All @@ -45,6 +52,8 @@ fun App(
val navController = rememberNavController()
val navigationComponentType = rememberNavigationComponentType()

// TODO: This could be moved into a separate function, and the keyboard shortcuts could be
// associated with each screen directly (maybe through an optional interface)
val keyboardShortcuts = remember {
listOf(
KeyboardShortcut(
Expand Down Expand Up @@ -144,33 +153,103 @@ fun App(
NavigationComponentType.NavigationSuiteScaffold -> {
EnableProminentMovieListOverride {
Surface {

val hasXrSpatialFeature = hasXrSpatialFeature()
val screensInGlobalNavigation = remember {
Screens.entries.filter { it.isMainNavigation }
}

val topBarPaddingTop = remember(hasXrSpatialFeature) {
if (hasXrSpatialFeature) {
32.dp
} else {
0.dp
}
}

AppWithNavigationSuiteScaffold(
appState = appState,
navController = navController,
selectedScreen = appState.selectedScreen,
isNavigationVisible = appState.isNavigationVisible,
isTopBarVisible = appState.isTopBarVisible,
topBarPaddingTop = topBarPaddingTop,
onShowScreen = { screen ->
navController.navigate(screen())
},
onFocusChanged = { appState.updateTopBarFocusState(it) },
modifier = modifier.handleKeyboardShortcuts(keyboardShortcuts),
)
navigationItems = {
AdaptiveAppNavigationItems(
currentScreen = appState.selectedScreen,
screens = screensInGlobalNavigation,
onSelectScreen = { screen ->
if (screen != appState.selectedScreen) {
navController.navigate(screen())
}
},
)
if (hasXrSpatialFeature) {
RequestFullSpaceModeItem()
}
},
) { padding ->
NavigationTree(
navController = navController,
isTopBarVisible = appState.isTopBarVisible,
modifier = modifier.padding(padding),
onScroll = { updateTopBarVisibility(appState, it) }
)
}
}
}
}

NavigationComponentType.TopBar -> {
Surface {
AppWithTopBarNavigation(
appState = appState,
selectedScreen = appState.selectedScreen,
isNavigationVisible = appState.isNavigationVisible,
isTopBarVisible = appState.isNavigationVisible && appState.isTopBarVisible,
isTopBarFocussed = appState.isTopBarFocused,
onTopBarFocusChanged = { hasFocus ->
appState.updateTopBarFocusState(hasFocus)
},
onTopBarVisible = { appState.showTopBar() },
onActivityBackPressed = onActivityBackPressed,
navController = navController,
showScreen = { screen ->
navController.navigate(screen())
},
modifier = modifier.handleKeyboardShortcuts(keyboardShortcuts),
)
) {
NavigationTree(
navController = navController,
isTopBarVisible = appState.isTopBarVisible,
onScroll = { updateTopBarVisibility(appState, it) }
)
}
}
}

NavigationComponentType.Spatial -> {
EnableProminentMovieListOverride {
AppWithSpatialNavigation(
appState = appState,
navController = navController,
modifier = modifier.handleKeyboardShortcuts(keyboardShortcuts),
)
selectedScreen = appState.selectedScreen,
isNavigationVisible = appState.isNavigationVisible,
isTopBarVisible = appState.isTopBarVisible,
onShowScreen = { screen ->
navController.navigate(screen())
},
onTopBarFocusChanged = { appState.updateTopBarFocusState(it) },
containerColor = appState.selectedScreen.xrContainerColor(),
) { paddingValues ->
NavigationTree(
navController = navController,
isTopBarVisible = appState.isTopBarVisible,
modifier = Modifier
.handleKeyboardShortcuts(keyboardShortcuts)
.padding(paddingValues),
onScroll = { updateTopBarVisibility(appState, it) }
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ class AppState internal constructor(
private set

var isTopBarFocused by mutableStateOf(false)
private set

var isNavigationVisible by mutableStateOf(true)
private set

private var navigationComponentType
by mutableStateOf(NavigationComponentType.NavigationSuiteScaffold)
Expand Down Expand Up @@ -69,18 +71,11 @@ class AppState internal constructor(
}

private fun updateNavigationVisibility() {
isNavigationVisible = when (navigationComponentType) {
NavigationComponentType.TopBar -> {
selectedScreen.navigationVisibility.isVisibleInTopBar
}
else -> {
selectedScreen.navigationVisibility.isVisibleInNavigationSuite
}
}
isNavigationVisible = selectedScreen.shouldShowNavigation(navigationComponentType)
}

private fun updateSelectedScreen(destination: String) {
val screen = Screens.tryFrom(destination) ?: Screens.Home
val screen = Screens.tryFrom(destination) ?: error("Could not find screen from $destination")
updateSelectedScreen(screen)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,79 +20,60 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.jetstream.presentation.app.AppState
import com.google.jetstream.presentation.app.NavigationTree
import com.google.jetstream.presentation.app.updateTopBarVisibility
import com.google.jetstream.presentation.components.feature.hasXrSpatialFeature
import com.google.jetstream.presentation.screens.Screens

@Composable
fun AppWithNavigationSuiteScaffold(
appState: AppState,
navController: NavHostController,
selectedScreen: Screens,
isNavigationVisible: Boolean,
isTopBarVisible: Boolean,
topBarPaddingTop: Dp,
onFocusChanged: (Boolean) -> Unit,
onShowScreen: (Screens) -> Unit,
navigationItems: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable (PaddingValues) -> Unit,
) {
val navigationSuiteScaffoldState = rememberNavigationSuiteScaffoldState()
val screensInGlobalNavigation = remember {
Screens.entries.filter { it.isMainNavigation }
}

val hasXrSpatialFeature = hasXrSpatialFeature()

LaunchedEffect(appState.isNavigationVisible) {
if (appState.isNavigationVisible) {
LaunchedEffect(isNavigationVisible) {
if (isNavigationVisible) {
navigationSuiteScaffoldState.show()
} else {
navigationSuiteScaffoldState.hide()
}
}

val topBarPaddingTop = remember(hasXrSpatialFeature) {
if (hasXrSpatialFeature) {
32.dp
} else {
0.dp
}
}

NavigationSuiteScaffold(
modifier = modifier,
state = navigationSuiteScaffoldState,
navigationItemVerticalArrangement = Arrangement.Center,
navigationItems = {
AdaptiveAppNavigationItems(
currentScreen = appState.selectedScreen,
screens = screensInGlobalNavigation
) {
if (it != appState.selectedScreen) {
navController.navigate(it())
}
}
RequestFullSpaceModeItem(hasXrSpatialFeature = hasXrSpatialFeature)
}
navigationItems = navigationItems
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
AnimatedVisibility(
visible = appState.isNavigationVisible,
visible = isNavigationVisible,
enter = slideInVertically(),
exit = slideOutVertically()
) {
TopBar(
appState = appState,
navController = navController,
selectedScreen = selectedScreen,
isTopBarVisible = isTopBarVisible,
onFocusChanged = { onFocusChanged(it) },
onShowScreen = { screen -> onShowScreen(screen) },
modifier = Modifier.padding(
start = 24.dp,
end = 24.dp,
Expand All @@ -101,13 +82,7 @@ fun AppWithNavigationSuiteScaffold(
)
}
}
) { padding ->
NavigationTree(
navController = navController,
isTopBarVisible = appState.isTopBarVisible,
modifier = modifier.padding(padding),
onScroll = { updateTopBarVisibility(appState, it) }
)
}
) { paddingValues -> content(paddingValues) }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ fun TopAppBar(
) {
val (avatar, search) = remember { FocusRequester.createRefs() }

/**
* When the row becomes focussed, automatically focus either the search or profile
* composables depending on the current screen.
*
* TODO: This could be refactored to take a list of
* navigation items rather than a hardcoded list
*/
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
Expand Down Expand Up @@ -72,7 +79,9 @@ fun TopAppBar(
Spacer(modifier.weight(1f))
SearchButton(
modifier = Modifier.focusRequester(search),
onClick = { showScreen(Screens.Search) }
onClick = {
showScreen(Screens.Search)
}
)
UserAvatar(
modifier = Modifier.focusRequester(avatar),
Expand All @@ -81,3 +90,5 @@ fun TopAppBar(
)
}
}


Loading