Mostra i luoghi nelle vicinanze in AR su Android (Kotlin)

1. Prima di iniziare

Abstract

Questo codelab ti insegna a utilizzare i dati di Google Maps Platform per visualizzare i luoghi nelle vicinanze in realtà aumentata (AR) su Android.

2344909dd9a52c60.png

Prerequisiti

  • Conoscenza di base dello sviluppo per Android utilizzando Android Studio
  • Familiarità con Kotlin

Cosa imparerai a fare

  • Richiedi all'utente l'autorizzazione per accedere alla fotocamera e alla posizione del dispositivo.
  • Esegui l'integrazione con l'API Places per recuperare i luoghi nelle vicinanze della posizione del dispositivo.
  • Esegui l'integrazione con ARCore per trovare superfici piane orizzontali in modo che gli oggetti virtuali possano essere ancorati e posizionati nello spazio 3D utilizzando Sceneform.
  • Raccogli informazioni sulla posizione del dispositivo nello spazio utilizzando SensorManager e utilizza la libreria di utilità di Maps SDK for Android per posizionare gli oggetti virtuali nella direzione corretta.

Che cosa ti serve

2. Configurazione

Android Studio

Questo codelab utilizza Android 10.0 (livello API 29) e richiede l'installazione di Google Play Services in Android Studio. Per installare entrambe queste dipendenze, completa i seguenti passaggi:

  1. Vai a SDK Manager, a cui puoi accedere facendo clic su Strumenti > SDK Manager.

6c44a9cb9cf6c236.png

  1. Verifica che sia installato Android 10.0. In caso contrario, installalo selezionando la casella di controllo accanto ad Android 10.0 (Q), poi fai clic su Ok e infine di nuovo su Ok nella finestra di dialogo visualizzata.

368f17a974c75c73.png

  1. Infine, installa Google Play Services andando alla scheda SDK Tools, seleziona la casella di controllo accanto a Google Play Services, fai clic su Ok e poi di nuovo su Ok nella finestra di dialogo visualizzata**.**

497a954b82242f4b.png

API richieste

Nel passaggio 3 della sezione seguente, abilita Maps SDK for Android e Places API per questo codelab.

Inizia a utilizzare Google Maps Platform

Se non hai mai utilizzato Google Maps Platform, segui la guida Inizia a utilizzare Google Maps Platform o guarda la playlist Inizia a utilizzare Google Maps Platform per completare i seguenti passaggi:

  1. Crea un account di fatturazione.
  2. Creare un progetto.
  3. Abilita le API e gli SDK di Google Maps Platform (elencati nella sezione precedente).
  4. Genera una chiave API.

(Facoltativo) Emulatore Android

Se non hai un dispositivo supportato da ARCore, puoi utilizzare in alternativa l'emulatore Android per simulare una scena AR e falsificare la posizione del dispositivo. Dato che in questo esercizio utilizzerai anche Sceneform, dovrai anche assicurarti di seguire i passaggi descritti in "Configurare l'emulatore per supportare Sceneform".

3. Avvio rapido

Per iniziare il più rapidamente possibile, ecco un codice iniziale che ti aiuterà a seguire questo codelab. Puoi passare direttamente alla soluzione, ma se vuoi vedere tutti i passaggi, continua a leggere.

Puoi clonare il repository se hai installato git.

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

In alternativa, puoi fare clic sul pulsante qui sotto per scaricare il codice sorgente.

Una volta ottenuto il codice, apri il progetto che si trova nella directory starter.

4. Panoramica del progetto

Esplora il codice scaricato dal passaggio precedente. All'interno di questo repository dovresti trovare un singolo modulo denominato app, che contiene il pacchetto com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

Nel file AndroidManifest.xml sono dichiarati i seguenti attributi per consentirti di utilizzare le funzionalità richieste in questo codelab:

<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" />

Per uses-permission, che specifica quali autorizzazioni devono essere concesse dall'utente prima che queste funzionalità possano essere utilizzate, vengono dichiarate le seguenti:

  • android.permission.INTERNET: in modo che la tua app possa eseguire operazioni di rete e recuperare dati su internet, ad esempio informazioni sui luoghi tramite l'API Places.
  • android.permission.CAMERA: è necessario l'accesso alla fotocamera per poterla utilizzare per visualizzare oggetti in realtà aumentata.
  • android.permission.ACCESS_FINE_LOCATION: l'accesso alla posizione è necessario per recuperare i luoghi nelle vicinanze rispetto alla posizione del dispositivo.

