Skip to content

Commit 62d82be

Browse files
committed
Implemented XR NavigationRail
1 parent 0f1c468 commit 62d82be

File tree

4 files changed

+134
-19
lines changed

4 files changed

+134
-19
lines changed

app-android/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ dependencies {
124124
implementation(compose.materialIconsExtended)
125125
implementation(libs.material3)
126126
debugImplementation(compose.uiTooling)
127+
implementation(libs.haze)
127128

128129
implementation(libs.androidxActivityCompose)
129130

app-android/src/main/kotlin/io/github/droidkaigi/confsched/MainActivity.kt

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,30 @@ import androidx.activity.ComponentActivity
77
import androidx.activity.SystemBarStyle
88
import androidx.activity.compose.setContent
99
import androidx.activity.enableEdgeToEdge
10+
import androidx.compose.foundation.background
11+
import androidx.compose.foundation.shape.CornerSize
12+
import androidx.compose.foundation.shape.RoundedCornerShape
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.CompositionLocalProvider
16+
import androidx.compose.runtime.ExperimentalComposeApi
17+
import androidx.compose.ui.Alignment
18+
import androidx.compose.ui.Modifier
1019
import androidx.xr.compose.material3.EnableXrComponentOverrides
1120
import androidx.xr.compose.material3.ExperimentalMaterial3XrApi
21+
import androidx.xr.compose.platform.LocalSpatialCapabilities
22+
import androidx.xr.compose.spatial.ContentEdge
23+
import androidx.xr.compose.spatial.Orbiter
24+
import androidx.xr.compose.spatial.OrbiterOffsetType
25+
import androidx.xr.compose.spatial.Subspace
26+
import androidx.xr.compose.subspace.SpatialPanel
27+
import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
28+
import androidx.xr.compose.subspace.layout.SubspaceModifier
29+
import androidx.xr.compose.subspace.layout.fillMaxSize
30+
import io.github.droidkaigi.confsched.component.GlassLikeNavigationRailBar
31+
import io.github.droidkaigi.confsched.component.LocalNavigationRailOverride
32+
import io.github.droidkaigi.confsched.component.NavigationRailOverride
33+
import io.github.droidkaigi.confsched.component.NavigationRailOverrideScope
1234

1335
class MainActivity : ComponentActivity() {
1436
@OptIn(ExperimentalMaterial3XrApi::class)
@@ -25,7 +47,60 @@ class MainActivity : ComponentActivity() {
2547
with(appGraph) {
2648
setContent {
2749
EnableXrComponentOverrides {
28-
KaigiApp()
50+
EnableXrKaigiComponentOverrides {
51+
KaigiApp()
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
@Composable
60+
private fun EnableXrKaigiComponentOverrides(
61+
content: @Composable () -> Unit,
62+
) {
63+
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
64+
CompositionLocalProvider(
65+
LocalNavigationRailOverride provides XrNavigationRailOverride,
66+
content = content,
67+
)
68+
} else {
69+
content()
70+
}
71+
}
72+
73+
private object XrNavigationRailOverride : NavigationRailOverride {
74+
@OptIn(ExperimentalComposeApi::class)
75+
@Composable
76+
override fun NavigationRailOverrideScope.NavigationRail() {
77+
val cornerSize = CornerSize(percent = 50)
78+
/**
79+
* SpatialPanel should not be held here but should be implemented on the screen,
80+
* but because it causes crashes when combined with Navigation3, SpatialPanel is placed here.
81+
*/
82+
Subspace {
83+
SpatialPanel(
84+
modifier = SubspaceModifier.fillMaxSize(),
85+
) {
86+
// Orbiter is only displayed in SpatialPanel
87+
Orbiter(
88+
position = ContentEdge.Vertical.Start,
89+
offsetType = OrbiterOffsetType.OuterEdge,
90+
alignment = Alignment.CenterVertically,
91+
shape = SpatialRoundedCornerShape(cornerSize),
92+
) {
93+
GlassLikeNavigationRailBar(
94+
hazeState = hazeState,
95+
currentTab = currentTab,
96+
onTabSelected = onTabSelected,
97+
animatedSelectedTabIndex = animatedSelectedTabIndex,
98+
animatedColor = animatedColor,
99+
modifier = Modifier.background(
100+
color = MaterialTheme.colorScheme.surface,
101+
shape = RoundedCornerShape(cornerSize),
102+
),
103+
)
29104
}
30105
}
31106
}

app-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/component/GlassLikeNavigationRailBar.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import androidx.compose.foundation.border
55
import androidx.compose.foundation.layout.Box
66
import androidx.compose.foundation.layout.Column
77
import androidx.compose.foundation.layout.PaddingValues
8+
import androidx.compose.foundation.layout.WindowInsets
9+
import androidx.compose.foundation.layout.WindowInsetsSides
810
import androidx.compose.foundation.layout.fillMaxHeight
911
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.only
1013
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.safeDrawing
1115
import androidx.compose.foundation.layout.size
1216
import androidx.compose.foundation.layout.width
17+
import androidx.compose.foundation.layout.windowInsetsPadding
1318
import androidx.compose.foundation.selection.selectableGroup
1419
import androidx.compose.foundation.shape.CircleShape
1520
import androidx.compose.material3.MaterialTheme
1621
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.ProvidableCompositionLocal
23+
import androidx.compose.runtime.compositionLocalOf
1724
import androidx.compose.ui.Alignment
1825
import androidx.compose.ui.Modifier
1926
import androidx.compose.ui.draw.BlurredEdgeTreatment
@@ -176,6 +183,45 @@ private fun SelectedTabSideLineIndicator(
176183
}
177184
}
178185

186+
val LocalNavigationRailOverride: ProvidableCompositionLocal<NavigationRailOverride> =
187+
compositionLocalOf {
188+
DefaultNavigationRailOverride
189+
}
190+
191+
private object DefaultNavigationRailOverride : NavigationRailOverride {
192+
@Composable
193+
override fun NavigationRailOverrideScope.NavigationRail() {
194+
GlassLikeNavigationRailBar(
195+
hazeState = hazeState,
196+
currentTab = currentTab,
197+
onTabSelected = onTabSelected,
198+
animatedSelectedTabIndex = animatedSelectedTabIndex,
199+
animatedColor = animatedColor,
200+
modifier = Modifier
201+
.padding(start = 16.dp, end = 8.dp)
202+
.windowInsetsPadding(
203+
WindowInsets.safeDrawing.only(
204+
WindowInsetsSides.Vertical + WindowInsetsSides.Start,
205+
),
206+
),
207+
)
208+
}
209+
}
210+
211+
interface NavigationRailOverride {
212+
@Composable
213+
fun NavigationRailOverrideScope.NavigationRail()
214+
}
215+
216+
class NavigationRailOverrideScope internal constructor(
217+
val hazeState: HazeState,
218+
val currentTab: MainScreenTab,
219+
val onTabSelected: (MainScreenTab) -> Unit,
220+
val animatedSelectedTabIndex: Float,
221+
val animatedColor: Color,
222+
val modifier: Modifier,
223+
)
224+
179225
@Preview
180226
@Composable
181227
private fun GlassLikeNavigationRailBarPreview() {

app-shared/src/commonMain/kotlin/io/github/droidkaigi/confsched/component/KaigiNavigationScaffold.kt

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,17 @@ private fun KaigiNavigationScaffold(
9292
) {
9393
Row(modifier = modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
9494
AnimatedVisibility(currentTab != null && navigationBarType == NavigationBarType.NavigationRail) {
95-
/**
96-
* TODO: Orbiter support for NavigationRail in XR.
97-
* Also, as of September 9, 2025, there is a problem where Orbiter cannot be displayed on the XR emulator.
98-
*/
99-
GlassLikeNavigationRailBar(
100-
currentTab = currentTab ?: MainScreenTab.Timetable,
101-
hazeState = hazeState,
102-
onTabSelected = onTabSelected,
103-
animatedSelectedTabIndex = animatedSelectedTabIndex,
104-
animatedColor = animatedColor,
105-
modifier = Modifier
106-
.padding(start = 16.dp, end = 8.dp)
107-
.windowInsetsPadding(
108-
WindowInsets.safeDrawing.only(
109-
WindowInsetsSides.Vertical + WindowInsetsSides.Start,
110-
),
111-
),
112-
)
95+
with(LocalNavigationRailOverride.current) {
96+
NavigationRailOverrideScope(
97+
currentTab = currentTab ?: MainScreenTab.Timetable,
98+
hazeState = hazeState,
99+
onTabSelected = onTabSelected,
100+
animatedSelectedTabIndex = animatedSelectedTabIndex,
101+
animatedColor = animatedColor,
102+
modifier = Modifier,
103+
)
104+
.NavigationRail()
105+
}
113106
}
114107
Scaffold(
115108
bottomBar = {

0 commit comments

Comments
 (0)