Develop UI with Jetpack Compose for XR

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 use setContent. 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.

Example of a spatial UI 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 inside Subspace or setSubspaceContent. Calling them outside of a subspace will throw an exception.
  • Allow the user to resize or move the panel by adding .movable or .resizable SubspaceModifiers.
  • 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.

Example of 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 spatial layout using SpatialRow, SpatialColumn, SpatialBox, and SpatialLayoutSpacer.

Example of multiple spatial panels in a spatial layout

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

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.

Example of a 3D object in a layout

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

SpatialDialog

Panel will push slightly back in z-depth to display an elevated dialog

Falls back to 2D Dialog.

SpatialPopUp

Panel will push slightly back in z-depth to display an elevated popup

Falls back to a 2D PopUp.

SpatialElevation

SpatialElevationLevel can be set to add elevation.

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

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.

See also