Per uses-feature, che specifica le funzionalità hardware necessarie per questa app, vengono dichiarate le seguenti:

  • È richiesta la versione 3.0 di OpenGL ES.
  • È necessario un dispositivo compatibile con ARCore.

Inoltre, vengono aggiunti i seguenti tag di metadati all'oggetto applicazione:

<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>

La prima voce di metadati indica che ARCore è un requisito per l'esecuzione di questa app, mentre la seconda indica come fornire la chiave API Google Maps Platform a Maps SDK for Android.

build.gradle

In build.gradle vengono specificate le seguenti dipendenze aggiuntive:

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

Ecco una breve descrizione di ogni dipendenza:

  • Le librerie con l'ID gruppo com.google.android.gms, ovvero play-services-location e play-services-maps, vengono utilizzate per accedere alle informazioni sulla posizione del dispositivo e alle funzionalità relative a Google Maps.
  • com.google.maps.android:maps-utils-ktx è la libreria di estensioni Kotlin (KTX) per la libreria di utilità di Maps SDK for Android. La funzionalità verrà utilizzata in questa libreria per posizionare in un secondo momento gli oggetti virtuali nello spazio reale.
  • com.google.ar.sceneform.ux:sceneform-ux è la libreria Sceneform, che ti consentirà di eseguire il rendering di scene 3D realistiche senza dover imparare OpenGL.
  • Le dipendenze all'interno dell'ID gruppo com.squareup.retrofit2 sono le dipendenze Retrofit, che ti consentono di scrivere rapidamente un client HTTP per interagire con l'API Places.

Struttura del progetto

Qui troverai i seguenti pacchetti e file:

  • **api**: questo pacchetto contiene classi utilizzate per interagire con l'API Places utilizzando Retrofit.
  • **ar**: questo pacchetto contiene tutti i file correlati ad ARCore.
  • **model**: questo pacchetto contiene una singola classe di dati Place, utilizzata per incapsulare un singolo luogo restituito dall'API Places.
  • MainActivity.kt: è l'unica Activity contenuta nella tua app, che mostrerà una mappa e una visualizzazione della videocamera.

5. Configurazione della scena

Esplora i componenti principali dell'app a partire dagli elementi di realtà aumentata.

MainActivity contiene un SupportMapFragment, che gestirà la visualizzazione dell'oggetto mappa, e una sottoclasse di un ArFragmentPlacesArFragment—che gestisce la visualizzazione della scena di realtà aumentata.

Configurazione della realtà aumentata

Oltre a visualizzare la scena di realtà aumentata, PlacesArFragment gestirà anche la richiesta di autorizzazione per l'utilizzo della fotocamera da parte dell'utente, se non è già stata concessa. È possibile richiedere autorizzazioni aggiuntive anche eseguendo l'override del metodo getAdditionalPermissions. Dato che devi anche concedere l'autorizzazione di accesso alla posizione, specifica questa autorizzazione e sostituisci il metodo getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Run it

Apri il codice di base nella directory starter in Android Studio. Se fai clic su Esegui > Esegui "app" dalla barra degli strumenti e implementi l'app sul tuo dispositivo o emulatore, ti verrà chiesto prima di attivare l'autorizzazione per la posizione e la fotocamera. Fai clic su Consenti e dovresti visualizzare una visualizzazione della videocamera e una della mappa affiancate, come in questo esempio:

e3e3073d5c86f427.png

Rilevamento di aerei

Quando guardi l'ambiente in cui ti trovi con la fotocamera, potresti notare un paio di punti bianchi sovrapposti alle superfici orizzontali, un po' come i punti bianchi sul tappeto in questa immagine.

2a9b6ea7dcb2e249.png

Questi punti bianchi sono linee guida fornite da ARCore per indicare che è stato rilevato un piano orizzontale. Questi piani rilevati ti consentono di creare un "ancoraggio" per posizionare gli oggetti virtuali nello spazio reale.

Per saperne di più su ARCore e su come comprende l'ambiente circostante, leggi i suoi concetti fondamentali.

6. Trovare luoghi nelle vicinanze

