Learn Android XR Fundamentals: Part 1 - Modes and Spatial Panels

1. Before you begin

What you'll learn

  • The unique user experiences that are made possible by the XR form factor.
  • The fundamentals of how apps can be adapted to make the most of running on an Android XR headset by using the composables provided by the Jetpack Compose XR library.
  • How to use the UI elements provided by the Compose XR library.
  • Where to learn more about building apps for Android XR.

What this isn't

What you'll need

What you'll build

In this codelab, you'll enhance a basic single-screen app to provide an immersive user experience through Android XR.

Starting point

Final result

2. Get set up

Get the code

  1. The code for this codelab can be found in the xr-fundamentals directory within the xr-codelabs GitHub repository. To clone the repo, run the following command:
git clone https://github.com/android/xr-codelabs.git
  1. Alternatively, you can download the repository as a ZIP file:

Open the project

  • After starting Android Studio, import the project, selecting just the xr-fundamentals/start directory. The xr-fundamentals/part1 directory contains the solution code, which you can reference at any point if you get stuck or just want to see the full project.

Familiarize yourself with the code

  • After opening the project in Android studio, take some time to look through the starting code.

3. Learn XR concepts: Modes and Spatial Panels

In this codelab, you'll learn about two Android XR concepts: modes and spatial panels. You'll also learn how to apply these concepts to apps running on an Android XR device.

Modes

On Android XR devices, apps run in one of two modes: Home Space mode or Full Space mode.

Home Space mode

d779257a53898d36.jpeg

In Home Space mode, multiple apps run side by side so users can multitask across apps. Android apps can be run in Home Space mode without modification.

Full Space mode

c572cdee69669a23.jpeg

In Full Space mode, one app runs at a time with no space boundaries. All other apps are hidden. Apps must do additional work to enter Full Space mode and make use of the additional capabilities available to them in this mode.

To learn more about these modes, see ​​ Home Space and Full Space modes

Spatial panels

Spatial panels are container elements that serve as the fundamental building blocks of Android XR apps.

When running in Home Space mode, your app will be contained within a single panel for an experience similar to desktop windowing on a large screen Android device.

When running in Full Space mode, you can break out your app's content into one or more panels to provide a more immersive experience.

To learn more about panels, see Spatial panels.

4. Run the app in the Android XR emulator

Before getting started with enhancing the app for Android XR, you can run the app in the Android XR emulator to see how it looks in Home Space mode.

Install the Android XR system image

  1. First, open the SDK Manager in Android Studio and select the SDK Platforms tab if it is not already selected. In the bottom-right corner of the SDK Manager window, make sure that the Show package details box is checked.
  2. Under the Android 14 section, install the Android XR ARM 64 v8a or Android XR Intel x86_64 emulator image. Images can only run on machines with the same architecture (x86/ARM) as themselves.

Create an Android XR virtual device

  1. After opening the Device Manager, select XR under the Category column on the left side of the window. Then, select the XR Device hardware profile from the list and click Next.

7a5f6b9c1766d837.png

  1. On the next page, select the system image you installed previously. Click Next and select any advanced options you want before finally creating the AVD by clicking Finish.
  2. Run the app on the AVD you just created.

7cf6569ef7967d87.png

5. Set up dependencies

Before you can begin adding XR-specific functionality to your app, you'll need to add a dependency on the Jetpack Compose for XR library, androidx.xr.compose:compose, which contains all of the composables you need to build an Android XR optimized experience for your app.

libs.version.toml

[versions]
...
xrCompose = "1.0.0-alpha01"

[libraries]
...
androidx-xr-compose = { group = "androidx.xr.compose", name = "compose", version.ref = "xrCompose" }

build.gradle.kts (Module :app)

dependencies {
    ...
    implementation(libs.androidx.xr.compose)
    ...
}

After updating these files, make sure to do a Gradle sync to ensure the dependencies are downloaded to your project.

6. Enter Full Space mode

To make use of XR features like panels, an app must be running in Full Space mode. There are two ways for an app to enter Full Space mode:

  • Programmatically, such as in response to a user interaction within your app
  • Immediately upon launch by adding a directive to your app manifest.

Enter Full Space mode programmatically

To enter Full Space mode programmatically, you can provide affordances in your UI to let the user control in which mode they'd like to use your app. Additionally, you can also enter Full Space mode when it makes sense within the context of how your app is used. For example, entering Full Space mode when beginning to view video content and exiting when playback is complete.

