Visualizza luoghi nelle vicinanze in AR su Android (Kotlin)

1. Prima di iniziare

Abstract

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

2344909gg9a52c60.png

Prerequisiti

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

Cosa imparerai a fare:

  • Richiedi all'utente l'autorizzazione ad accedere alla fotocamera e alla posizione del dispositivo.
  • Esegui l'integrazione con l'API Places per recuperare i luoghi nelle vicinanze intorno alla posizione del dispositivo.
  • Esegui l'integrazione con ARCore per trovare superfici piane orizzontali in modo da poter ancorare e posizionare gli oggetti virtuali in uno spazio 3D utilizzando Sceneform.
  • Raccogli informazioni sulla posizione del dispositivo nello spazio utilizzando SensorManager e utilizza la Libreria Maps SDK for Android Utility per posizionare gli oggetti virtuali nell'intestazione corretta.

Che cosa ti serve

2. Configura

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 le dipendenze, completa i seguenti passaggi:

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

6c44a9cb9cf6c236.png

  1. Controlla se Android 10.0 è installato. In caso contrario, installala selezionando la casella di controllo accanto ad Android 10.0 (Q), fai clic su OK e infine su OK nella finestra di dialogo visualizzata.

368f17a974c75c73.png

  1. Infine, installa Google Play Services accedendo alla scheda Strumenti SDK, seleziona la casella di controllo accanto a Google Play Services, fai clic su OK, quindi seleziona nuovamente OK nella finestra di dialogo visualizzata**.

497a954b82242f4b.png

API obbligatorie

Nel passaggio 3 della sezione seguente, attiva l'SDK Maps per Android e l'API Places per questo codelab.

Inizia a utilizzare Google Maps Platform

Se non hai mai utilizzato Google Maps Platform, segui la guida introduttiva a Google Maps Platform o guarda la playlist Introduzione a Google Maps Platform per completare la seguente procedura:

  1. Crea un account di fatturazione.
  2. Crea 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, in alternativa puoi utilizzare l'emulatore Android per simulare una scena AR e simulare la posizione del tuo dispositivo. Dato che utilizzerai anche Sceneform in questo esercizio, dovrai anche assicurarti di seguire i passaggi descritti nella sezione "Configurare l'emulatore per supportare Sceneform".

3. Avvio rapido

Per iniziare il più rapidamente possibile, ecco un codice iniziale per aiutarti a seguire questo codelab. Ti diamo il benvenuto nella 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 di seguito per scaricare il codice sorgente.

Dopo avere ottenuto il codice, apri il progetto che si trova all'interno della directory starter.

4. Panoramica del progetto

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

AndroidManifest.xml

I seguenti attributi sono dichiarati nel file AndroidManifest.xml 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 tali funzionalità possano essere utilizzate, vengono dichiarate le seguenti autorizzazioni:

  • android.permission.INTERNET, in modo che la tua app possa effettuare 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 utilizzare la fotocamera del dispositivo per visualizzare oggetti in realtà aumentata.
  • android.permission.ACCESS_FINE_LOCATION: è necessario l'accesso alla posizione per poter recuperare luoghi nelle vicinanze in relazione alla posizione del dispositivo.

Per uses-feature, che specifica quali funzionalità hardware sono richieste dall'app, vengono dichiarate le seguenti informazioni:

  • È necessaria OpenGL ES versione 3.0.
  • È necessario un dispositivo che supporta ARCore.

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

<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 consiste nell'indicare che ARCore è un requisito per l'esecuzione di questa app, mentre la seconda è come fornisci la chiave API di Google Maps Platform all'SDK di Maps per Android.

build.gradle

In build.gradle sono 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 ciascuna dipendenza:

  • Le biblioteche con 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 Kotlin (KTX) della Maps SDK for Android Utility Library. La funzionalità verrà utilizzata in questa libreria per posizionare gli oggetti virtuali in un secondo momento nello spazio reale.
  • com.google.ar.sceneform.ux:sceneform-ux è la libreria Sceneform, che ti consente di eseguire il rendering di scene 3D realistiche senza dover imparare a utilizzare 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 mediante Retrofit.
  • **ar—**questo pacchetto contiene tutti i file correlati ad ARCore.
  • **model:**questo pacchetto contiene un'unica classe di dati Place, che viene utilizzata per incapsulare un singolo luogo restituito dall'API Places.
  • MainActivity.kt: il singolo Activity contenuto nella tua app, che mostrerà una mappa e la visualizzazione di una fotocamera.

