在 Android 上以 AR 顯示附近地點 (Kotlin)

1. 事前準備

摘要

本程式碼研究室說明如何使用 Google 地圖平台的資料,在 Android 裝置上以擴增實境 (AR) 顯示附近地點。

2344909dd9a52c60.png

必要條件

  • 對使用 Android Studio 進行 Android 開發有基本瞭解
  • 熟悉 Kotlin

課程內容

  • 要求使用者授予裝置攝影機和位置資訊的存取權。
  • 整合 Places API,擷取裝置位置附近的場所。
  • 整合 ARCore,找出水平平面表面,以便使用 Sceneform 將虛擬物件錨定並放置在 3D 空間中。
  • 使用 SensorManager 收集裝置在空間中的位置資訊,並使用 Maps SDK for Android 公用程式庫,將虛擬物件放置在正確的方位。

軟硬體需求

2. 做好準備

Android Studio

本程式碼研究室使用 Android 10.0 (API 級別 29),且您必須在 Android Studio 中安裝 Google Play 服務。如要安裝這兩個依附元件,請完成下列步驟:

  1. 前往 SDK 管理工具,方法是依序點選「Tools」 >「SDK Manager」

6c44a9cb9cf6c236.png

  1. 檢查是否已安裝 Android 10.0。如果沒有,請勾選「Android 10.0 (Q)」旁邊的核取方塊,然後依序點選「OK」和隨即顯示的對話方塊中的「OK」,即可安裝。

368f17a974c75c73.png

  1. 最後,前往「SDK Tools」分頁,勾選「Google Play services」旁邊的核取方塊,然後依序點選「OK」和隨即顯示的對話方塊中的「OK」,即可安裝 Google Play 服務。

497a954b82242f4b.png

必要 API

在下一節的步驟 3 中,請為本程式碼研究室啟用 Maps SDK for AndroidPlaces API

開始使用 Google 地圖平台

如果您從未使用過 Google 地圖平台,請按照「開始使用 Google 地圖平台」指南或「開始使用 Google 地圖平台」播放清單中的操作說明,完成下列步驟:

  1. 建立帳單帳戶。
  2. 建立專案。
  3. 啟用 Google 地圖平台 API 和 SDK (如上一節所列)。
  4. 產生 API 金鑰。

選用:Android Emulator

如果沒有支援 ARCore 的裝置,也可以使用 Android 模擬器模擬 AR 場景,以及模擬裝置的位置。由於您也會在本練習中使用 Sceneform,因此請務必按照「設定模擬器以支援 Sceneform」一節中的步驟操作。

3. 快速入門

為協助您盡快上手,我們提供了一些入門程式碼,方便您跟著本程式碼研究室的說明操作。歡迎直接跳到解決方案,但如要查看所有步驟,請繼續閱讀。

如果已安裝 git,可以複製存放區。

git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git

或者,您也可以點選下方按鈕下載原始碼。

取得程式碼後,請開啟 starter 目錄中的專案。

4. 專案總覽

探索您在上一個步驟中下載的程式碼。在這個存放區中,您應該會找到名為 app 的單一模組,其中包含 com.google.codelabs.findnearbyplacesar套件。

AndroidManifest.xml

AndroidManifest.xml 檔案中會宣告下列屬性,讓您使用本程式碼研究室所需的功能:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature
   android:glEsVersion="0x00030000"
   android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

對於 uses-permission,這項權限會指定使用者必須先授予哪些權限,才能使用這些功能,因此聲明如下:

  • android.permission.INTERNET:這樣應用程式才能執行網路作業,並透過網際網路擷取資料,例如透過 Places API 取得地點資訊。
  • android.permission.CAMERA:必須授予相機存取權,才能使用裝置的相機以擴增實境顯示物件。
  • android.permission.ACCESS_FINE_LOCATION:需要位置資訊存取權,才能根據裝置位置擷取附近的場所。

對於 uses-feature (指定這個應用程式需要的硬體功能),系統會宣告下列項目:

  • 需要 OpenGL ES 3.0 版。
  • 必須使用支援 ARCore 的裝置。

