使用 Jetpack Compose for XR 开发界面

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

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

子空间和空间化组件简介

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

子空间简介

针对 Android XR 进行开发时,您需要向应用或布局添加子空间。子空间是应用内 3D 空间的一个分区,您可以在其中放置 3D 内容、构建 3D 布局,以及为原本为 2D 的内容添加深度。只有在启用空间化时,系统才会渲染子空间。在主屏幕空间或非 XR 设备上,该子空间中的任何代码都会被忽略。

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

  • setSubspaceContent:此函数会创建应用级子空间。您可以在 MainActivity 中调用此方法,方法与使用 setContent 相同。应用级子空间的高度、宽度和深度不受限制,从本质上讲,为空间内容提供了无限的画布。
  • Subspace:此可组合项可放置在应用界面层次结构中的任何位置,让您能够维护 2D 和空间界面的布局,而不会丢失文件之间的上下文。这样,您就可以更轻松地在 XR 设备和其他设备类型之间共享现有应用架构等内容,而无需通过整个界面树提取状态或重新构建应用。

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

关于空间化组件

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

  • 关于子空间修饰符的注意事项:请仔细注意 SubspaceModifier API 的顺序。
    • 偏移量必须在修饰符链中首先出现
    • 可移动和可调整大小必须最后出现
    • 必须先应用旋转,然后再应用缩放

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

创建空间面板

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

空间界面面板示例

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

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

// 2D content placed within the spatial panel
@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
        )
    }
}

代码要点

  • 关于子空间修饰符的注意事项:请仔细注意 SubspaceModifier API 的顺序。
    • 偏移量必须在修饰符链中首先出现。
    • 可移动和可调整大小的修饰符必须最后出现。
    • 必须先应用旋转,然后再应用缩放。
  • 由于 SpatialPanel API 是子空间可组合项,因此您必须在 SubspacesetSubspaceContent 内调用它们。在子空间之外调用它们会抛出异常。
  • 通过添加 .movable.resizable SubspaceModifier,允许用户调整面板大小或移动面板。
  • 如需详细了解尺寸和定位,请参阅我们的空间面板设计指南。如需详细了解代码实现,请参阅我们的参考文档

创建轨道器

轨道器是一种空间界面组件。它旨在附加到相应的空间面板,并包含与该空间面板相关的导航和上下文操作项。例如,如果您创建了用于显示视频内容的空间面板,则可以在绕视器中添加视频播放控件。

轨道器示例

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

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

//2D content inside Orbiter
@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
                )
            }
        }
    }
}

代码要点

  • 关于子空间修饰符的注意事项:请仔细注意 SubspaceModifier API 的顺序。
    • 偏移量必须在修饰符链中首先出现
    • 可移动和可调整大小必须最后出现
    • 必须先应用旋转,然后再应用缩放
  • 由于 Orbiter 是空间界面组件,因此代码可在 2D 或 3D 布局中重复使用。在 2D 布局中,您的应用仅渲染 Orbiter 中的内容,并忽略 Orbiter 本身。
  • 如需详细了解如何使用和设计轨道器,请参阅我们的设计指南

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

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

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

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

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 对象,您需要使用一个名为“体积”的子空间可组合项。以下示例展示了如何执行此操作。

布局中的 3D 对象示例

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) {
    val xrCoreSession = checkNotNull(LocalSession.current)
    val scope = rememberCoroutineScope()
    if (show3DObject) {
        Subspace {
            Volume(
                modifier = SubspaceModifier
                    .offset(volumeXOffset, volumeYOffset, volumeZOffset) //
Relative position
                    .scale(1.2f) // Scale to 120% of the size

            ) { parent ->
                scope.launch {
                   // Load your 3D Object here
                }
            }
        }
    }
}

代码要点

  • 关于子空间修饰符的注意事项:请仔细注意 SubspaceModifier API 的顺序。
    • 偏移量必须在修饰符链中首先出现
    • 可移动和可调整大小必须最后出现
    • 必须先应用旋转,然后再应用缩放
  • 如需更好地了解如何在卷中加载 3D 内容,请参阅添加 3D 内容

添加其他空间界面组件

空间界面组件可放置在应用界面层次结构中的任意位置。这些元素可在 2D 界面中重复使用,并且只有在启用空间功能时,其空间属性才会显示。这样一来,您无需编写两次代码,即可为菜单、对话框和其他组件添加高度。请查看以下空间界面示例,以便更好地了解如何使用这些元素。

界面组件

启用空间化时

在 2D 环境中

SpatialDialog

面板会在 z 深度上稍微向后推,以显示提升式对话框

回退到 2D Dialog

SpatialPopUp

面板会在 z 深度上稍微向后推,以显示提升后的弹出式窗口

回退到 2D PopUp

SpatialElevation

SpatialElevationLevel 可设置为添加海拔。

不含空间高度信息的节目。

SpatialDialog

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

@Composable
fun DelayedDialog() {
   var showDialog by remember { mutableStateOf(false) }
   LaunchedEffect(Unit) {
       Handler(Looper.getMainLooper()).postDelayed({
           showDialog = true
       }, 3000)
   }
   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 不支持的自定义面板,您可以使用 SceneCore API 直接与 PanelEntities 和场景图表进行交互。

另请参阅