5. Configurazione della scena

Esplora i componenti fondamentali dell'app partendo dagli elementi in realtà aumentata.

MainActivity contiene un SupportMapFragment, che gestisce la visualizzazione dell'oggetto mappa e una sottoclasse di un elemento ArFragment-PlacesArFragment, che gestisce la visualizzazione della scena della realtà aumentata.

Configurazione della realtà aumentata

Oltre a mostrare la scena della realtà aumentata, PlacesArFragment gestirà anche la richiesta di autorizzazione della fotocamera da parte dell'utente se non è già stata concessa. È inoltre possibile richiedere autorizzazioni aggiuntive eseguendo l'override del metodo getAdditionalPermissions. Poiché ti occorre anche l'autorizzazione di accesso alla posizione, specificala e sostituisci il metodo getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Esegui

Procedi e apri il codice di scheletro nella directory starter in Android Studio. Se fai clic su Esegui > Esegui "app' dalla barra degli strumenti ed esegui il deployment dell'app sul tuo dispositivo o emulatore, ti dovrebbe essere prima chiesto di attivare l'autorizzazione di accesso alla posizione e alla fotocamera. Quindi, fai clic su Consenti e, una volta eseguita l'operazione, dovresti vedere una visualizzazione della fotocamera e una affiancata della seguente mappa:

e3e.3073d5c86f427.png

Rilevamento dei piani

Osservando l'ambiente circostante con la videocamera, potresti notare un paio di punti bianchi sovrapposti sulle superfici orizzontali, 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 permettono di creare un "anchor" in modo da poter posizionare gli oggetti virtuali nello spazio reale.

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

6. Trova luoghi nelle vicinanze

A questo punto, dovrai accedere alla posizione corrente del dispositivo e visualizzarla e recuperarla utilizzando l'API Places.

Configurazione di Maps

Chiave API di Google Maps Platform

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

Visualizza la posizione del dispositivo sulla mappa

Una volta aggiunta la chiave API, aggiungi un helper sulla mappa per aiutare gli utenti a orientarsi in relazione alla mappa. A tale scopo, vai al metodo setUpMaps e, nella chiamata di mapFragment.getMapAsync, imposta googleMap.isMyLocationEnabled su true.. Il punto blu sulla mappa verrà visualizzato.

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

Ottieni la posizione attuale

Per ottenere la posizione del dispositivo, dovrai utilizzare la classe FusedLocationProviderClient. L'istanza di questo problema è già stata eseguita nel metodo onCreate di MainActivity. Per utilizzare questo oggetto, compila il metodo getCurrentLocation, che accetta un argomento lambda in modo che un indirizzo possa essere trasmesso a un chiamante di questo metodo.

Per completare questo metodo, puoi accedere alla proprietà lastLocation dell'oggetto FusedLocationProviderClient seguito da un addOnSuccessListener come segue:

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

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

Avvia chiamata di rete luoghi

Nella chiamata del metodo getNearbyPlaces, tieni presente che i seguenti parametri vengono passati al metodo placesServices.nearbyPlaces, ovvero 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, procedi e trasmetti la chiave API che hai definito nel tuo 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 della stringa google_maps_key sarà disponibile al momento della creazione.

Per completare la chiamata di rete, puoi semplicemente leggere questa risorsa di 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. Autorizzazioni fotocamera e posizione richieste all'utente la prima volta che viene eseguita l'app
  2. Configura ARCore per iniziare a monitorare i piani orizzontali
  3. Configura l'SDK di Maps con la chiave API
  4. Ha rilevato la posizione corrente del dispositivo
  5. Luoghi nelle vicinanze recuperati (in particolare parchi) mediante l'API Places

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

Comprensione della scena

ARCore è in grado di comprendere la scena reale attraverso la fotocamera del dispositivo rilevando punti interessanti e distinti, chiamati punti funzione, in ogni frame dell'immagine. Quando questi punti di funzionalità vengono raggruppati in base a un piano orizzontale comune, ad esempio tabelle e piani, ARCore può rendere disponibile questa funzionalità all'app come piano orizzontale.

Come hai visto in precedenza, ARCore aiuta l'utente a rilevare un aereo mostrando dei punti bianchi.

2a9b6ea7dcb2e249.png