此外,應用程式物件下方會新增下列中繼資料標記:

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  
  <!-- 
     Indicates that this app requires Google Play Services for AR ("AR Required") and causes
     the Google Play Store to download and install Google Play Services for AR along with
     the app. For an "AR Optional" app, specify "optional" instead of "required". 
  -->

  <meta-data
     android:name="com.google.ar.core"
     android:value="required" />

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="@string/google_maps_key" />

  <!-- Additional elements here --> 

</application>

第一個中繼資料項目是指出這個應用程式必須有 ARCore 才能執行,第二個項目則是說明如何將 Google 地圖平台 API 金鑰提供給 Maps SDK for Android。

build.gradle

build.gradle 中指定了下列額外依附元件:

dependencies {
    // Maps & Location
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.maps.android:maps-utils-ktx:1.7.0'

    // ARCore
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.15.0"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.7.1"
    implementation "com.squareup.retrofit2:converter-gson:2.7.1"
}

以下簡要說明各項依附元件:

  • 群組 ID 為 com.google.android.gms 的程式庫 (即 play-services-locationplay-services-maps) 用於存取裝置的位置資訊,以及存取與 Google 地圖相關的功能。
  • com.google.maps.android:maps-utils-ktx 是 Maps SDK for Android 公用程式庫的 Kotlin 擴充功能 (KTX) 程式庫。這個程式庫中的功能稍後會用於在真實空間中放置虛擬物件。
  • com.google.ar.sceneform.ux:sceneform-uxSceneform 程式庫,可讓您算繪逼真的 3D 場景,不必學習 OpenGL。
  • 群組 ID com.squareup.retrofit2 中的依附元件是 Retrofit 依附元件,可讓您快速編寫 HTTP 用戶端,與 Places API 互動。

專案架構

您會看到下列套件和檔案:

  • **api:**這個套件包含用於透過 Retrofit 與 Places API 互動的類別。
  • **ar**:這個套件包含所有與 ARCore 相關的檔案。
  • **model:**這個套件包含單一資料類別 Place,用於封裝 Places API 傳回的單一地點。
  • MainActivity.kt:這是應用程式中包含的單一 Activity,會顯示地圖和攝影機畫面。

5. 設定場景

從擴增實境元件開始,深入瞭解應用程式的核心元件。

MainActivity 包含 SupportMapFragment,用於處理地圖物件的顯示作業,以及 ArFragment 的子類別,用於處理擴增實境場景的顯示作業。PlacesArFragment

擴增實境設定

除了顯示擴增實境場景,如果使用者尚未授予相機權限,PlacesArFragment 也會處理要求權限的程序。您也可以覆寫 getAdditionalPermissions 方法,要求其他權限。由於您也需要位置資訊權限,請指定該權限並覆寫 getAdditionalPermissions 方法:

class PlacesArFragment : ArFragment() {

   override fun getAdditionalPermissions(): Array<String> =
       listOf(Manifest.permission.ACCESS_FINE_LOCATION)
           .toTypedArray()
}

開始執行

請在 Android Studio 中開啟 starter 目錄的架構程式碼。如果您從工具列點選「Run」 >「Run ‘app'」,並將應用程式部署至裝置或模擬器,系統應會先提示您啟用位置資訊和相機權限。按一下「允許」,畫面上就會並排顯示攝影機和地圖畫面,如下所示:

e3e3073d5c86f427.png

偵測平面

使用相機環顧周遭環境時,您可能會發現水平表面上疊加了幾個白點,就像這張圖片中地毯上的白點一樣。

2a9b6ea7dcb2e249.png

這些白點是 ARCore 提供的指引,表示系統已偵測到水平平面。偵測到的平面可讓您建立所謂的「錨點」,以便在真實空間中放置虛擬物件。

如要進一步瞭解 ARCore 和瞭解周遭環境的方式,請參閱基本概念

6. 取得附近地點的資訊

接著,您需要存取及顯示裝置目前的位置,然後使用 Places API 擷取附近的商家。

地圖設定

Google 地圖平台 API 金鑰

您先前已建立 Google 地圖平台 API 金鑰,以便查詢 Places API 及使用 Maps SDK for Android。請開啟 gradle.properties 檔案,並將 "YOUR API KEY HERE" 字串替換為您建立的 API 金鑰。

