使用 Android 裝置 (AR) 以 AR 顯示附近地點

1. 事前準備

摘要

本程式碼研究室可教您如何使用 Google 地圖平台在 Android 適用的擴增實境 (AR) 功能顯示附近地點。

2344909dd9a52c60.png

必要條件

  • 對 Android 開發作業有基本瞭解 (使用 Android Studio)
  • 熟悉 Kotlin

您將會瞭解的內容

  • 要求使用者授權存取裝置的相機和位置資訊。
  • 整合 Places API,以擷取裝置位置附近的附近地點。
  • 整合 ARCore 並找出水平平面,以便使用場景將虛擬物體固定在 3D 空間中。
  • 使用 SensorManager 收集有關裝置在空間中的資訊,並使用 Maps SDK for Android 公用程式庫將虛擬物件定位在正確的標題中。

軟硬體需求

2. 做好準備

Android Studio

此程式碼研究室採用 Android 10.0 (API 級別 29),並要求您在 Android Studio 上安裝 Google Play 服務。如要安裝這兩種依附元件,請完成下列步驟:

  1. 前往 SDK 管理工具,方法是按一下 [工具] > [SDK 管理員]

6c44a9cb9cf6c236.png

  1. 檢查是否已安裝 Android 10.0。如果沒有安裝,請勾選 [Android 10.0 (Q)] 旁的核取方塊,再按一下 [確定],然後在隨即顯示的對話方塊中再次點選 [確定] 即可。

368f17a974c75c73.png

  1. 最後,前往 [SDK Tools] 分頁安裝 Google Play 服務,選取 [Google Play 服務] 旁的核取方塊,然後按一下 [確定],然後在顯示的對話方塊中再次選取 [確定]**。

497a954b82242f4b.png

必要的 API

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

開始使用 Google 地圖平台

如果您未曾使用過 Google 地圖平台,請按照開始使用 Google 地圖平台指南或觀看 Google 地圖平台入門指南完成下列步驟:

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

選用:Android 模擬器

如果您沒有 ARCore 支援的裝置,也可選擇使用 Android Emulator 模擬 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:如此一來,您的應用程式就可以透過網路 API 執行網路作業及擷取資料,例如透過 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 Maps Platform 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 程式庫,您不必學習 OpenGL,就能呈現真實的 3D 場景。
  • 群組 ID com.squareup.retrofit2 內的依附元件是 Retrofit 依附元件,可讓您快速撰寫 HTTP 用戶端與 Places API 互動。

專案架構

這裡會顯示下列套件和檔案:

  • **api—**這個套件包含類別,可用來使用 Retrofit 與 Places API 互動。
  • **ar—**這個套件包含所有與 ARCore 相關的檔案。
  • **model—**這個套件包含單一資料類別 Place,用來封裝 Places API 傳回的單一地點。
  • MainActivity.kt - 您的應用程式內包含的單一 Activity,可顯示地圖和相機畫面。

5. 設定場景

深入瞭解擴增應用程式的核心元件。

MainActivity 包含可處理顯示物件的 SupportMapFragment,以及呈現擴增實境場景的 ArFragmentPlacesArFragment 的子類別。

擴增實境設定

如未提供已擴增實境場景,PlacesArFragment 也會處理使用者要求取得相機的權限。您也可以透過覆寫 getAdditionalPermissions 方法要求其他權限。假如您也需要授予位置存取權,請加以指定並覆寫 getAdditionalPermissions 方法:

class PlacesArFragment : ArFragment() {

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

執行

請直接在 Android Studio 的「starter」目錄中開啟空架程式碼。如果您按一下工具列上的 [執行] > [執行「應用程式」圖示],並將應用程式部署至裝置或模擬器,請先提示您啟用位置資訊和相機權限。請按一下 [Allow],然後系統就會以下列方式並排顯示相機視圖和地圖檢視:

e3e3073d5c86f427.png

偵測飛機

透過相機觀察周遭環境時,您或許會發現有兩條白點重疊在水平表面上,就像圖片中的地毯上的白色點一樣。

2a9b6ea7dcb2e249.png

這些白色圓點是 ARCore 提供的指南,指出系統偵測到水平平面。這些偵測到的平面可讓您建立所謂「錨點」的物體,以便在虛擬空間中放置虛擬物件。

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

6. 取得附近地點

接下來,您需要存取並顯示裝置目前的位置,然後使用 Places API 擷取附近的地點。

Google 地圖設定

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")
}

getCurrentLocation 方法是在 getMapAsync 中 brbrda 提供的 setUpMaps 方法中提取的,用於為其提取附近地位。

啟動地點呼叫網路

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

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

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

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

這樣就能在建構期間提供字串資源值 google_maps_key

要完成網絡呼叫,您可以通過 getStringContext 對像上讀取此字符串數據。

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

新增錨點

系統偵測到平面後,您就可以附加名為 anchor 的物件。您可以透過錨點放置虛擬物件,確保這些物件在空間中會維持在相同位置。因此,在偵測到飛機時,請修改要附加的程式碼。

setUpAr 中,OnTapArPlaneListener 附加至 PlacesArFragment。只要輕觸 AR 場景中的飛機,就會叫用這個事件監聽器。在這場呼叫中,您可以透過事件監聽器中的 HitResult 建立 AnchorAnchorNode,如下所示:

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

AnchorNode 是您在 addPlaces 方法呼叫中處理的場景中附加子節點物件 PlaceNode 執行個體。

執行

如果您執行了上述修改的應用程式,請偵測周遭環境,直到偵測到飛機。請直接輕觸代表飛機的白色圓點。這樣,您即可在地圖上查看附近所有公園的標記。不過,如果您發現「虛擬物件」,該物件會固定在已建立的錨點上,且不會根據這些公園所在的空間放置。

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 中正確定位方向的最後一個步驟是使用 getPositionVector 的結果,將 PlaceNode 個物件加到場景中。請直接在 MainActivity 的「placeNode」(下方下方 placeNode.setParent(anchorNode)) 為家長設定的行下方,前往「addPlaces」:將「placeNode」的localPosition設為呼叫 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. 恭喜

恭喜您獲得這樣的成果!

瞭解詳情