Android XR

為有線 XR 眼鏡打造世界錨定體驗

喇叭

關於

講座簡介

Android XR 可讓您突破螢幕空間限制,將應用程式錨定在真實世界。本課程將說明如何使用 Jetpack Compose Glimmer 和 ARCore 的 Geospatial API,打造具備地理空間感知能力的 Android XR 體驗。首先,我們會說明如何使用 ARCore 取得裝置的地理空間姿勢。接著,您將瞭解如何透過視覺定位系統 (VPS),將 Glimmer 的精簡 UI 元件錨定至實際位置。我們會結合這些元素,打造簡單的位置感知空間體驗。

摘要

  • Android XR 可將數位體驗融入實體環境,支援智慧型眼鏡、有線 XR 眼鏡等各種裝置。現有的 2D Android 相容應用程式,只要稍加修改,就能在 XR 頭戴式裝置和有線 XR 眼鏡上運作。
  • Jetpack Compose Glimmer 提供專為光學透視螢幕設計的元件和樣式。內建輔助函式會將品牌顏色調整為適合智慧眼鏡的色調,在各種光線條件下都能保持清晰可見和對比度。
  • Glimmer 程式庫新增了翻頁元素和按鈕群組等新元件。在黑色為透明的穿透式螢幕上,深度 API 有助於區分堆疊元素。
  • 子空間可組合項會將介面置於 3D 空間中,並可透過追蹤目標 (例如追蹤的 QR code) 追蹤 ARCore 錨點。
  • ARCore 中的 Geospatial API 結合街景服務資料和 GPS 測量結果,可在支援的區域準確定位裝置。您可以使用 Places API 查詢中的座標,將 Glimmer 元件附加至真實世界的地理空間錨點。

資源

Gemini Notebook

主題

  • android-xr
  • jetpack-compose
  • 微光
  • arcore
  • geospatial-api
  • augmented-reality

轉錄稿

Android XR 簡介

  • 大家好,歡迎參加 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 眼鏡上運作。

Jetpack XR SDK 和開發人員工具

  • 為了打造出色的 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 Compose Glimmer 建構 UI

  • 接著來談談擴增應用程式。

  • 您可以使用 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。

顯示型眼鏡的 UI 設計原則

  • 測試,好!

  • 我必須按下「下一步」按鈕。

  • 好的,謝謝 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。

Geospatial API 和 Google Maps Platform 整合

  • 好的。

  • 我已示範如何將數位內容放置在附近的真實世界中。

  • 我們回到 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 開發人員高峰會度過愉快時光。

  • 謝謝。

程式碼

程式碼片段

「使用 Jetpack Compose Glimmer 建構 UI」影片 04:39 處的殼層程式碼片段
android skills add display-glasses-with-jetpack-compose-glimmer
「使用 Jetpack Compose Glimmer 建構 UI」影片 05:49 處的 Kotlin 程式碼片段
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 */
   }
}
「顯示型眼鏡的 UI 設計原則」影片 08:37 處的 Kotlin 程式碼片段
val pagerState =
  rememberGlimmerPagerState(pageCount = { 10 })

GlimmerHorizontalPager(pagerState) { page ->
   /* Card Content */
}
「顯示型眼鏡的 UI 設計原則」影片 09:50 處的 Kotlin 程式碼片段
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") }
}
「顯示型眼鏡的 UI 設計原則」影片 10:13 處的 Kotlin 程式碼片段
var buttonChecked by remember { mutableStateOf(false) }

ToggleButton(
  checked = buttonChecked,
  leadingIcon = { Icon(FavoriteIcon) },
  onCheckedChange = { buttonChecked = it }
) {
  Text(if (buttonChecked) "On" else "Off")
}
「Overview of location-based anchors」影片 13:55 處的 Kotlin 程式碼片段
Subspace {
  SpatialPanel {
    VideoPlayer()
  }
}
「Overview of location-based anchors」影片 14:16 處的 Kotlin 程式碼片段
val state by
  qrCode.state.collectAsState()
Subspace(FollowTarget.anchor(anchor)) {
  SpatialPanel {
    Row {
      IndicatorDot()
      Spacer()
      QrCodeCard(state)
    }
  }
}
「Geospatial API 與 Google Maps Platform 整合」影片 16:43 處的 Kotlin 程式碼片段
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)

}
「Geospatial API 和 Google Maps Platform 整合」影片 17:27 處的 Kotlin 程式碼片段
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()
「Geospatial API 和 Google Maps Platform 整合」中 18:09 處的 Kotlin 程式碼片段
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()
 )
}
「Geospatial API 與 Google Maps Platform 整合」影片 18:53 處的 Kotlin 程式碼片段
when (anchorResult) {
 is AnchorCreateSuccess ->
   Subspace(FollowTarget.anchor(anchorResult.anchor)) {
     SpatialPanel(SubspaceModifier.rotateToLookAtUser()) {
       PlaceIndicator(place)
     }
   }

 else ->
   Log.e("Demo", "Failed to create anchor $anchorResult")
}