在地圖上顯示裝置位置

新增 API 金鑰後,請在地圖上新增輔助工具,協助使用者判斷自己在地圖上的相對位置。如要這麼做,請前往 setUpMaps 方法,並在 mapFragment.getMapAsync 呼叫中,將 googleMap.isMyLocationEnabled 設為 true.。這樣一來,地圖上就會顯示藍點。

private fun setUpMaps() {
   mapFragment.getMapAsync { googleMap ->
       googleMap.isMyLocationEnabled = true
       // ...
   }
}

取得目前位置

如要取得裝置的位置資訊,請使用 FusedLocationProviderClient 類別。在 MainActivityonCreate 方法中,我們已取得這個執行個體。如要使用這個物件,請填寫 getCurrentLocation 方法,該方法會接受 lambda 引數,以便將位置資訊傳遞給這個方法的呼叫端。

如要完成這個方法,您可以存取 FusedLocationProviderClient 物件的 lastLocation 屬性,然後新增 addOnSuccessListener,如下所示:

fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    currentLocation = location
    onSuccess(location)
}.addOnFailureListener {
    Log.e(TAG, "Could not get location")
}

系統會從 setUpMaps 方法中擷取附近地點,並在 getMapAsync 中提供的 lambda 內呼叫 getCurrentLocation 方法。

發起地點網路通話

getNearbyPlaces 方法呼叫中,請注意下列參數會傳遞至 placesServices.nearbyPlaces 方法:API 金鑰、裝置位置、半徑 (以公尺為單位,設為 2 公里) 和地點類型 (目前設為 park)。

val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
   apiKey = apiKey,
   location = "${location.latitude},${location.longitude}",
   radiusInMeters = 2000,
   placeType = "park"
)

如要完成網路呼叫,請繼續傳遞您在 gradle.properties 檔案中定義的 API 金鑰。以下程式碼片段定義在 android > defaultConfig 設定下的 build.gradle 檔案中:

android {
   defaultConfig {
       resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
   }
}

這樣一來,系統就會在建構時提供字串資源值 google_maps_key

如要完成網路呼叫,您只需透過 Context 物件上的 getString 讀取這個字串資源即可。

val apiKey = this.getString(R.string.google_maps_key)

7. AR 中的地點

目前為止,您已完成下列事項:

  1. 首次執行應用程式時,向使用者要求攝影機和位置資訊權限
  2. 設定 ARCore,開始追蹤水平平面
  3. 使用 API 金鑰設定 Maps SDK
  4. 取得裝置目前的所在位置資訊
  5. 使用 Places API 擷取附近地點 (特別是公園)

完成這項練習的最後一個步驟,是在擴增實境中放置您要擷取的地點。

瞭解場景

ARCore 會透過裝置的相機偵測每個影像影格中稱為特徵點的有趣且獨特點,藉此瞭解真實世界場景。當這些特徵點叢集並顯示在共同水平平面上 (例如桌子和地板),ARCore 就能將這項特徵提供給應用程式做為水平平面。

如先前所述,ARCore 會在偵測到平面時顯示白點,引導使用者。

2a9b6ea7dcb2e249.png

新增錨點

偵測到平面後,您就能附加名為「錨點」的物件。透過錨點,您可以放置虛擬物件,並確保這些物件在空間中會保持在相同位置。請繼續修改程式碼,在偵測到飛機時附加一個。

setUpAr 中,OnTapArPlaneListener 會附加至 PlacesArFragment。每當在 AR 場景中輕觸平面時,系統就會叫用這個事件監聽器。在這次呼叫中,您可以從監聽器提供的 HitResult 建立 AnchorAnchorNode,如下所示:

arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
   val anchor = hitResult.createAnchor()
   anchorNode = AnchorNode(anchor)
   anchorNode?.setParent(arFragment.arSceneView.scene)
   addPlaces(anchorNode!!)
}

您會在 AnchorNode 中附加子節點物件 (PlaceNode 例項),這些物件位於 addPlaces 方法呼叫處理的場景中。

開始執行