Aggiungere ancoraggi

Dopo aver rilevato un piano, puoi allegare un oggetto chiamato ancoraggio. Tramite un ancoraggio, puoi posizionare oggetti virtuali e garantire che sembrino rimanere nella stessa posizione nello spazio. Modifica il codice per allegarne uno dopo il rilevamento di un aereo.

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

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

AnchorNode è il punto in cui collegherai oggetti nodo secondari (istanze PlaceNode) nella scena gestita nella chiamata al metodo addPlaces.

Esegui

Se esegui l'app con le modifiche sopra indicate, guardati intorno fino a quando non viene rilevato un aereo. Procedi a toccare i punti bianchi che indicano un aereo. In questo modo, dovresti vedere sulla mappa gli indicatori relativi a tutti i parchi più vicini. Tuttavia, se noterai che gli oggetti virtuali sono bloccati sull'ancoraggio creato e non posizionati rispetto a dove si trovano questi parchi nello spazio.

f93eb87c98a0098d.png

Per l'ultimo passaggio, devi correggerlo utilizzando l'SDK Maps per Android e la funzionalità SensorManager del dispositivo.

8. Posizione dei luoghi

Per poter posizionare l'icona del luogo virtuale in realtà aumentata in un'intestazione precisa, avrai bisogno di due informazioni:

  • Dove il nord è vero
  • L'angolo tra nord e ogni luogo

Determinare il nord

Il nord può essere determinato utilizzando i sensori di posizione (geomagnetici e accelerometro) disponibili sul dispositivo. Utilizzando questi due sensori, puoi raccogliere informazioni in tempo reale sulla posizione del dispositivo nello spazio. Per ulteriori informazioni sui sensori di posizione, consulta l'articolo Eseguire il calcolo dell'orientamento del dispositivo.

Per accedere a questi sensori, dovrai ottenere un SensorManager seguito da un SensorEventListener registrato su questi sensori. Questi passaggi sono già stati eseguiti nei metodi di 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 sull'origine di un determinato sensore quando cambia nel tempo. Aggiungi il seguente codice al 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 consente di verificare il tipo di sensore e, a seconda del tipo, aggiornerà la lettura del sensore appropriata (la lettura dell'accelerometro o del magnetometro). Utilizzando queste letture del sensore, è possibile determinare il valore di quanti gradi rispetto al nord rispetto al dispositivo (ovvero il valore di orientationAngles[0]).

Intestazione sferica

Ora che è stato determinato il nord, il passaggio successivo è determinare l'angolo tra il nord e ogni luogo seguito, utilizzando queste informazioni, per posizionare i luoghi nell'intestazione corretta nella realtà aumentata.

Per calcolare l'intestazione, utilizzerai l'SDK Maps per Android Utility Library, che contiene una serie di funzioni helper per il calcolo di distanze e intestazioni tramite la geometria sferica. Per ulteriori informazioni, leggi questa panoramica della libreria.

Successivamente, utilizzerai il metodo sphericalHeading nella libreria di utilità, che calcola l'intestazione/cuscinetto tra due oggetti LatLng. Queste informazioni sono necessarie all'interno del metodo getPositionVector definito in Place.kt. Questo metodo restituirà un oggetto Vector3, che sarà poi utilizzato da ogni PlaceNode come posizione locale nello spazio AR.

Sostituisci la definizione dell'intestazione in questo metodo con il seguente:

val heading = latLng.sphericalHeading(placeLatLng)

Tale operazione dovrebbe comportare 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 è utilizzare il risultato di getPositionVector quando vengono aggiunti oggetti PlaceNode alla scena. Procedi su addPlaces in MainActivity, subito sotto la riga in cui è impostato il genitore su ogni placeNode (proprio sotto placeNode.setParent(anchorNode)). Imposta il localPosition di placeNode sul risultato di chiamare getPositionVector in questo modo:

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 a 1 metro come specificato dal valore y nel metodo getPositionVector. Se vuoi regolare questa distanza, ad esempio su 2 metri, vai avanti e modifica il valore in base alle tue esigenze.

Con questa modifica, gli oggetti PlaceNode aggiunti ora dovrebbero essere orientati nell'intestazione corretta. Ora procedi ed esegui l'app per vedere il risultato.

9. Complimenti

Congratulazioni per aver raggiunto questa destinazione!

Scopri di più