FollowingSubspace

Functions summary

Unit
@Composable
@ComposableOpenTarget(index = -1)
@ExperimentalFollowingSubspaceApi
FollowingSubspace(
    target: FollowTarget,
    behavior: FollowBehavior,
    modifier: SubspaceModifier,
    dimensions: TrackedDimensions,
    allowUnboundedSubspace: Boolean,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
)

Create a user-centric 3D space that is ideal for spatial UI content that follows a target.

Functions

FollowingSubspace

@Composable
@ComposableOpenTarget(index = -1)
@ExperimentalFollowingSubspaceApi
fun FollowingSubspace(
    target: FollowTarget,
    behavior: FollowBehavior,
    modifier: SubspaceModifier = SubspaceModifier,
    dimensions: TrackedDimensions = TrackedDimensions.All,
    allowUnboundedSubspace: Boolean = false,
    content: @Composable @SubspaceComposable SpatialBoxScope.() -> Unit
): Unit

Create a user-centric 3D space that is ideal for spatial UI content that follows a target.

Each call to FollowingSubspace 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 in the world is determined solely by its target parameter. By default, this Subspace is automatically bounded by the system's recommended content box, similar to Subspace.

When the target parameter is specified to be FollowTarget.ArDevice, the content will be positioned relative the view of the AR device. This is sometimes referred to as head-locked content. For this API, it is required for headtracking 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 = session.config.copy(deviceTracking = Config.DeviceTrackingMode.LAST_KNOWN) ) The FollowTarget.ArDevice is not compatible with FollowBehavior.Tight. Combining these together will cause this composable to not be displayed. For a near tight experience, use FollowBehavior.Soft with a low duration value such as FollowBehavior.Soft([FollowBehavior.Companion.MIN_SOFT_DURATION_MS])

When the target 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 AnchorEntity. For Creating, loading, and persisting anchors, please check androidx.xr.scenecore.AnchorEntity 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 FollowingSubspace (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.DisposableEffect
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.FollowingSubspace
import androidx.xr.compose.subspace.FollowBehavior
import androidx.xr.compose.subspace.FollowTarget
import androidx.xr.compose.subspace.SpatialMainPanel
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SpatialRow
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.DeviceTrackingMode
import androidx.xr.runtime.Session
import androidx.xr.runtime.math.Pose
import androidx.xr.scenecore.AnchorEntity

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 = session.config.copy(deviceTracking = DeviceTrackingMode.LAST_KNOWN)
    )
    FollowingSubspace(
        target = FollowTarget.ArDevice(session),
        behavior = FollowBehavior.Soft(durationMs = 500),
    ) {
        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 -> AnchorEntity.create(session, anchorResult.anchor)
                else -> {
                    Log.e(TAG, "Failed to create anchor: ${anchorResult::class.simpleName}")
                    null
                }
            }
        }
    if (anchor != null) {
        FollowingSubspace(
            target = FollowTarget.Anchor(anchorEntity = anchor),
            behavior = FollowBehavior.Tight,
            modifier = SubspaceModifier.rotate(pitch = -90f, yaw = 0f, roll = 0f),
        ) {
            SpatialRow {
                SpatialPanel { Text("Spatial panel") }
                SpatialMainPanel()
            }
        }
        DisposableEffect(anchor) { onDispose { anchor.dispose() } }
    }
}
Parameters
target: FollowTarget

Specifies an area which the Subspace will move towards.

behavior: FollowBehavior

determines how the FollowingSubspace follows the target. It can be made to move faster and be more responsive. The default is FollowBehavior.Soft().

modifier: SubspaceModifier = SubspaceModifier

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

dimensions: TrackedDimensions = TrackedDimensions.All

A set of boolean flags to determine the dimensions of movement that are tracked. Possible tracking dimensions are: translationX, translationY, translationZ, rotationX, rotationY, and rotationZ. By default, all dimensions are tracked. Any dimensions not listed will not be tracked. For example if translationY is not listed, this means the content will not move as the user moves vertically up and down.

allowUnboundedSubspace: Boolean = false

If true, the default recommended content box constraints will not be applied, allowing the Subspace to be infinite. Defaults to false, providing a safe, bounded space.

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

The 3D content to render within this Subspace.