使用上述修改項目執行應用程式,並在偵測到平面之前四處查看。請輕觸代表飛機的白點。完成後,地圖上應該會顯示你附近所有公園的標記。不過,您會發現虛擬物件會固定在建立的錨點上,而不是放置在這些公園在空間中的相對位置。

f93eb87c98a0098d.png

最後一個步驟是使用裝置上的 Maps SDK for Android 公用程式庫SensorManager 修正這個問題。

8. 放置地點

如要在擴增實境中將虛擬地點圖示放置在正確的方位,您需要兩項資訊:

  • 正北方向
  • 北方與各個地點之間的角度

判斷北方

裝置上的位置感應器 (地磁和加速計) 可判斷北方。您可以使用這兩個感應器,收集裝置在空間中的即時位置資訊。如要進一步瞭解位置感應器,請參閱「計算裝置的螢幕方向」。

如要存取這些感應器,您必須先取得 SensorManager,然後在感應器上註冊 SensorEventListener。這些步驟已在 MainActivity 的生命週期方法中完成:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   // ...
   sensorManager = getSystemService()!!
   // ...
}

override fun onResume() {
   super.onResume()
   sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
   sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
}

override fun onPause() {
   super.onPause()
   sensorManager.unregisterListener(this)
}

onSensorChanged 方法中,系統會提供 SensorEvent 物件,其中包含特定感應器資料在一段時間內的變化詳細資料。請在該方法中加入下列程式碼:

override fun onSensorChanged(event: SensorEvent?) {
   if (event == null) {
       return
   }
   if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
       System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
   } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
       System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
   }

   // Update rotation matrix, which is needed to update orientation angles.
   SensorManager.getRotationMatrix(
       rotationMatrix,
       null,
       accelerometerReading,
       magnetometerReading
   )
   SensorManager.getOrientation(rotationMatrix, orientationAngles)
}

上方程式碼會檢查感應器類型,並視類型更新適當的感應器讀數 (加速計或磁力計讀數)。現在可以使用這些感應器讀數,判斷裝置相對於北方的角度值 (即 orientationAngles[0] 的值)。

球形標題

確定北方後,下一步是判斷北方與各個地點之間的角度,然後使用這項資訊,在擴增實境中將地點放置在正確的方位。

如要計算方位,請使用 Maps SDK for Android 公用程式庫,其中包含幾個輔助函式,可透過球面幾何計算距離和方位。詳情請參閱這份程式庫總覽

接著,您將使用公用程式庫中的 sphericalHeading 方法,計算兩個 LatLng 物件之間的方位/方位角。Place.kt 中定義的 getPositionVector 方法需要這項資訊。這個方法最終會傳回 Vector3 物件,然後每個 PlaceNode 都會將這個物件做為 AR 空間中的本機位置。

請將該方法中的標題定義替換為下列內容:

val heading = latLng.sphericalHeading(placeLatLng)

完成後,應該會產生下列方法定義:

fun Place.getPositionVector(azimuth: Float, latLng: LatLng): Vector3 {
   val placeLatLng = this.geometry.location.latLng
   val heading = latLng.sphericalHeading(placeLatLng)
   val r = -2f
   val x = r * sin(azimuth + heading).toFloat()
   val y = 1f
   val z = r * cos(azimuth + heading).toFloat()
   return Vector3(x, y, z)
}

本地位置

在 AR 中正確調整地點方向的最後一個步驟,是在將 PlaceNode 物件新增至場景時,使用 getPositionVector 的結果。請前往 MainActivity 中的 addPlaces,就在設定每個 placeNode 父項的行下方 (placeNode.setParent(anchorNode) 下方)。將 placeNodelocalPosition 設為呼叫 getPositionVector 的結果,如下所示:

val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)

根據預設,方法 getPositionVector 會將節點的 y 距離設為 1 公尺,如 getPositionVector 方法中的 y 值所指定。如要調整這段距離 (例如 2 公尺),請視需要修改該值。

這項異動完成後,新增的 PlaceNode 物件應會以正確的標題方向顯示。現在請執行應用程式,看看結果如何!

9. 恭喜

恭喜你完成目前為止的工作!

瞭解詳情