Successivamente, dovrai accedere alla posizione attuale del dispositivo e visualizzarla, per poi recuperare i luoghi nelle vicinanze utilizzando l'API Places.

Configurazione di Maps

Chiave API Google Maps Platform

In precedenza, hai creato una chiave API di Google Maps Platform per abilitare le query dell'API Places e per poter utilizzare Maps SDK for Android. Apri il file gradle.properties e sostituisci la stringa "YOUR API KEY HERE" con la chiave API che hai creato.

Visualizzare la posizione del dispositivo sulla mappa

Dopo aver aggiunto la chiave API, aggiungi un indicatore sulla mappa per aiutare gli utenti a orientarsi rispetto alla mappa. Per farlo, vai al metodo setUpMaps e, all'interno della chiamata mapFragment.getMapAsync, imposta googleMap.isMyLocationEnabled su true.. In questo modo, il punto blu verrà visualizzato sulla mappa.

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

Ottenere la posizione attuale

Per ottenere la posizione del dispositivo, dovrai utilizzare la classe FusedLocationProviderClient. L'ottenimento di un'istanza è già stato eseguito nel metodo onCreate di MainActivity. Per utilizzare questo oggetto, compila il metodo getCurrentLocation, che accetta un argomento lambda in modo che una posizione possa essere passata al chiamante di questo metodo.

Per completare questo metodo, puoi accedere alla proprietà lastLocation dell'oggetto FusedLocationProviderClient e aggiungere un addOnSuccessListener nel seguente modo:

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

Il metodo getCurrentLocation viene chiamato dall'interno della lambda fornita in getMapAsync nel metodo setUpMaps da cui vengono recuperati i luoghi nelle vicinanze.

Avviare una chiamata di rete di luoghi

Nella chiamata al metodo getNearbyPlaces, tieni presente che i seguenti parametri vengono passati al metodo placesServices.nearbyPlaces: una chiave API, la posizione del dispositivo, un raggio in metri (impostato su 2 km) e un tipo di luogo (attualmente impostato su park).

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

Per completare la chiamata di rete, inserisci la chiave API definita nel file gradle.properties. Il seguente snippet di codice è definito nel file build.gradle nella configurazione android > defaultConfig:

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

In questo modo, il valore della risorsa stringa google_maps_key sarà disponibile al momento della compilazione.

Per completare la chiamata di rete, puoi semplicemente leggere questa risorsa stringa tramite getString sull'oggetto Context.

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

7. Luoghi in AR

Finora hai eseguito le seguenti operazioni:

  1. Richiesta all'utente delle autorizzazioni per la videocamera e la posizione al primo avvio dell'app
  2. Configurare ARCore per iniziare a monitorare i piani orizzontali
  3. Configurare l'SDK Maps con la chiave API
  4. Ha ottenuto la posizione corrente del dispositivo
  5. Recupero di luoghi nelle vicinanze (in particolare parchi) utilizzando l'API Places

Il passaggio rimanente per completare questo esercizio consiste nel posizionare i luoghi che stai recuperando nella realtà aumentata.

Comprensione della scena

ARCore è in grado di comprendere la scena del mondo reale attraverso la fotocamera del dispositivo rilevando punti interessanti e distinti chiamati punti caratteristici in ogni frame dell'immagine. Quando questi punti caratteristici sono raggruppati e sembrano trovarsi su un piano orizzontale comune, come tavoli e pavimenti, ARCore può rendere questa funzionalità disponibile per l'app come piano orizzontale.

Come hai visto in precedenza, ARCore aiuta a guidare l'utente quando viene rilevato un piano visualizzando dei punti bianchi.

2a9b6ea7dcb2e249.png

Aggiungere ancore

Una volta rilevato un aereo, puoi collegare un oggetto chiamato ancora. Tramite un ancoraggio, puoi posizionare oggetti virtuali e garantire che rimangano nella stessa posizione nello spazio. Modifica il codice per allegarne una una volta rilevato un aereo.

In setUpAr, un OnTapArPlaneListener è collegato a PlacesArFragment. Questo listener viene richiamato ogni volta che viene toccato un aereo nella scena AR. All'interno di questa chiamata, puoi creare un Anchor e un AnchorNode dal HitResult fornito nel listener nel seguente modo:

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

