借助 Android XR,您可以突破屏幕空间的限制,将应用锚定在现实世界中。本课程将介绍如何使用 Jetpack Compose Glimmer 和 ARCore 的 Geospatial API 构建具有地理空间感知能力的 Android XR 体验。首先,我们将介绍如何使用 ARCore 获取设备的地理空间姿态。然后,您将了解如何通过视觉定位系统 (VPS) 将 Glimmer 的极简界面组件锚定到现实世界中的位置。我们将结合使用这些工具,构建一个简单的位置感知空间体验。
转写文稿
大家好,欢迎参加 Android 开发者峰会 Android XR 专题的第一场演讲。
好啊!
我们很高兴看到这么多人对 Android XR 感兴趣。
我叫 Yasmine Quintana Evjen,今天还有 Paul Hoover 和 Dereck Bridie 与我一起,他们将在对话继续时加入我们。
本期主题是 Android XR,以及如何使用 Jetpack Compose Glimmer 和 ARCore for Jetpack XR 构建空间锚定、现实世界体验。
不过,我们先来简要介绍一下 Android XR。
Android XR 是一个全新平台,它重新定义了用户与应用的互动方式,让互动不再局限于屏幕,而是延伸到用户周围的空间。
我们设计该平台是为了将数字体验直接带入物理环境。
无论是增强现实体验的一目了然的叠加层,还是无限沉浸式环境,都能轻松实现。
这是我们多年来共同打造的开放式 Android 生态系统,现在,画布可以像房间一样大,也可以像您的想象力一样广阔。
我们认为 XR 涵盖一系列设备,从智能眼镜(可在音频和带屏眼镜上实现轻量级增强体验)到沉浸式头戴设备和有线 XR 眼镜(可带来专注、高效和沉浸式娱乐功能)。
Android XR 家族的最新设备 XREAL AURA 现已上市,欢迎选购。
这款设备轻巧而强大,可让您在熟悉的 Android 环境中畅享 XR 体验。
我们对该平台的愿景是,如果您已经在开发 Android 应用,那么您就已经准备好开发 XR 应用了。
我们希望您能使用熟悉的同一应用模型、库和工具,轻松在 Android XR 上进行开发。
您现有的 2D Android 兼容应用只需进行少量修改或无需修改,即可在 Android XR 头戴设备和有线 XR 眼镜上运行。
为了打造自然流畅、令人惊艳的 XR 体验,我们提供了一套专为 XR 平台打造的库。
对于习惯在其他沉浸式平台上构建体验的开发者,您可以使用 OpenXR 以及我们支持的 3D 引擎(例如 Unity、Godot 和 Unreal)进行构建。
我们来谈谈如何使用 Android 库进行构建。
我们提供 Jetpack XR SDK,这是一组 Kotlin 库,可帮助您将应用扩展到 XR,打造沉浸式和增强现实体验。
我们来看看沉浸式库。
我们的 Jetpack XR 库将于今年推出 Beta 版和稳定版,让您能够安心着手打造沉浸式 XR 体验。
感知层是 ARCore for Jetpack XR。
此库可提供有关现实世界的信息,例如桌子等平坦表面的位置以及用户手部的位置。
它能跟踪 3D 空间中的点,并在获得适当权限后将这些信息提供给应用。
然后是 Jetpack XR SceneCore,它提供了一个场景图,用于放置空间内容。
该库能够在 3D 空间中呈现实体,并提供空间行为,以便在场景图中精确放置实体。
Jetpack Compose for XR 提供了专为 3D 打造的新子空间可组合项。
已经熟悉 Compose 布局(例如使用行和列进行构建)的 Android 开发者会发现,空间界面布局非常直观。
通过创建空间行,您可以以声明方式并排布置多个空间面板,而无需手动计算其位置或大小。
使用空间面板或 Orbiter 等子空间可组合项,您可以轻松地将 2D 组件扩展到这个新的 3D 空间中。
当然,子空间可组合项的功能不仅仅是显示 2D 内容。
Compose for XR 通过空间外部 Surface 和空间 glTF 模型支持空间视频以及 3D 模型。
现在,我们来谈谈增强型应用。
借助 Jetpack Projected 库,您可以增强现有移动应用,以利用智能眼镜硬件(例如传感器和显示屏)。
最后,Jetpack Compose Glimmer 是一款界面工具包,用于构建针对眼镜优化的 Android XR 体验。
Jetpack Compose Glimmer 包含各种样式和即用型组件,可用于打造 XR 体验。
我们去年推出了 Glimmer,并持续扩展其功能,包括一项智能体技能,可帮助您使用 Glimmer 构建显示眼镜应用。
android skills add display-glasses-with-jetpack-compose-glimmer自发布以来,我们一直在听取您的反馈。
我们了解到,您希望我们将 Jetpack Compose Glimmer 的应用范围扩展到智能眼镜之外。
我们很高兴地宣布,我们已做出一些改进,以便更好地支持您使用 Glimmer 打造空间锚定体验,我们将在本演讲的后续部分深入探讨这些改进。
我们对 Glimmer 的优化工作始于创建一个新的界面,以便在各种 XR 设备类型(从显示屏到有线 XR 眼镜)上显示。
新增的这一新界面让您在自定义界面时拥有更大的灵活性。
为了提升应用品牌的独特性,我们为 Glimmer 组件添加了自定义颜色。
使用这些设置可在 XR 应用中表达您的品牌风格。
我们的 Jetpack Compose Glimmer 库提供了辅助函数,可将您的目标颜色和品牌颜色转换为适合眼镜显示的颜色。
这是为了确保在光学透视显示屏上获得最佳可见性。
我们来看看实际用例。
我们想更改这款眼镜应用的聚焦按钮颜色,我将选择一种我最喜欢的颜色。
val brandColor = Color(0xFFFFAABB)
val focusedColor = ButtonDefaults.focusedColor(brandColor)
Column(…) {
LeadingImageCard(
focusable = false,
…
) {
…
}
ButtonGroup(...) {
Button(
onClick = {},
leadingIcon = { … },
focusedColor = focusedColor,
) { Text("Add to cart") }
/* Rest of the buttons */
}
}请注意,将颜色添加到代码中后,Glimmer 辅助函数会将其调整为适合眼镜显示的颜色,以优化可见性。
所有这些功能都内置于我们的 Jetpack Compose Glimmer 库中,可确保您打造的体验在各种环境下都能呈现出色的视觉效果。
在 Android Studio 中,您可以使用模拟器直观呈现眼镜界面在各种背景(例如明亮、黑暗或繁忙的环境)下的显示效果。
这些更新仅仅是冰山一角。
好吧,不行。
我的团队告诉我,这个笑话不会逗笑大家,但我还是坚持讲了。
现在,我将把时间交给 Paul,请他为大家详细介绍 Jetpack Compose Glimmer。
测试,没问题!
我得点击“下一步”按钮。
好的,谢谢 Yasmine。
好了
正如 Yasmine 所说,Jetpack Compose Glimmer 提供了基础构建块,用于开发专为带屏眼镜量身定制的应用。
我们的组件集成了专为光学透视显示屏打造的最佳实践,并且我们直接将这些组件内置到 API 中,让所有开发者都能构建美观且实用的光学透视体验。
在为此类光学透视设备设计和构建应用时,您需要注意以下几点。
首先,它是一个透视显示屏,这意味着没有黑色。
当世界可见时,黑色是透明的。
因此,您不能指望通过深色背景来保持界面、文本和内容的对比度。
虽然黑色不可见,但您可以使用它来遮挡其他数字内容。
在 Glimmer 界面框架中,我们提供了一个深度 API,可让您轻松地将界面元素彼此分开,并提供层次感。
在这里,您可以看到此堆叠中卡片的深度如何帮助它们彼此保持分离。
我们的视觉科学家提供了文字和图标的目标对比度,这些文字和图标在大多数光照条件下都清晰可见。
正如 Yasmine 所展示的,我们已经将这一功能内置到框架中,因此当您自定义表面颜色时,可以确信您的界面对用户来说是清晰易读的。
我们已将这些最佳实践和其他最佳实践融入到不断扩展的组件库中。
我们最近发布了许多新组件。
我们最新推出的组件之一是分页器组件,它内置了流畅的动作过渡和手势。
val pagerState =
rememberGlimmerPagerState(pageCount = { 10 })
GlimmerHorizontalPager(pagerState) { page ->
/* Card Content */
}如果您熟悉 Compose 的水平分页器,那么您对这个组件也会非常熟悉。
我们还推出了四种新的卡片变体,旨在与分页器、堆叠和其他 Glimmer 界面无缝集成。
前导图片卡片和尾随图片卡片会并排显示文字和图片。
因此,当文本和视觉信息量相当时,请使用这些卡片变体。
我们已将图片卡片拆分为单独的卡片变体。
如果您的图片很大,且只有一行文字或说明(例如这个视频选择器),则最适合使用此选项。
操作卡片变体会在卡片中嵌入一个按钮。
如果您希望用户确切知道激活卡片后会发生什么,请使用此选项,例如在此目的地选择器中。
按钮组可让您在狭小的空间内放置大量按钮。
ButtonGroup {
Button(onClick = {}) { Text("Button 1") }
Button(onClick = {}) { Text("Button 2") }
Button(onClick = {}) { Text("Button 3") }
Button(onClick = {}) { Text("Button 4") }
Button(onClick = {}) { Text("Button 5") }
}只需使用这段简单的代码,您就可以获得这些精美的过渡效果、滚动行为、隐藏边缘的遮罩(仅在需要时显示),以及从眼镜自动路由到控件的输入。
按钮组还可以包含新的图标按钮和切换按钮。
var buttonChecked by remember { mutableStateOf(false) }
ToggleButton(
checked = buttonChecked,
leadingIcon = { Icon(FavoriteIcon) },
onCheckedChange = { buttonChecked = it }
) {
Text(if (buttonChecked) "On" else "Off")
}您可以在这里看到我们所有的按钮类型,它们都用于创建此媒体控件。
现在,您可以轻松地在不同背景上测试界面。
眼镜模拟器实际上允许您放入 360 度全景照片,并让您平移,以便您可以在不同的背景、繁忙的背景以及明亮、黑暗的环境中进行测试。
按钮组非常适合在这样的详情视图中使用,以显示关联操作。
点按后,您可以使用共享元素过渡导航到详细查看器状态,然后使用系统返回手势返回到上一个视图。
太棒了。
此外,我们还添加了语音输入指示器和提醒对话框。
所以,是的,有很多新功能。
现在,我们已经了解了一些新功能,接下来让我们动手构建一些东西吧。
除了使用 Glimmer 构建智能眼镜应用之外,我们还可以使用 Glimmer 为有线 XR 眼镜构建应用体验。
我们希望您能打造增强现实体验,让用户能够从现实世界中基于地理位置的锚点开始探索。
假设您身处一个陌生的城市,也许是去参加开发者大会,您想在周围找点吃的。
您可以让喜爱的导航应用查找附近的餐厅及其评分。
信息可以直接叠加在位置上,提供背景信息、智能信息和指导。
在这里,您可以看到我们最新的 Glimmer 组件,名为 Icon Marker。
您可以借助它突出显示现实世界中的关键元素,并将其与 Glimmer 的其他组件(例如此卡片)搭配使用。
在此示例中,它使用 Google 地图、地点和 Geospatial API 为地图注点添加注释。
Dereck,我们来演示一下如何设置。
那就开始吧。
好了
好了
正如 Paul 之前提到的,我们开始构建吧。
因此,我们实际上会使用 developer.android.com/xr 上的开发者文档,并查看 Jetpack Compose Glimmer 组件库。
因此,您将能够直观地了解 Glimmer 上提供的所有组件。
我们先从 Paul 之前展示的这个界面开始。
因此,我们将使用 Glimmer 界面来创建空间指示器和信息框。
首先,我们来确定要使用的组件。
左侧是一个图标标记,右侧是一个用于描述地点的卡片。
因此,如果您前往 Android Studio,实际上可以像预览任何其他 Compose 组件一样预览此窗格,并且可以实时查看更改。
如前所述,您可以在各种不同类型的背景环境中预览界面,并确保界面在多种类型的场景(例如深色和浅色环境)中具有足够的对比度。
现在,画布已设置完毕,我们将创建一个行,在其中放置一个图标标记(带有一些间距),以及一个包含一些示例文本的卡片。
看,结果出来了。
如果您对 2D 预览感到满意,我们就可以开始在现实世界中标记一些内容,为此,我们需要将界面空间化。
也就是说,我们将以 3D 形式(而非 2D 形式)进行布局。
因此,空间界面包含在子空间中,子空间是用户面前的 3D 空间。
Subspace {
SpatialPanel {
VideoPlayer()
}
}默认情况下,子空间实际上会在距离用户舒适的距离处布置其内容。
因此,在向您展示如何使用 Geospatial API 之前,我们先在附近放置一些内容。
因此,我之前提到的子空间可组合项有一个可选参数,可让您指定跟随目标。
val state by
qrCode.state.collectAsState()
Subspace(FollowTarget.anchor(anchor)) {
SpatialPanel {
Row {
IndicatorDot()
Spacer()
QrCodeCard(state)
}
}
}因此,如需将内容附加到现实世界,您首先需要创建一个从 ARCore for Jetpack XR 获取的锚点。
在此示例中,我实际上使用的是二维码识别功能。
这样,您就可以读取和跟踪二维码,让内容在空间中跟随二维码移动,并且我们将附加之前看到的界面。
好的。
我向您展示了如何在附近的现实世界中放置数字内容。
我们再回来看 Paul 的例子。
我个人来自荷兰。
我对山景城不太熟悉。
因此,在本次活动结束后,我想帮助您创建一款应用,帮助您找到一些有趣的咖啡店。
现在,ARCore for Jetpack XR 中已包含 Geospatial API,让我们看看如何利用它来帮助用户找到这些宝藏。
因此,最初在智能手机上推出的 Geospatial API 现在也支持 Android XR。
它会使用 Google 街景中的数据,并将其与 GPS 测量结果相结合,从而在支持的区域中定位设备。
因此,在智能眼镜上,Geospatial API 可用于准确确定用户的位置和朝向,并支持高精度的精细导航用例。
在有线 XR 眼镜等沉浸式设备上,Geospatial API 可让您打造在现实世界中精确定位的体验。
例如,您可以将空间内容锚定在建筑物上,或创建专门的城市游览。
那么,我们开始通过以下三个步骤来使用 Geospatial API。
第一步是创建 Google Cloud Platform 项目并在应用中配置身份验证。
然后,在应用中配置并启用 Geospatial API 后,您需要请求用户授予权限,以便使用精确位置信息检索其位置。
然后,将该会话配置为使用 Geospatial API,一切就准备就绪了。
因此,您可以在我们全新改版的 Android XR 开发者页面上找到有关 Geospatial API 的开发者文档的所有详细信息。
首先,我们使用用户的位置信息,通过 Google Maps Platform 的 Places API 查找用户附近的一些优质咖啡馆。
首先,用户在哪里?
我们将请求 Geospatial API 持续更新用户的位置信息。
produceState<AppStatus>(Initializing) {
val geospatial = Geospatial.getInstance(session)
val wait = geospatial.state.onEach {
value = WaitingForGeospatial(it.geospatialTrackingState)
}.first {
it.geospatialTrackingState == Geospatial.GeospatialTrackingState.RUNNING
}
val accuracyWait = geospatial.state.onEach {
value = WaitingForGeospatialAccuracy(it)
}.first { it.horizontalAccuracy < horizontalAccuracyRequirement }
value = WaitingForPlacesResults
val result = client.findNearby(context, accWait.geospatialPose)
value = Done(result)
}因此,我们将通过 XR 会话访问地理空间对象。
之后,我们将等待 Geospatial 初始化必要的传感器并连接到 Cloud API,以便启动并运行。
然后,我们会等待准确率达到一定水平。
因此,随着 GPS 测量结果的传入,或者当视觉定位系统在用户所在位置找到匹配项时,精确度水平可能会提高。
等待高精确度级别对于确保基于位置的体验锚定在正确的位置以及结果来自用户附近区域非常重要。
然后,我们终于可以使用 Geospatial API 中的位置值,通过 Places API 执行搜索了。
val bounds = CircularBounds.newInstance(
LatLng(pose.latitude, pose.longitude),
100.0, // radius, in meters
)
val placeFields = listOf(
Place.Field.ID, Place.Field.DISPLAY_NAME,
Place.Field.RATING, Place.Field.LOCATION,
)
return placesClient.searchNearby(
SearchNearbyRequest.builder(bounds, placeFields)
.setIncludedTypes(listOf(PlaceTypes.CAFE))
.setMaxResultCount(10)
.build()
).await()因此,我们将使用 SearchNearbyRequest 和 Places API 来查询我们之前获取的用户位置周围的地点,并查找 100 米范围内的结果。
我们将获取该地点的 ID、显示名称、评分,当然还有其位置信息。
最后,我们将使用指定的参数(包括地点类型过滤器)执行搜索。
数据部分就介绍到这里,我们回到界面。
因此,我们需要弄清楚如何放置之前的标记和信息框,为此,我们将使用地理空间锚点。
回到代码,我们再次使用 XR 会话访问 Geospatial API,然后使用之前从搜索结果中获取的特定纬度和经度创建地理空间锚点。
val anchorResult by produceState<AnchorResult?>(null) {
val geospatial = Geospatial.getInstance(session)
value = geospatial.createAnchorOnSurface(
place.location.latitude, place.location.longitude,
surface = GeospatialSurface.TERRAIN,
altitudeAboveSurface = 3.0,
eastUpSouthQuaternion = Quaternion()
)
}因此,在本例中,我们指定的高度是这些坐标处地形上方 3 米,但您也可以将其附加到受支持的屋顶,这意味着高度值将相对于该位置建筑物的屋顶来确定。
因此,您也可以使用四元数来旋转锚点,四元数指定了锚点的旋转值。
但在此示例中,我们实际上会使用默认值,因为我们会让界面旋转以面向用户。
然后,我们将之前的 Glimmer 界面附加到此锚点,这样您就拥有了构建空间锚定体验的基础。
when (anchorResult) {
is AnchorCreateSuccess ->
Subspace(FollowTarget.anchor(anchorResult.anchor)) {
SpatialPanel(SubspaceModifier.rotateToLookAtUser()) {
PlaceIndicator(place)
}
}
else ->
Log.e("Demo", "Failed to create anchor $anchorResult")
}因此,我们相信 Geospatial API 能够实现出色的寻路体验,而使用 Glimmer 进行设计将有助于打造专为透视显示屏而设计的“一瞥即得”式用户体验。
如需详细了解 Geospatial API,请访问最近更新的 developer.android.com/xr。
接下来,我将把时间交还给 Yasmine,让她为我们总结一下。
在本演讲中,我们介绍了 Android XR 的一些功能:ARCore for Jetpack XR 和 Jetpack Compose Glimmer。
我们非常期待 Android XR 将为有线 XR 眼镜带来哪些新功能。
开发者们已经在为 Android XR 开发应用,我们期待在 Google Play 商店中看到您的创意作品。
为了帮助您快速上手,请查看我们的开发者文档,从我们的 XR 实验中汲取灵感,并通过 Codelab 快速入门。
我们为大家准备了大量精彩的 XR 专题会议、演示和研讨会。
因此,请观看我们的演讲,了解如何打造 Android XR 沉浸式和增强现实体验。
我谨代表 Android XR 团队,衷心感谢大家的参与,希望大家在 Android 开发者峰会上度过一段愉快的时光。
所以,谢谢大家。
android skills add display-glasses-with-jetpack-compose-glimmerval brandColor = Color(0xFFFFAABB)
val focusedColor = ButtonDefaults.focusedColor(brandColor)
Column(…) {
LeadingImageCard(
focusable = false,
…
) {
…
}
ButtonGroup(...) {
Button(
onClick = {},
leadingIcon = { … },
focusedColor = focusedColor,
) { Text("Add to cart") }
/* Rest of the buttons */
}
}val pagerState =
rememberGlimmerPagerState(pageCount = { 10 })
GlimmerHorizontalPager(pagerState) { page ->
/* Card Content */
}ButtonGroup {
Button(onClick = {}) { Text("Button 1") }
Button(onClick = {}) { Text("Button 2") }
Button(onClick = {}) { Text("Button 3") }
Button(onClick = {}) { Text("Button 4") }
Button(onClick = {}) { Text("Button 5") }
}var buttonChecked by remember { mutableStateOf(false) }
ToggleButton(
checked = buttonChecked,
leadingIcon = { Icon(FavoriteIcon) },
onCheckedChange = { buttonChecked = it }
) {
Text(if (buttonChecked) "On" else "Off")
}Subspace {
SpatialPanel {
VideoPlayer()
}
}val state by
qrCode.state.collectAsState()
Subspace(FollowTarget.anchor(anchor)) {
SpatialPanel {
Row {
IndicatorDot()
Spacer()
QrCodeCard(state)
}
}
}produceState<AppStatus>(Initializing) {
val geospatial = Geospatial.getInstance(session)
val wait = geospatial.state.onEach {
value = WaitingForGeospatial(it.geospatialTrackingState)
}.first {
it.geospatialTrackingState == Geospatial.GeospatialTrackingState.RUNNING
}
val accuracyWait = geospatial.state.onEach {
value = WaitingForGeospatialAccuracy(it)
}.first { it.horizontalAccuracy < horizontalAccuracyRequirement }
value = WaitingForPlacesResults
val result = client.findNearby(context, accWait.geospatialPose)
value = Done(result)
}val bounds = CircularBounds.newInstance(
LatLng(pose.latitude, pose.longitude),
100.0, // radius, in meters
)
val placeFields = listOf(
Place.Field.ID, Place.Field.DISPLAY_NAME,
Place.Field.RATING, Place.Field.LOCATION,
)
return placesClient.searchNearby(
SearchNearbyRequest.builder(bounds, placeFields)
.setIncludedTypes(listOf(PlaceTypes.CAFE))
.setMaxResultCount(10)
.build()
).await()val anchorResult by produceState<AnchorResult?>(null) {
val geospatial = Geospatial.getInstance(session)
value = geospatial.createAnchorOnSurface(
place.location.latitude, place.location.longitude,
surface = GeospatialSurface.TERRAIN,
altitudeAboveSurface = 3.0,
eastUpSouthQuaternion = Quaternion()
)
}when (anchorResult) {
is AnchorCreateSuccess ->
Subspace(FollowTarget.anchor(anchorResult.anchor)) {
SpatialPanel(SubspaceModifier.rotateToLookAtUser()) {
PlaceIndicator(place)
}
}
else ->
Log.e("Demo", "Failed to create anchor $anchorResult")
}