Subspace

Functions summary

Unit

Create a 3D area that the app can render spatial content into.

Unit

Creates a 3D space that positions spatial UI content relative to a FollowTarget.

Functions

@Composable
@ComposableOpenTarget(index = -1)
fun Subspace(
    modifier: SubspaceModifier = SubspaceModifier,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Create a 3D area that the app can render spatial content into.

Subspace creates a Compose for XR Spatial UI hierarchy (3D Scene Graph) in your application's regular Compose UI tree. In this Subspace, you can use @SubspaceComposable-annotated composable functions to create 3D UI elements.

Each call to Subspace creates a new, independent Spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent Subspace it is nested within. Its position and scale are solely decided by the system's recommended position and scale. To create an embedded Subspace within a SpatialPanel, Orbiter, SpatialPopup, etc., use the PlanarEmbeddedSubspace instead.

By default, this Subspace is automatically bounded by the system's recommended content box. The recommended content box is a fixed 3D volume that uses the device's field of view (FOV) angles, the system's default launch distance from the user, and the default scale of the system to calculate a box that is sized to encompass the user's primary field of view.

This size does not change throughout the lifecycle of the application, and it does not have an independent concept of pose. When used by Compose for XR to set the constraints of a Subspace, its effective pose is the root of the Subspace.

Using this default is the suggested way to create responsive spatial layouts that look great without hardcoding dimensions. SubspaceModifiers like SubspaceModifier.fillMaxSize will expand to fill this recommended box. This default can be overridden by applying a custom size-based modifier.

This composable is a no-op and does not render anything in non-XR environments (i.e., Phone and Tablet).

On XR devices that cannot currently render spatial UI, the Subspace will still create its scene and all of its internal state, even though nothing may be rendered. This is to ensure that the state is maintained consistently in the spatial scene and to allow preparation for the support of rendering spatial UI. State should be maintained by the compose runtime and events that cause the compose runtime to lose state (app process killed or configuration change) will also cause the Subspace to lose its state.

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.fillMaxSize
import androidx.xr.compose.subspace.layout.requiredSizeIn

@Composable
fun AppContent() {
    // By default, Subspace is automatically bounded by the system's recommended content box.
    // SubspaceModifiers like fillMaxSize() will expand to fill this recommended box.
    Subspace {
        SpatialPanel(SubspaceModifier.fillMaxSize()) {
            Text("Panel filling the default recommended content box")
        }
    }

    // To escape the default recommended content box constraints and define a custom
    // bounded or unbounded Subspace, apply the requiredSizeIn modifier.
    Subspace(
        modifier =
            SubspaceModifier.requiredSizeIn(
                maxWidth = 10000.dp,
                maxHeight = 10000.dp,
                maxDepth = 10000.dp,
            )
    ) {
        SpatialPanel(SubspaceModifier.fillMaxSize()) {
            Text("Panel in a custom sized Subspace escaping default constraints")
        }
    }
}
Parameters
modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to the content of this Subspace.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The 3D content to render within this Subspace.

@Composable
@ComposableOpenTarget(index = -1)
@ExperimentalFollowingSubspaceApi
fun Subspace(
    follow: FollowTarget,
    modifier: SubspaceModifier = SubspaceModifier,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Creates a 3D space that positions spatial UI content relative to a FollowTarget.

Each call to Subspace creates a new, independent spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent Subspace it is nested within. Its scale is decided by the system's recommended scale. Its position in the world is determined solely by its follow parameter. By default, this Subspace is automatically bounded by the system's recommended content box, similar to Subspace.

When the follow parameter is specified to be FollowTarget.view, the content will be positioned relative to the view of the AR device. This is sometimes referred to as head-locked content. For this API, it is required for device tracking to not be disabled in the session configuration. If it is disabled, this API will not return anything. The session configuration should resemble session.configure( config = Config.Builder(session.config).setDeviceTracking(DeviceTrackingMode.SPATIAL).build() )

When the follow parameter is specified to be FollowTarget.anchor, the content will be positioned around an anchor. This is useful for placing UI elements on real-world surfaces or at specific spatial locations. The visual stability of the anchored content depends on the underlying system's ability to track the androidx.xr.scenecore.AnchorSpace. For creating, loading, and persisting anchors, please check androidx.xr.scenecore.AnchorSpace for more information.

This composable is a no-op in non-XR environments (i.e., Phone and Tablet).

Managing Spatial Overlap

Because each call to any kind of Subspace function creates an independent 3D scene, these spaces are not aware of one another. This can lead to a scenario where a moving Subspace (like a head-locked menu) can intersect with content in another stationary Subspace. This overlap can cause jarring visual artifacts and z-depth ordering issues (Z-fighting), creating a confusing user experience. A Subspace does not perform automatic collision avoidance between these independent Subspaces. It is the developer's responsibility to manage the layout and prevent these intersections or to introduce custom hit handling.

Guidelines for Preventing Overlap:

  1. Control Volume Size: Carefully define the bounds of your Subspace instances. Instead of letting content fill the maximum recommended constraints, use sizing modifiers to create smaller, manageable content areas that are less likely to collide.

  2. Use Strategic Offsets: Use SubspaceModifier.offset to position a Subspace. For example, a head-locked menu can be offset to appear in the user's peripheral vision, reducing the chance it will collide with central content. Also, consider placing different Subspace instances at different depths. This ensures that if they overlap, their z-depth ordering will be clear and predictable. Note, however, that while the visual ordering may be clear, Jetpack XR doesn't guarantee predictable interaction behaviors between UI elements in separate, overlapping Subspaces.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.xr.arcore.Anchor
import androidx.xr.arcore.AnchorCreateSuccess
import androidx.xr.compose.platform.LocalSession
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialMainPanel
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SpatialRow
import androidx.xr.compose.subspace.animation.follow.FollowTarget
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.height
import androidx.xr.compose.subspace.layout.rotate
import androidx.xr.compose.subspace.layout.width
import androidx.xr.runtime.Config
import androidx.xr.runtime.DeviceTrackingMode
import androidx.xr.runtime.Session
import androidx.xr.runtime.math.Pose

val TAG = "AnchoredSubspaceSample"
@Composable
fun MainPanelContent() {
    Text("Main panel")
}

@Composable
fun AppContent() {
    MainPanelContent()

    val session: Session? = LocalSession.current
    if (session == null) return
    session.configure(
        Config.Builder(session.config).setDeviceTracking(DeviceTrackingMode.SPATIAL).build()
    )
    Subspace(follow = FollowTarget.view()) {
        SpatialPanel(SubspaceModifier.height(100.dp).width(200.dp)) {
            Text(
                modifier =
                    Modifier.fillMaxWidth()
                        .fillMaxHeight()
                        .background(Color.White)
                        .padding(all = 32.dp),
                text = "This panel will follow AR device movement.",
            )
        }
    }

    val anchor =
        remember(session) {
            when (val anchorResult = Anchor.create(session, Pose.Identity)) {
                is AnchorCreateSuccess -> anchorResult.anchor
                else -> {
                    Log.e(TAG, "Failed to create anchor: ${anchorResult::class.simpleName}")
                    null
                }
            }
        }
    if (anchor != null) {
        Subspace(
            follow = FollowTarget.anchor(anchor),
            modifier = SubspaceModifier.rotate(pitch = -90f, yaw = 0f, roll = 0f),
        ) {
            SpatialRow {
                SpatialPanel { Text("Spatial panel") }
                SpatialMainPanel()
            }
        }
    }
}
Parameters
follow: FollowTarget

Specifies an entity towards which the Subspace will move.

modifier: SubspaceModifier = SubspaceModifier

The SubspaceModifier to be applied to the content of this Subspace.

content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit

The 3D content to render within this Subspace.