使用 Jetpack Compose for XR 开发空间界面

适用的 XR 设备
本指南可帮助您为以下类型的 XR 设备打造优质体验。
扩展现实头戴设备
有线扩展现实眼镜

借助 Jetpack Compose for XR,您可以使用熟悉的 Compose 概念(例如行和列)以声明方式构建空间界面和布局。这样,您就可以将现有的 Android 界面扩展到 3D 空间,或者构建全新的沉浸式 3D 应用。

如果您要将基于 Android View 的现有应用空间化,则有多种开发选项。您可以使用互操作性 API,将 Compose 和 View 搭配使用,也可以直接使用 SceneCore 库。如需了解详情,请参阅我们的视图使用指南

关于子空间和空间化组件

为 Android XR 编写应用时,请务必了解子空间空间化组件的概念。

关于子空间

为 Android XR 开发时,您需要向应用或布局添加子空间。子空间是应用中 3D 空间的一部分,您可以在其中放置 3D 内容、构建 3D 布局,以及为原本的 2D 内容添加深度。仅当空间化处于启用状态时,才会渲染子空间。在主共享空间或非 XR 设备上,该子空间内的任何代码都会被忽略。

您可以通过以下几种方式创建子空间:

  • Subspace:此可组合项会创建一个新的独立空间界面层次结构。它不会继承任何父级 Subspace(如果嵌套在其中)的空间位置、方向或缩放比例。Subspace 会自动绑定到系统的推荐内容框。
  • PlanarEmbeddedSubspace:此可组合项可放置在应用界面层次结构中,让您能够维护 2D 和空间界面的布局。PlanarEmbeddedSubspace 会遵守其父级的约束条件和定位。然后,放置在其中的 3D 内容会相对于此 2D 定义的区域进行定位。

如需了解详情,请参阅向应用添加子空间

关于空间化组件

子空间可组合项:这些组件只能在子空间中呈现。 它们必须先封装在 Subspace 中,然后才能放置在 2D 布局中。 借助 SubspaceModifier,您可以向子空间可组合项添加深度、偏移和定位等属性。

其他空间化组件不需要在子空间内调用。它们由封装在空间容器中的常规 2D 元素组成。如果为 2D 和 3D 布局都定义了这些元素,则可以在这两种布局中使用它们。如果未启用空间化,系统会忽略其空间化功能,并回退到其 2D 对应项。

创建空间面板

SpatialPanel 是一种子空间可组合项,可用于显示应用内容,例如,您可以在空间面板中显示视频播放、静态图片或任何其他内容。

空间界面面板示例

您可以使用 SubspaceModifier 更改空间面板的大小、行为和位置,如以下示例所示。

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .movable()
            .resizable(),
    ) {
        SpatialPanelContent()
    }
}

@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
        )
    }
}

代码要点

  • 由于 SpatialPanel API 是子空间可组合项,因此您必须在 Subspace 内调用它们。在子空间之外调用它们会抛出异常。
  • SpatialPanel 的大小已使用 SubspaceModifier 上的 heightwidth 规范进行设置。如果省略这些规范,面板的大小将由其内容的尺寸决定。
  • 通过添加 movable 子空间修饰符,允许用户移动面板。
  • 通过添加 resizable 子空间修饰符,允许用户调整面板大小。
  • 如需详细了解尺寸调整和定位,请参阅我们的空间面板设计指南。如需详细了解代码植入,请参阅我们的参考文档

movable 修饰符的运作方式

当用户将面板移离自己时,默认情况下,movable 修饰符会以类似于系统在主空间中调整面板大小的方式来缩放面板。所有子内容都会继承此行为。如需停用此功能,请将 shouldScaleWithDistance 参数设置为 false

创建轨道飞行器

轨道器是一种空间界面组件。它旨在附加到相应的空间面板或空间布局组件,例如 SpatialColumnSpatialRowSpatialBox。轨道通常包含与所锚定的实体相关的导航和关联操作项。例如,如果您创建了一个用于显示视频内容的空间面板,则可以在轨道球中添加视频播放控件。

轨道飞行器示例

如下例所示,在 SpatialPanel 中调用轨道飞行器,以封装导航等用户控件。这样一来,系统会从 2D 布局中提取这些元素,并根据您的配置将其附加到空间面板。

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .resizable()
            .movable(),
    ) {
        SpatialPanelContent()
        OrbiterExample()
    }
}