AnchorNode è il punto in cui collegherai gli oggetti nodo secondario, ovvero le istanze PlaceNode, nella scena gestita nella chiamata al metodo addPlaces.

Esegui

Se esegui l'app con le modifiche sopra indicate, guardati intorno finché non viene rilevato un aereo. Procedi e tocca i punti bianchi che indicano un aereo. A questo punto, dovresti visualizzare sulla mappa i segnaposto di tutti i parchi più vicini. Tuttavia, noterai che gli oggetti virtuali sono bloccati sull'ancora creata e non sono posizionati rispetto alla posizione dei parchi nello spazio.

f93eb87c98a0098d.png

Nell'ultimo passaggio, correggerai questo problema utilizzando la libreria di utilità dell'SDK Maps per Android e SensorManager sul dispositivo.

8. Posizionamento dei luoghi

Per poter posizionare l'icona del luogo virtuale in realtà aumentata con una direzione precisa, hai bisogno di due informazioni:

  • Dove si trova il nord geografico
  • L'angolo tra il nord e ogni luogo

Determinare il nord

Il nord può essere determinato utilizzando i sensori di posizione (geomagnetico e accelerometro) disponibili sul dispositivo. Utilizzando questi due sensori, puoi raccogliere informazioni in tempo reale sulla posizione del dispositivo nello spazio. Per saperne di più sui sensori di posizione, leggi Calcolare l'orientamento del dispositivo.

Per accedere a questi sensori, devi ottenere un SensorManager e poi registrare un SensorEventListener su questi sensori. Questi passaggi sono già stati eseguiti per te nei metodi del ciclo di vita di 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)
}

Nel metodo onSensorChanged viene fornito un oggetto SensorEvent, che contiene i dettagli del dato di un determinato sensore man mano che cambia nel tempo. Aggiungi il seguente codice a questo metodo:

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

Il codice riportato sopra controlla il tipo di sensore e, a seconda del tipo, aggiorna la lettura del sensore appropriata (accelerometro o magnetometro). Utilizzando queste letture del sensore, ora è possibile determinare il valore di quanti gradi a nord rispetto al dispositivo (ovvero il valore di orientationAngles[0]).

Intestazione sferica

Ora che è stato determinato il nord, il passaggio successivo consiste nel determinare l'angolo tra il nord e ogni luogo, quindi utilizzare queste informazioni per posizionare i luoghi nella direzione corretta in realtà aumentata.

Per calcolare la direzione, utilizzerai la libreria di utilità di Maps SDK for Android, che contiene una serie di funzioni di assistenza per il calcolo di distanze e direzioni tramite la geometria sferica. Per ulteriori informazioni, leggi questa panoramica della libreria.

A questo punto, utilizzerai il metodo sphericalHeading nella libreria di utilità, che calcola la direzione/il rilevamento tra due oggetti LatLng. Queste informazioni sono necessarie all'interno del metodo getPositionVector definito in Place.kt. Questo metodo restituirà infine un oggetto Vector3, che verrà poi utilizzato da ogni PlaceNode come posizione locale nello spazio AR.

Procedi e sostituisci la definizione dell'intestazione in questo metodo con la seguente:

val heading = latLng.sphericalHeading(placeLatLng)

In questo modo, dovresti ottenere la seguente definizione del metodo:

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

Posizione locale

L'ultimo passaggio per orientare correttamente i luoghi in AR consiste nell'utilizzare il risultato di getPositionVector quando gli oggetti PlaceNode vengono aggiunti alla scena. Vai a addPlaces in MainActivity, subito sotto la riga in cui è impostato il genitore per ogni placeNode (subito sotto placeNode.setParent(anchorNode)). Imposta localPosition di placeNode sul risultato della chiamata a getPositionVector come segue:

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

Per impostazione predefinita, il metodo getPositionVector imposta la distanza y del nodo su 1 metro, come specificato dal valore y nel metodo getPositionVector. Se vuoi regolare questa distanza, ad esempio a 2 metri, modifica il valore in base alle tue esigenze.

Con questa modifica, gli oggetti PlaceNode aggiunti ora dovrebbero essere orientati nella direzione corretta. Ora esegui l'app per vedere il risultato.

9. Complimenti

Congratulazioni per aver raggiunto questo traguardo.

Scopri di più