使用 Jetpack Compose 为 XR 开发界面

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

如果您要将现有基于 Android Views 的应用空间化,有多种开发选项。您可以使用互操作性 API,将 Compose 和 Views 结合使用,或者直接使用 SceneCore 库。有关详情,请参阅我们的使用视图指南

关于子空间和空间化组件

为 Android XR 编写应用时,了解以下概念非常重要:子空间空间化组件

关于子空间

在为 Android XR 进行开发时,您需要为应用或布局添加子空间。子空间是应用内的一个 3D 空间分区,您可以在其中放置 3D 内容、构建 3D 布局,并为原本的 2D 内容添加深度。子空间仅在启用空间化功能时才会渲染。在 Home Space 或非 XR 设备上,该子空间内的任何代码都将被忽略。

创建子空间有两种方式

  • setSubspaceContent():此函数会创建一个应用级子空间。您可以在主 activity 中调用此函数,就像使用 setContent() 一样。应用级子空间的高度、宽度和深度均不受限制,实质上为空间内容提供了无限画布。
  • Subspace:此可组合项可以放置在您应用的界面层次结构中的任何位置,让您可以在 2D 界面和空间界面之间保持布局,而不会丢失文件之间的上下文。这使得在 XR 和其他外形规格之间共享现有应用架构等内容变得更加容易,而无需在整个界面树中提升状态或重新构建应用架构。

如需了解详情,请参阅将子空间添加到您的应用

关于空间化组件

子空间可组合项:这些组件只能在子空间中渲染。它们必须封装在 SubspacesetSubspaceContent() 中,然后才能放置在 2D 布局中。SubspaceModifier 可让您为子空间可组合项添加深度、偏移和定位等属性。

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

创建空间面板

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

Example of a spatial UI panel

您可以使用 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 是子空间可组合项,因此您必须在 SubspacesetSubspaceContent() 中调用它们。在子空间外部调用它们会抛出异常。
  • SpatialPanel 的大小已通过 SubspaceModifier 上的 heightwidth 规范设置。省略这些规范将使面板的大小由其内容的大小决定。
  • 通过添加 movableresizable 修饰符,允许用户调整面板大小或移动面板。
  • 有关尺寸和定位的详细信息,请参阅我们的空间面板设计指南。有关代码实现的更多详细信息,请参阅我们的参考文档

可移动子空间修饰符的工作原理

当用户将面板移开时,默认情况下,可移动子空间修饰符会以与系统在 home space 中调整面板大小类似的方式来缩放面板。所有子内容都继承此行为。要停用此功能,请将 scaleWithDistance 参数设为 false

创建一个 orbiter

Orbiter 是一种空间界面组件。它旨在附加到相应的空间面板、布局或其他实体。Orbiter 通常包含与它所锚定的实体相关的导航和上下文操作项。例如,如果您创建了一个空间面板来显示视频内容,则可以在 orbiter 中添加视频播放控件。

Example of an orbiter

如以下示例所示,在 SpatialPanel 中的 2D 布局内调用一个 orbiter,以封装导航等用户控件。这样做会将其从您的 2D 布局中提取出来,并根据您的配置将其附加到空间面板。

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

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

关于代码的要点

  • 由于 orbiter 是空间界面组件,因此代码可以在 2D 或 3D 布局中重复使用。在 2D 布局中,您的应用仅渲染 orbiter 内的内容,并忽略 orbiter 本身。
  • 有关如何使用和设计 orbiter 的更多信息,请查看我们的设计指南

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

您可以使用 SpatialRowSpatialColumnSpatialBoxSpatialLayoutSpacer 创建多个空间面板并将其放置在空间布局中。

Example of multiple spatial panels in a spatial layout

以下代码示例展示了如何实现此目的。

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

关于代码的要点

使用体积在布局中放置 3D 对象

要在布局中放置 3D 对象,您需要使用一个名为“体积”的子空间可组合项。下面是操作示例。

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

更多信息

添加用于图片或视频内容的表面

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

此示例演示了如何使用 Media3 Exoplayer 和 SpatialExternalSurface 加载并排立体视频

@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 = StereoMode.SideBySide,
        ) {
            val exoPlayer = remember { ExoPlayer.Builder(context).build() }
            val videoUri = Uri.Builder()
                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                // Represents a side-by-side stereo video, where each frame contains a pair of
                // video frames arranged side-by-side. The frame on the left represents the left
                // eye view, and the frame on the right represents the right eye view.
                .path("sbs_video.mp4")
                .build()
            val mediaItem = MediaItem.fromUri(videoUri)

            // onSurfaceCreated is invoked only one time, when the Surface is created
            onSurfaceCreated { surface ->
                exoPlayer.setVideoSurface(surface)
                exoPlayer.setMediaItem(mediaItem)
                exoPlayer.prepare()
                exoPlayer.play()
            }
            // onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its
            // associated Surface are destroyed
            onSurfaceDestroyed { exoPlayer.release() }
        }
    }
}