@Composable
fun OrbiterExample() {
    Orbiter(
        alignment = OrbiterAlignment.BottomCenter(
            edgeOffsetType = OrbiterEdgeOffsetType.OuterEdge,
            offset = DpVolumeOffset(y = 96.dp)
        ),
    ) {
        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
                )
            }
        }
    }
}

代码要点

  • 由于轨道飞行器是空间界面组件,因此代码可以在 2D 或 3D 布局中重复使用。在 2D 布局中,应用仅渲染轨道飞行器内部的内容,而忽略轨道飞行器本身。
  • 如需详细了解如何使用和设计轨道球,请参阅我们的设计指南

向空间布局添加多个空间面板

您可以使用 SpatialRowSpatialColumnSpatialBoxSpatialSpacer 创建多个空间面板,并将它们放置在空间布局中。

空间布局中多个空间面板的示例

以下代码示例展示了如何执行此操作。

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
        )
    }
}

代码要点

使用 SpatialGltfModel 将 3D 对象添加到布局中

Android XR 支持 3D 模型的 glTF 格式,通常保存为 .glb 文件。如需将这些对象添加到布局中,您应使用 SpatialGltfModel 可组合函数。此 API 可简化加载资源和管理资源状态的过程。

如需显示模型,请先使用 rememberSpatialGltfModelState 定义其来源和状态。您可以从应用的 assets 文件夹、URIraw data 加载模型。

val modelState = rememberSpatialGltfModelState(
    source = SpatialGltfModelSource.fromPath(
        Paths.get("models/model_name.glb")
    )
)

定义状态后,使用 SpatialGltfModel 可组合项在子空间内呈现该状态。

SpatialGltfModel(state = modelState, modifier = SubspaceModifier)

代码要点

  • 异步加载:模型以异步方式加载。在初始合成期间,其固有大小可能为零;模型准备就绪后,布局会重新测量。
  • 控制状态:使用 SpatialGltfModelState.status 查询加载状态或控制动画。
  • 大小调整和缩放:默认情况下,布局大小与素材资源的边界框相匹配。您可以使用 SubspaceModifier.size 替换此设置,以按比例调整模型大小,使其在指定范围内。

使用 SceneCoreEntity 在布局中放置实体

SceneCoreEntity 可组合项可桥接 Jetpack SceneCoreCompose for XR 库,以便您可以在 Compose 布局中使用通过 SceneCore 构建的实体。这样,您就可以构建较低级别的实体和自定义组件,同时允许 Compose 调整这些实体的大小、定位、重新设置父级、添加子级和应用修饰符。

val virtualPixelDensity = session.scene.virtualPixelDensity

Subspace {
    SceneCoreEntity(
        modifier = SubspaceModifier.offset(x = 50.dp),
        factory = {
            SurfaceEntity.create(
                session = session,
                pose = Pose.Identity,
                stereoMode = SurfaceEntity.StereoMode.MONO
            )
        },
        update = { entity ->
            // Compose state changes may be applied to the SceneCore entity here.
            entity.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE
        },
        sizeAdapter =
            object : SceneCoreEntitySizeAdapter<SurfaceEntity> {
                override fun onLayoutSizeChanged(
                    entity: SurfaceEntity,
                    size: IntVolumeSize
                ) {
                    val extents = FloatSize2d(
                        size.width.toFloat(),
                        size.height.toFloat()
                    )
                    entity.shape = SurfaceEntity.Shape.Quad(extents)
                }
            },
    ) {
        // Content here will be children of the SceneCoreEntity
        // in the scene graph.
    }
}

代码要点

  • 工厂块:工厂块用于初始化底层 SceneCore 实体。
  • 更新块:使用更新块来修改实体的属性,以响应 Compose 状态的变化。
  • 大小自适应:sizeAdapter 将实体的尺寸传回给 Compose 布局系统。

其他信息

添加图片或视频内容展示界面

SpatialExternalSurface 是一种可组合的子空间,用于创建和管理应用可在其中绘制内容(例如图片或视频)的 SurfaceSpatialExternalSurface 支持立体或单视场内容。

此示例演示了如何使用 Media3 ExoplayerSpatialExternalSurface 加载并排立体视频:

@OptIn(ExperimentalComposeApi::class)
@Composable
fun SpatialExternalSurfaceContent() {
    val context = LocalContext.current
    Subspace {
        SpatialExternalSurface(
            modifier = SubspaceModifier
                .width(1200.dp) // Default width is 400.dp if no width modifier is specified
                .height(676.dp), // Default height is 400.dp if no height modifier is specified
            // Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending
            // upon which type of content you are rendering: monoscopic content, side-by-side stereo
            // content, or top-bottom stereo content
            stereoMode =