To keep things simple, this can first be accomplished by adding a button to the top app bar to toggle the mode.

  1. Create a new file, ToggleSpaceModeButton.kt in the com.example.android.xrfundamentals.ui.component package and add the following composables:

ToggleSpaceModeButton.kt

package com.example.android.xrfundamentals.ui.component

import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.xr.compose.platform.LocalSpatialCapabilities
import androidx.xr.compose.platform.LocalSpatialConfiguration
import com.example.android.xrfundamentals.R
import com.example.android.xrfundamentals.ui.theme.XRFundamentalsTheme

@Composable
fun ToggleSpaceModeButton(modifier: Modifier = Modifier) {
    val spatialConfiguration = LocalSpatialConfiguration.current

    if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
        ToggleSpaceModeButton(
            modifier = modifier,
            contentDescription = "Request Home Space mode",
            iconResource = R.drawable.ic_home_space_mode,
            onClick = { spatialConfiguration.requestHomeSpaceMode() }
        )
    } else {
        ToggleSpaceModeButton(
            modifier = modifier,
            contentDescription = "Request Full Space mode",
            iconResource = R.drawable.ic_full_space_mode,
            onClick = { spatialConfiguration.requestFullSpaceMode() }
        )
    }
}

@Composable
fun ToggleSpaceModeButton(
    contentDescription: String,
    @DrawableRes iconResource: Int,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    IconButton(
        modifier = modifier,
        onClick = onClick
    ) {
        Icon(
            painterResource(iconResource),
            contentDescription
        )
    }
}
  1. Add the button as an action in the TopAppBar when the app is running on an XR device

XRFundamentalsTopAppBar.kt

import androidx.xr.compose.ui.platform.LocalHasXrSpatialFeature

...

TopAppBar(
    ...,
    actions = {
        // Only show the mode toggle if the device supports spatial UI
        if (LocalHasXrSpatialFeature.current) {
            ToggleSpaceModeButton()
        }
    }
)

Now run the app.

The app running in Home Space mode when launched.Tap the button in the top right of the panel to switch into Full Space mode.

The app running in Full Space mode. Notice that the system UI for minimizing/closing the app is gone.Tap the button in the top right of the panel to switch back into home space mode.

These snippets includes a few new APIs of note:

  • LocalSpatialConfiguration is a composition local that provides access to the current spatial configuration of the app. Beyond the methods to request changing modes, this includes other information such as the size of the volume containing the app.
  • LocalSpatialCapabilities is a composition local that can be used to determine which spatial capabilities are currently available for an app to use. In addition to the mode (Home or Full Space), this includes capabilities such as spatial audio and 3D content support.
  • LocalHasXrSpatialFeature is a composition local that can be used to determine whether the app is running on a device that supports spatial UI features. Under the hood, it checks whether the device has the android.software.xr.immersive system feature.

Enter Full Space mode at launch

To instruct the OS to start an activity in Full Space mode, you can include a <property> element with the following attributes within the corresponding <activity> element. This is recommended only if it is unlikely that users would like to use another app at the same time that they're using yours.

AndroidManifest.xml

<activity
    android:name=".MainActivity" 
    ... >
    <property
        android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
        android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED" />
</activity>

Now when the app is launched, the user is immediately taken into Full Space mode.

abbf3d27cd2a4532.gif

Before you continue, remove the aforementioned <property> element from the manifest so that the app uses the default behavior of opening in Home Space mode.

7. Split the UI into multiple panels

Now that your app can enter and exit Full Space mode, it's time to make better use of it. One great way to do this is by splitting your app's content into multiple panels to fill the space and (optionally) allow users to move and resize those panels as they see fit.

Embed your app in a subspace

To begin, add a Subspace composable after the Scaffold composable in the XRFundamentalsApp composable. Subspaces are a partition of 3D space within your app where you can build 3D layouts (e.g. adding spatial panels), place 3D models, and add depth to otherwise 2D content.

When running on a non-XR device, the contents of the Subspace composable never enter the Composition. When running on an XR device, the contents only enter the Composition when the app is running in Full Space mode.

XRFundamentalsApp.kt

import androidx.xr.compose.spatial.Subspace

...

HelloAndroidXRTheme {
    Scaffold(...)
    Subspace {
    }
}

Now, run the app:

2d47561a616f4a11.gif