关于代码的要点

  • 根据您要渲染的内容类型,将 StereoMode 设为 MonoSideBySideTopBottom
    • Mono:图片或视频帧由显示给双眼的一个相同图片组成。
    • SideBySide:图片或视频帧包含一对并排排列的图片或视频帧,其中左侧的图片或视频帧代表左眼视图,右侧的图片或视频帧代表右眼视图。
    • TopBottom:图片或视频帧包含一对垂直堆叠的图片或视频帧,其中顶部的图片或视频帧代表左眼视图,底部的图片或视频帧代表右眼视图。
  • SpatialExternalSurface 仅支持矩形表面。
  • Surface 不会捕获输入事件。
  • 无法将 StereoMode 更改与应用渲染或视频解码同步。
  • 此可组合项无法在其他面板前面渲染,因此如果布局中存在其他面板,您不应使用可移动修饰符。

添加其他空间界面组件

空间界面组件可以放置在您应用的界面层次结构中的任何位置。这些元素可以在您的 2D 界面中重复使用,并且其空间属性仅在启用空间功能时可见。这让您无需编写两次代码即可为菜单、对话框和 T 其他组件添加高程。请参阅以下空间界面示例,以更好地了解如何使用这些元素。

界面组件

启用空间化功能时

在 2D 环境中

SpatialDialog

面板将在 z 深度方向稍微后推,以显示一个提升的对话框

回退到 2D Dialog

SpatialPopup

面板将在 z 深度方向稍微后推,以显示一个提升的弹出式窗口

回退到 2D Popup

SpatialElevation

可以设置 SpatialElevationLevel 以添加高程。

不带空间高程显示。

SpatialDialog

这是一个对话框示例,它会在短暂延迟后打开。使用 SpatialDialog 时,对话框会以与空间面板相同的 z 深度显示,并且在启用空间化功能时,面板会后推 125dp。即使未启用空间化功能,也可以使用 SpatialDialog,在这种情况下,SpatialDialog 会回退到其 2D 对应项 Dialog

@Composable
fun DelayedDialog() {
    var showDialog by remember { mutableStateOf(false) }
    LaunchedEffect(Unit) {
        delay(3000)
        showDialog = true
    }
    if (showDialog) {
        SpatialDialog(
            onDismissRequest = { showDialog = false },
            SpatialDialogProperties(
                dismissOnBackPress = true
            )
        ) {
            Box(
                Modifier
                    .height(150.dp)
                    .width(150.dp)
            ) {
                Button(onClick = { showDialog = false }) {
                    Text("OK")
                }
            }
        }
    }
}

关于代码的要点

创建自定义面板和布局

要创建 Compose for XR 不支持的自定义面板,您可以直接使用 PanelEntity 实例和场景图,方法是使用 SceneCore API。

将 orbiter 锚定到空间布局和其他实体

您可以将 orbiter 锚定到 Compose 中声明的任何实体。这涉及在 `SpatialRow`、`SpatialColumn` 或 `SpatialBox` 等界面元素的空间布局中声明一个 orbiter。orbiter 会锚定到离您声明它的位置最近的父实体。

orbiter 的行为由您声明它的位置决定

  • 在封装在 SpatialPanel 中的 2D 布局中(如前面的代码片段所示),orbiter 会锚定到该 SpatialPanel
  • Subspace 中,orbiter 会锚定到最近的父实体,即声明 orbiter 的空间布局。

以下示例展示了如何将 orbiter 锚定到空间行

Subspace {
    SpatialRow {
        Orbiter(
            position = OrbiterEdge.Top,
            offset = EdgeOffset.inner(8.dp),
            shape = SpatialRoundedCornerShape(size = CornerSize(50))
        ) {
            Text(
                "Hello World!",
                style = MaterialTheme.typography.h2,
                modifier = Modifier
                    .background(Color.White)
                    .padding(16.dp)
            )
        }
        SpatialPanel(
            SubspaceModifier
                .height(824.dp)
                .width(1400.dp)
        ) {
            Box(
                modifier = Modifier
                    .background(Color.Red)
            )
        }
        SpatialPanel(
            SubspaceModifier
                .height(824.dp)
                .width(1400.dp)
        ) {
            Box(
                modifier = Modifier
                    .background(Color.Blue)
            )
        }
    }
}

关于代码的要点

  • 当您在 2D 布局外部声明 orbiter 时,orbiter 会锚定到它最近的父实体。在这种情况下,orbiter 会锚定到声明它的 SpatialRow 的顶部。
  • `SpatialRow`、`SpatialColumn`、`SpatialBox` 等空间布局都关联了无内容实体。因此,在空间布局中声明的 orbiter 会锚定到该布局。

另请参阅