Android XR 可讓您突破螢幕空間限制,將應用程式錨定在真實世界。本課程將說明如何使用 Jetpack Compose Glimmer 和 ARCore 的 Geospatial API,打造具備地理空間感知能力的 Android XR 體驗。首先,我們會說明如何使用 ARCore 取得裝置的地理空間姿勢。接著,您將瞭解如何透過視覺定位系統 (VPS),將 Glimmer 的精簡 UI 元件錨定至實際位置。我們會結合這些元素,打造簡單的位置感知空間體驗。
轉錄稿
大家好,歡迎參加 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 眼鏡 (可提供專注、高效率和沉浸式娛樂功能)。
XREAL AURA 是 Android XR 系列的最新裝置,現已開放購買。
這款裝置輕巧強大,可提供熟悉的 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 設計的新子空間可組合函式。
Android 開發人員如果熟悉 Compose 版面配置 (例如使用列和欄建構),就會發現空間 UI 的版面配置相當直覺。
建立空間列後,您就能以宣告方式並排多個空間面板,不必手動計算位置或大小。
使用空間面板或 Orbiter 等子空間可組合函式,即可輕鬆將 2D 元件延伸至這個新的 3D 空間。
當然,子空間可組合函式不只能顯示 2D 內容。
Compose for XR 支援空間影片,以及透過空間外部表面和空間 glTF 模型呈現的 3D 模型。
接著來談談擴增應用程式。
您可以使用 Jetpack Projected 程式庫強化現有行動應用程式,充分運用智慧眼鏡硬體,例如感應器和螢幕。
最後,Jetpack Compose Glimmer 是 UI 工具包,可用於建構專為眼鏡最佳化的 Android XR 體驗。
Jetpack Compose Glimmer 包含樣式和現成元件,可用於建構 XR 體驗。
我們在去年推出 Glimmer,並持續擴充功能,包括可協助您使用 Glimmer 建構顯示型眼鏡應用程式的代理技能。
android skills add display-glasses-with-jetpack-compose-glimmer自推出以來,我們持續傾聽各位的意見。
我們聽說您希望將 Jetpack Compose Glimmer 擴展到智慧眼鏡以外的裝置。
我們很高興宣布,我們已做出調整,能更妥善支援 Glimmer 的空間錨定體驗,稍後會詳細說明。
為了最佳化 Glimmer,我們首先建立新的介面,以便在各種 XR 裝置上顯示,包括螢幕和有線 XR 眼鏡。
新增這個介面後,您就能更靈活地自訂 UI。
此外,我們也為 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 中,您可以使用模擬器,查看眼鏡 UI 在各種背景 (例如明亮、昏暗或雜亂環境) 中的顯示效果。
上述更新只是冰山一角。
好的,沒問題
我的團隊告訴我,這個笑話不會讓人發笑,但我還是堅持要講。
現在,我將交給 Paul,進一步介紹 Jetpack Compose Glimmer。
測試,好!
我必須按下「下一步」按鈕。
好的,謝謝 Yasmine。
現在我們來做個小測驗
正如 Yasmine 所說,Jetpack Compose Glimmer 提供基礎建構模塊,可用於開發顯示型眼鏡專用應用程式。
我們的元件整合了專為光學透視顯示器打造的最佳做法,並直接內建在 API 中,讓所有開發人員都能打造美觀實用的光學透視體驗。
為這類光學透視裝置設計及建構應用程式時,請留意以下幾點。
首先,這是穿透式螢幕,因此不會顯示黑色。
當世界可見時,黑色會變成透明。
因此,您無法仰賴深色背景來維持 UI、文字和內容的對比度。
雖然黑色不可見,但可用來遮蓋其他數位內容。
在 Glimmer UI 架構中,我們提供深度 API,可讓您輕鬆分隔 UI 元素,並提供階層感。
在這裡,您可以看到這疊卡片的深度如何幫助它們保持彼此分離。
我們的視覺科學家提供了文字和圖示的目標對比度,確保在大多數光線條件下都能清楚辨識。
正如 Yasmine 所展示,我們將這項功能建構到架構中,因此當您自訂介面顏色時,可以確信使用者能清楚辨識介面。
我們已將這些和其他最佳做法融入不斷擴充的元件庫。
我們最近推出了許多新元件。
我們最新推出的其中一項功能是翻頁元素元件,內建流暢的動作轉換和手勢。
val pagerState =
rememberGlimmerPagerState(pageCount = { 10 })
GlimmerHorizontalPager(pagerState) { page ->
/* Card Content */
}如果您熟悉 Compose 的水平分頁器,就會對這個元件感到非常熟悉。
我們也推出四種新的資訊卡變化版本,可與翻頁元素、堆疊和其他 Glimmer UI 完美整合。
前置圖片資訊卡和後置圖片資訊卡會並排顯示文字和圖片。
因此,如果文字和視覺資訊量相當,請使用這些資訊卡。
我們已將圖片資訊卡分成獨立的資訊卡變化版本。
如果圖片較大,且只有一行文字或說明文字 (例如這個影片挑選器),最適合使用這個選項。
動作資訊卡變化版本會在資訊卡中嵌入單一按鈕。
如要讓使用者確切知道啟用資訊卡後會發生什麼事,請使用這個方法,例如這個目的地挑選器。
按鈕群組可讓您在狹小的空間中放入多個按鈕。
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")
}您可以在這裡看到我們使用的所有按鈕類型,用於建立這個媒體遙控器。
現在,您可以輕鬆在不同背景上測試 UI。
眼鏡模擬器可讓你放入 360 度相片,並平移畫面,在不同背景 (例如雜亂、明亮、昏暗等) 測試。
按鈕群組非常適合用於這類詳細資料檢視畫面,以顯示關聯動作。
輕觸後,您可以使用共用元素轉換功能,前往詳細檢視器狀態,然後使用系統返回手勢返回上一個檢視畫面。
太棒了。
此外,我們也新增了語音輸入指標和警告對話方塊。
所以,沒錯,有很多新功能。
現在我們已經看過一些新功能,接著就來建構一些內容吧。
除了使用 Glimmer 建構智慧眼鏡,我們也可以使用 Glimmer 建構有線 XR 眼鏡的體驗。
我們希望您能打造擴增周遭世界的體驗,首先就從現實世界中以位置為基礎的錨點開始。
想像一下,你來到一個新城市,可能是為了參加開發人員會議,想在附近找點吃的。
你可以要求常用的導航應用程式尋找附近的餐廳和評分。
資訊可直接疊加在位置上,提供背景資訊、情報和指引。
這裡可以看到最新的 Glimmer 元件,稱為「圖示標記」。
你可以用來突顯實體世界中的重要元素,並與其他 Glimmer 元件 (例如這張資訊卡) 搭配使用。
在這個範例中,系統會使用 Google 地圖、Places 和 Geospatial API 標註搜尋點。
Dereck,我們來示範如何設定。
那就開始吧!
現在我們來做個小測驗
現在我們來做個小測驗
正如 Paul 先前所說,我們開始建構吧。
因此,我們將使用 developer.android.com/xr 上的開發人員說明文件,並查看 Jetpack Compose Glimmer 元件程式庫。
因此,您可以查看 Glimmer 上所有可用元件的視覺總覽。
首先,我們來看看 Paul 稍早展示的這個 UI。
因此,我們會使用 Glimmer UI 建立空間指標和資訊方塊。
首先,我們來找出要使用的元件。
左側會顯示圖示標記,右側則會使用資訊卡說明地點。
因此,如果您前往 Android Studio,實際上就能預覽這個窗格,就像預覽任何其他 Compose 元件一樣,而且可以即時查看變更。
如前所述,您可以在各種背景環境中預覽 UI,確保 UI 在多種情境 (例如深色和淺色環境) 下都有足夠的對比度。
現在畫布已設定完成,我們將建立一個資料列,在其中加入圖示標記和一些間距,以及一張含有範例文字的資訊卡。
這樣就完成了。
如果對 2D 預覽畫面滿意,就可以開始在真實世界中標記內容,我們會透過空間化 UI 來完成這項操作。
也就是說,我們會以 3D 而非 2D 方式呈現。
因此,空間 UI 會包含在子空間中,也就是使用者前方的 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 取得的錨點。
在這個範例中,我使用的是 QR code 辨識功能。
這樣就能讀取及追蹤 QR code,讓內容跟著 QR code 在空間中移動,我們也會附加先前看到的 UI。
好的。
我已示範如何將數位內容放置在附近的真實世界中。
我們回到 Paul 的例子。
我來自荷蘭。
我對山景城不太熟悉。
因此,在本次活動結束後,我想協助你建立一個應用程式,幫助你找到一些有趣的咖啡店。
接著,我們來看看如何使用 Geospatial API,協助使用者找到咖啡廳。這個 API 現在位於 ARCore for Jetpack XR 中。
因此,原本在智慧型手機上推出的 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 地圖平台的 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,直到 Geospatial 啟動並執行。
接著,我們會等待準確度達到一定程度。
因此,當 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 公尺,但您也可以附加至支援的屋頂,也就是說,高度值會根據該位置建築物的屋頂決定。
因此,您也可以使用四元數旋轉錨點,這會指定錨點的旋轉值。
不過,在本範例中,我們實際上會使用預設值,因為我們會讓 UI 旋轉,面向使用者。
接著,我們會將先前的 Glimmer UI 附加到這個錨點,這樣就能建立空間錨定體驗。
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 實驗中汲取靈感,並透過程式碼實驗室著手開發。
我們為大家規劃了許多精彩的 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")
}