When your app includes a Subspace composable, it will be shown instead of the 2D content. This means that when you click the button to enter Full Space mode, nothing shows up anymore. To fix this, you'll add two spatial panels in the next few steps, one to contain the primary content and another for the secondary content.

Add a panel for the primary content

To display the primary content in Full Space mode, add a SpatialPanel within the Subspace composable.

Since this is the primary panel of the app, you can include the Scaffold within it to keep the controls within the top app bar present. In the next codelab, you'll learn about orbiters, which can be used to spatialize the controls typically contained in app bars such as navigation and context-specific actions.

XRFundamentalsApp.kt

import androidx.xr.compose.subspace.SpatialPanel

...

Subspace {
    SpatialPanel() {
        Scaffold(
            topBar = { XRFundamentalsTopAppBar() }
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                PrimaryCard(
                    modifier = Modifier
                        .padding(16.dp)
                        .verticalScroll(rememberScrollState())
                )
            }
        }
    }
}

Run the app again, and you'll see that the SpatialPanel with the primary content is visible in Full Space mode, but is very small.

89152c1991d422d4.gif

Modify the primary panel

To make the primary panel more usable, you can make it larger by supplying a SubspaceModifier. Subspace modifiers are analogous to modifiers and are used to modify spatial components like panels.

XRFundamentalsApp.kt

import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.height
import androidx.xr.compose.subspace.layout.width
import androidx.compose.ui.unit.dp

...

SpatialPanel(
    modifier = SubspaceModifier
        .width(1024.dp)
        .height(800.dp)
){
    ...
}

Run the app again, and the main panel should take up much more space.

c4f28838e16a3eb8.gif

Add a panel for the secondary content

Now that you've got the app running in Full Space mode and using a panel to display the primary content, it's time to move the secondary content into its own panel. Note the use of a Surface within the spatial panel. Without it, there wouldn't be a background for the secondary cards as spatial panels themselves are transparent (the Scaffold composable handled this in the prior step).

XRFundamentalsApp.kt

Subspace {
    SpatialPanel() { ... }
    SpatialPanel(
        modifier = SubspaceModifier
            .width(340.dp)
            .height(800.dp)
    ) {
        Surface {
            SecondaryCardList(
                modifier = Modifier
                    .padding(16.dp)
                    .verticalScroll(rememberScrollState())
            )
        }
    }
}

Now run the app again. At first glance, it may appear that the second panel isn't showing up, but it actually is – it's just being hidden behind the primary panel.

7db3c3428b64e482.gif

Layout the panels in a row

Just as with 2D content, using rows and columns is helpful for arranging composables side-by-side without overlapping. When working with spatial components like panels, you can use the SpatialRow and SpatialColumn composables to do this.

XRFundamentalsApp.kt

import androidx.xr.compose.subspace.SpatialRow

...

Subspace {
    SpatialRow(
        curveRadius = 825.dp
    ) {
        SpatialPanel(...) { ... }
        SpatialPanel(...) { ... }
    }
}

Run the app one more time and you should see that the panels are laid out in a row, one after the other. Additionally, because of the curveRadius provided to the SpatialRow, the panels curve around the user instead of staying in the same plane, providing a more encompassing experience.

7455811775088baf.gif

Make a panel resizable

To give users control over the appearance of your app, you can make panels resizable by using the resizable subspace modifier.

By default, resizable panels can be resized down to zero or expanded indefinitely, so you'll probably want to take the time to set appropriate minimumSize and maximumSize parameters based on the content they'll contain.

See the reference documentation for more details on all of the parameters the resizable modifier supports.

XRFundamentalsApp.kt

import androidx.xr.compose.subspace.layout.resizable

...

SpatialPanel(
    modifier = SubspaceModifier
        ...
        .resizable(true)
)

2ff2db33032fd251.gif

Make a panel movable

Similarly, you can make panels moveable by using the movable subspace modifier.

XRFundamentalsApp.kt

import androidx.xr.compose.subspace.layout.movable

...

SpatialPanel(
    modifier = SubspaceModifier
        ...
        .movable(true)
)

12b6166645ea1be.gif

See the reference documentation for more details on all of the parameters the movable modifier supports.

8. Congratulations

To continue learning about how to make the most of XR, check out the following resources and exercises. Also, apply to participate in the XR bootcamp!

Further reading

Challenges

  • Use the additional parameters available for the resizable and movable subspace modifiers.
  • Add additional panels.
  • Use other spatial components like a spatial dialog

Reference docs