With Jetpack Compose for XR, you can declaratively build your spatial UI and layout using familiar Compose concepts such as rows and columns. This lets you extend your existing Android UI into 3D space or build entirely new immersive 3D applications.
If you are spatializing an existing Android Views-based app, you have several development options. You can use interoperability APIs, use Compose and Views together, or work directly with the SceneCore library. See our guide to working with views for more details.
About subspaces and spatialized components
When you're writing your app for Android XR, it's important to understand the concepts of subspace and spatialized components.
About subspace
When developing for Android XR, you'll need to add a subspace to your app or layout. A subspace is a partition of 3D space within your app where you can place 3D content, build 3D layouts, and add depth to otherwise 2D content. A subspace is rendered only when spatialization is enabled. In Home Space or on non-XR devices, any code within that subspace is ignored.
There are two ways to create a subspace:
setSubspaceContent
: This function creates an app level subspace. This can be called in your MainActivity the same way you usesetContent
. An app level subspace is unlimited in height, width, and depth, essentially providing an infinite canvas for spatial content.Subspace
: This composable can be placed anywhere within your app's UI hierarchy, allowing you to maintain layouts for 2D and spatial UI without losing context between files. This makes it easier to share things like existing app architecture between XR and other form factors without needing to hoist state through your whole UI tree or re-architect your app.
For more information, read about adding a subspace to your app.
About spatialized components
Subspace composables: These components can only be rendered in a subspace.
They must be enclosed within Subspace
or setSubspaceContent
before being
placed within a 2D layout. A SubspaceModifier
lets you add
attributes like depth, offset, and positioning to your subspace composables.
- Note about subspace modifiers: Pay close attention to the order of
SubspaceModifier
APIs.- Offset must occur first in a modifier chain
- Movable and resizable must occur last
- Rotate must be applied before scale
Other spatialized components don't require being called inside a subspace. They consist of conventional 2D elements wrapped within a spatial container. These elements can be used within 2D or 3D layouts if defined for both. When spatialization is not enabled, their spatialized features will be ignored and they will fall back to their 2D counterparts.
Create a spatial panel
A SpatialPanel
is a subspace composable that lets you display app content–for
example, you could display video playback, still images, or any other content in
a spatial panel.
You can use SubspaceModifier
to change the size, behavior, and positioning of
the spatial panel, as shown in the following example.
Subspace {
SpatialPanel(
SubspaceModifier
.height(824.dp)
.width(1400.dp)
.movable()
.resizable()
) {
SpatialPanelContent()
}
}
// 2D content placed within the spatial panel
@Composable
fun SpatialPanelContent(){
Box(
Modifier
.background(color = Color.Black)
.height(500.dp)
.width(500.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Spatial Panel",
color = Color.White,
fontSize = 25.sp
)
}
}
Key points about the code
- Note about subspace modifiers: Pay close attention to the order of
SubspaceModifier
APIs.- The offset must occur first in a modifier chain.
- Movable and resizable modifiers must occur last.
- Rotation must be applied before scale.
- Because
SpatialPanel
APIs are subspace composables, you must call them insideSubspace
orsetSubspaceContent
. Calling them outside of a subspace will throw an exception. - Allow the user to resize or move the panel by adding
.movable
or.resizable
SubspaceModifier
s. - See our spatial panel design guidance for details on sizing and positioning. See our reference documentation for more specifics on code implementation.
Create an orbiter
An orbiter is a spatial UI component. It's designed to be attached to a corresponding spatial panel, and contains navigation and contextual action items related to that spatial panel. For example, if you've created a spatial panel to display video content, you could add video playback controls inside an orbiter.
As shown in the following example, call an orbiter inside a SpatialPanel
to
wrap user controls like navigation. Doing so extracts them from your 2D layout
and attaches them to the spatial panel according to your configuration.
setContent {
Subspace {
SpatialPanel(
SubspaceModifier
.height(824.dp)
.width(1400.dp)
.movable()
.resizable()
) {
SpatialPanelContent()
OrbiterExample()
}
}
}
//2D content inside Orbiter
@Composable
fun OrbiterExample() {
Orbiter(
position = OrbiterEdge.Bottom,
offset = 96.dp,
alignment = Alignment.CenterHorizontally
) {
Surface(Modifier.clip(CircleShape)) {
Row(
Modifier
.background(color = Color.Black)
.height(100.dp)
.width(600.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Orbiter",
color = Color.White,
fontSize = 50.sp
)
}
}
}
}
Key points about the code
- Note about Subspace Modifiers: Pay close attention to the order of
SubspaceModifier
APIs.- Offset must occur first in a modifier chain
- Movable and resizable must occur last
- Rotate must be applied before scale
- Because orbiters are spatial UI components, the code can be reused in 2D or 3D layouts. In a 2D layout, your app renders only the content inside the orbiter and ignores the orbiter itself.
- Check out our design guidance for more information on how to use and design orbiters.
Add multiple spatial panels to a spatial layout
You can create multiple spatial panels and place them within a
SpatialLayout
using SpatialRow
,
SpatialColumn
, SpatialBox
, and
SpatialLayoutSpacer
.
The following code example shows how to do this.
Subspace {
SpatialRow {
SpatialColumn {
SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
SpatialPanelContent("Top Left")
}
SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) {
SpatialPanelContent("Middle Left")
}
SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
SpatialPanelContent("Bottom Left")
}
}
SpatialColumn {
SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
SpatialPanelContent("Top Right")
}
SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) {
SpatialPanelContent("Middle Right")
}
SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
SpatialPanelContent("Bottom Right")
}
}
}
}
@Composable
fun SpatialPanelContent(text: String) {
Column(
Modifier
.background(color = Color.Black)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Panel",
color = Color.White,
fontSize = 15.sp
)
Text(
text = text,
color = Color.White,
fontSize = 25.sp,
fontWeight = FontWeight.Bold
)
}
}
Key points about the code
SpatialRow
,SpatialColumn
,SpatialBox
, andSpatialLayoutSpacer
are all subspace composables and must be placed within a subspace.- Use
SubspaceModifier
to customize your layout. - For layouts with multiple panels in a row, we recommend setting a curve radius
of 825dp using a
SubspaceModifier
so the panels will surround your user. See our design guidance for details.
Use a volume to place a 3D object in your layout
To place a 3D object in your layout, you'll need to use a subspace composable called a volume. Here's an example of how to do that.
Subspace {
SpatialPanel(
SubspaceModifier.height(1500.dp).width(1500.dp)
.resizable().movable()
) {
ObjectInAVolume(true)
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Welcome",
fontSize = 50.sp,
)
}
}
}
}
@Composable
fun ObjectInAVolume(show3DObject: Boolean) {
val xrCoreSession = checkNotNull(LocalSession.current)
val scope = rememberCoroutineScope()
if (show3DObject) {
Subspace {
Volume(
modifier = SubspaceModifier
.offset(volumeXOffset, volumeYOffset, volumeZOffset) //
Relative position
.scale(1.2f) // Scale to 120% of the size
) { parent ->
scope.launch {
// Load your 3D Object here
}
}
}
}
}
Key points about the code
- Note about Subspace Modifiers: Pay close attention to the order of
SubspaceModifier
APIs.- Offset must occur first in a modifier chain
- Movable and resizable must occur last
- Rotate must be applied before scale
- See Adding 3D content to better understand how to load 3D content within a volume.
Add other spatial UI components
Spatial UI components can be placed anywhere in your application's UI hierarchy. These elements can be reused in your 2D UI, and their spatial attributes will only be visible when spatial capabilities are enabled. This lets you add elevation to menus, dialogs, and other components without the need to write your code twice. See the following examples of spatial UI to better understand how to use these elements.
UI Component |
When spatialization is enabled |
In 2D environment |
---|---|---|
|
Panel will push slightly back in z-depth to display an elevated dialog |
Falls back to 2D |
|
Panel will push slightly back in z-depth to display an elevated popup |
Falls back to a 2D |
|
|
Shows without spatial elevation. |
SpatialDialog
This is an example of a dialog that opens after a short delay. When
SpatialDialog
is used, the dialog appears at the same z-depth as the spatial
panel, and the panel is pushed back by 125dp when spatialization is enabled.
SpatialDialog
can still be used when spatialization isn't enabled as well, and
it falls back to its 2D counterpart: Dialog
.
@Composable
fun DelayedDialog() {
var showDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
Handler(Looper.getMainLooper()).postDelayed({
showDialog = true
}, 3000)
}
if (showDialog) {
SpatialDialog (
onDismissRequest = { showDialog = false },
SpatialDialogProperties(
dismissOnBackPress = true)
){
Box(Modifier
.height(150.dp)
.width(150.dp)
) {
Button(onClick = { showDialog = false }) {
Text("OK")
}
}
}
}
}
Key points about the code
- This is an example of
SpatialDialog
. UsingSpatialPopUp
andSpatialElevation
will be very similar. See our API reference for more detail.
Create custom panels and layouts
To create custom panels that are not supported by Compose for XR, you can work
directly with PanelEntities
and the scene graph using the
SceneCore
APIs.