Afficher les adresses à proximité en RA sur Android (Kotlin)

1. Avant de commencer

Résumé

Cet atelier de programmation vous explique comment utiliser les données de Google Maps Platform pour afficher les adresses à proximité en réalité augmentée (RA) sur Android.

2344909dd9a52c60.png

Conditions préalables

  • Connaissances de base en développement Android avec Android Studio
  • Bonne connaissance de Kotlin

Points abordés

  • Demander à l'utilisateur l'autorisation d'accéder à la caméra et à la position de l'appareil
  • Intégrer l'API Places pour extraire les adresses à proximité en fonction de la position de l'appareil
  • Intégrer ARCore pour trouver des surfaces planes horizontales permettant d'ancrer les objets virtuels et de les placer dans l'espace 3D à l'aide de Sceneform
  • Collecter des informations sur la position de l'appareil dans l'espace à l'aide de SensorManager et utiliser la bibliothèque d'utilitaires du SDK Maps pour Android afin de positionner les objets virtuels selon le bon cap

Prérequis

2. Configuration

Android Studio

Cet atelier de programmation utilise Android 10.0 (API de niveau 29). Vous devez avoir installé les services Google Play dans Android Studio. Pour installer ces deux dépendances, procédez comme suit :

  1. Accédez à SDK Manager en cliquant sur Tools (Outils) > SDK Manager.

6c44a9cb9cf6c236.png

  1. Vérifiez si Android 10.0 est installé. Si ce n'est pas le cas, installez-le en cochant la case Android 10.0 (Q), cliquez sur OK, puis de nouveau sur OK dans la boîte de dialogue qui s'affiche.

368f17a974c75c73.png

  1. Enfin, installez les services Google Play. Pour ce faire, dans l'onglet SDK Tools, cochez la case Google Play services (Services Google Play), cliquez sur OK, puis à nouveau sur OK dans la boîte de dialogue qui s'affiche.

497a954b82242f4b.png

API requises

À l'étape 3 de la section suivante, activez les options Maps SDK for Android (SDK Maps pour Android) et Places API (API Places) pour cet atelier de programmation.

Premiers pas avec Google Maps Platform

Si vous n'avez jamais utilisé Google Maps Platform, suivez le guide Premiers pas avec Google Maps Platform ou regardez la playlist de démarrage avec Google Maps Platform pour effectuer les étapes suivantes :

  1. Créer un compte de facturation
  2. Créer un projet
  3. Activer les SDK et les API Google Maps Platform (listés dans la section précédente)
  4. Générer une clé API

Facultatif : Android Emulator

Si vous ne possédez pas d'appareil compatible avec ARCore, vous pouvez également utiliser Android Emulator pour simuler une position d'appareil dans une scène en RA. Étant donné que vous utiliserez également Sceneform dans cet exercice, vous devez suivre la procédure "Configurer l'émulateur pour prendre en charge Sceneform".

3. Démarrage rapide

Voici un code de démarrage qui vous permettra de commencer rapidement cet atelier de programmation. Vous pouvez tout à fait passer directement au résultat, mais si vous souhaitez voir toutes les étapes, poursuivez votre lecture.

Vous pouvez cloner le dépôt si vous avez installé git.

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

Vous pouvez également cliquer sur le bouton ci-dessous pour télécharger le code source.

Une fois que vous avez obtenu le code, ouvrez le projet qui se trouve dans le répertoire starter.

4. Présentation du projet

Parcourez le code que vous avez téléchargé à l'étape précédente. Dans ce répertoire, vous devriez trouver un module unique nommé app, qui contient le package com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

Les attributs suivants sont déclarés dans le fichier AndroidManifest.xml pour vous permettre d'utiliser les fonctionnalités requises dans cet atelier de programmation :

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

Pour uses-permission, qui spécifie les autorisations utilisateur nécessaires afin d'utiliser ces fonctionnalités, les éléments suivants sont déclarés :

  • android.permission.INTERNET : cette autorisation permet à votre application d'effectuer des opérations réseau et d'extraire des données via Internet (par exemple, des informations sur les adresses via l'API Places).
  • android.permission.CAMERA : l'accès à la caméra de l'appareil est nécessaire pour pouvoir afficher des objets en réalité augmentée.
  • android.permission.ACCESS_FINE_LOCATION : l'accès à la position est nécessaire pour extraire les adresses à proximité de l'appareil.

Pour uses-feature, qui spécifie les fonctions matérielles requises par cette application, les éléments suivants sont déclarés :

  • OpenGL ES version 3.0 est requis.
  • Un appareil compatible avec ARCore est requis.

De plus, les balises de métadonnées suivantes sont ajoutées sous l'objet d'application :

<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 première entrée "meta-data" indique qu'ARCore est nécessaire pour exécuter cette application. La seconde vous permet de fournir votre clé API Google Maps Platform au SDK Maps pour Android.

build.gradle

Dans build.gradle, les dépendances supplémentaires suivantes sont spécifiées :

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

Voici une brève description de chaque dépendance :

  • Les bibliothèques avec l'ID de groupe com.google.android.gms, à savoir play-services-location et play-services-maps, sont utilisées pour accéder aux informations de position de l'appareil et aux fonctionnalités d'accès associées à Google Maps.
  • com.google.maps.android:maps-utils-ktx correspond à la bibliothèque d'extensions Kotlin (KTX) pour la bibliothèque d'utilitaires du SDK Maps pour Android. Les fonctionnalités seront utilisées dans cette bibliothèque pour, plus tard, positionner des objets virtuels dans un espace réel.
  • com.google.ar.sceneform.ux:sceneform-ux est la bibliothèque Sceneform, qui vous permet d'afficher des scènes 3D réalistes sans avoir à apprendre à utiliser OpenGL.
  • Les dépendances dont l'ID de groupe est com.squareup.retrofit2 sont celles de Retrofit. Elles vous permettent d'écrire rapidement un client HTTP pour interagir avec l'API Places.

Structure du projet

À cet endroit, vous trouverez les packages et fichiers suivants :

  • api : ce package contient des classes qui sont utilisées pour interagir avec l'API Places à l'aide de l'outil Retrofit.
  • ar : ce package contient tous les fichiers associés à ARCore.
  • model : ce package contient une seule classe de données Place, utilisée pour encapsuler une adresse unique telle qu'elle est renvoyée par l'API Places.
  • MainActivity.kt : il s'agit du seul élément Activity contenu dans votre application. Il affichera une carte et une vue de la caméra.

5. Configuration de la scène

Découvrez les principaux composants de l'application en commençant par les sections faisant appel à la réalité augmentée.

MainActivity contient un SupportMapFragment, qui gérera l'affichage de l'objet de carte, et une sous-classe d'un ArFragment (PlacesArFragment) qui gérera l'affichage de la scène en réalité augmentée.

Configuration de la réalité augmentée

En plus d'afficher la scène en réalité augmentée, PlacesArFragment gérera également la demande d'autorisation d'accès à la caméra, si elle n'a pas déjà été obtenue. Il est possible de demander des autorisations supplémentaires en ignorant la méthode getAdditionalPermissions. Étant donné que vous avez également besoin d'une autorisation d'accéder à la position, spécifiez-la et ignorez la méthode getAdditionalPermissions :

class PlacesArFragment : ArFragment() {

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

Exécuter le code

Ouvrez le code de squelette du répertoire starter dans Android Studio. Si vous cliquez sur Run (Exécuter) > Run 'app' (Exécuter l'application) dans la barre d'outils, et que vous déployez l'application sur votre appareil ou votre émulateur, vous devriez d'abord être invité à autoriser l'accès à la position et à la caméra. Cliquez sur Allow (Autoriser). Vous devriez alors voir une vue de la caméra et une vue plan côte à côte comme sur cette image :

e3e3073d5c86f427.png

Détection des surfaces planes

En parcourant l'environnement dans lequel vous vous trouvez avec la caméra, vous remarquerez peut-être quelques points blancs superposés aux surfaces horizontales comme ceux situés sur le tapis dans cette image.

2a9b6ea7dcb2e249.png

Ces points blancs sont des indications fournies par ARCore pour signifier qu'une surface plane horizontale a été détectée. Ces surfaces planes détectées vous permettent de créer ce que l'on appelle une "ancre" pour positionner des objets virtuels dans l'espace.

Pour en savoir plus sur ARCore et sur la façon dont l'outil appréhende l'environnement autour de vous, consultez les concepts fondamentaux.

6. Obtenir les adresses à proximité

Ensuite, vous devrez accéder à la position actuelle de l'appareil et l'afficher, puis extraire les adresses à proximité à l'aide de l'API Places.

Configuration Maps

Clé API Google Maps Platform

Précédemment, vous avez créé une clé API Google Maps Platform pour pouvoir interroger l'API Places et utiliser le SDK Maps pour Android. Ouvrez le fichier gradle.properties et remplacez la chaîne "YOUR API KEY HERE" par la clé API que vous avez créée.

Afficher la position de l'appareil sur la carte

Une fois que vous avez ajouté votre clé API, ajoutez un outil d'aide sur la carte pour que les utilisateurs sachent où ils se trouvent par rapport à la carte. Pour ce faire, accédez à la méthode setUpMaps, puis, dans l'appel mapFragment.getMapAsync, définissez googleMap.isMyLocationEnabled sur true.. Un point bleu s'affichera sur la carte.

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

Obtenir la position actuelle de l'appareil

Pour connaître la position de l'appareil, vous devez utiliser la classe FusedLocationProviderClient. Vous avez déjà obtenu une instance de cette classe dans la méthode onCreate de MainActivity. Pour utiliser cet objet, remplissez la méthode getCurrentLocation, qui accepte un argument lambda afin qu'une position puisse être transmise à l'appelant de cette méthode.

Pour mettre en œuvre cette méthode, vous pouvez accéder à la propriété lastLocation de l'objet FusedLocationProviderClient, puis ajouter un addOnSuccessListener comme suit :

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

La méthode getCurrentLocation est appelée depuis le lambda fourni dans getMapAsync dans la méthode setUpMaps à partir de laquelle les adresses à proximité sont extraites.

Lancer un appel réseau d'adresse

Dans l'appel de méthode getNearbyPlaces, notez que les paramètres suivants sont transmis à la méthode placesServices.nearbyPlaces : une clé API, la position de l'appareil, un rayon en mètres (défini sur 2 km) et un type d'adresse (actuellement défini sur park).

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

Pour terminer l'appel réseau, transmettez la clé API que vous avez définie dans le fichier gradle.properties. L'extrait de code suivant est défini dans votre fichier build.gradle sous la configuration android > defaultConfig :

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

La valeur de la ressource de chaîne google_maps_key sera ainsi disponible au moment de la compilation.

Pour effectuer l'appel réseau, vous pouvez simplement lire cette ressource de chaîne via getString sur l'objet Context.

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

7. Adresses en RA

Jusqu'à présent, vous avez :

  1. demandé à l'utilisateur l'autorisation d'accéder à la caméra et à la position de l'appareil lorsqu'il lance l'application pour la première fois ;
  2. configuré ARCore pour commencer à suivre des surfaces planes horizontales ;
  3. configuré le SDK Maps avec votre clé API ;
  4. obtenu la position actuelle de l'appareil ;
  5. extrait les adresses à proximité (en particulier des parcs) à l'aide de l'API Places.

Pour terminer cet exercice, il vous suffit de positionner les adresses que vous extrayez en réalité augmentée.

Compréhension de la scène

ARCore est en mesure de comprendre la scène réelle à travers l'appareil photo de l'appareil en détectant des points intéressants et distincts (appelés points de caractéristiques) dans chaque image. Lorsque ces points de caractéristiques sont regroupés et semblent se trouver sur une surface plane horizontale commune, comme des tables et des sols, ARCore peut rendre cette caractéristique disponible dans l'application en tant que surface plane horizontale.

Comme vous l'avez vu précédemment, ARCore aide l'utilisateur lorsqu'une surface plane est détectée en affichant des points blancs.

2a9b6ea7dcb2e249.png

Ajouter des ancres

Une fois qu'une surface plane a été détectée, vous pouvez joindre un objet appelé une ancre. Grâce aux ancres, vous pouvez placer des objets virtuels afin de garantir que ceux-ci restent à la même position dans l'espace. Modifiez le code pour joindre une ancre lorsqu'une surface plane est détectée.

Dans setUpAr, un OnTapArPlaneListener est joint à PlacesArFragment. Cet écouteur est appelé chaque fois que l'utilisateur appuie sur une surface plane dans la scène en RA. Dans cet appel, vous pouvez créer une Anchor et un AnchorNode à partir du HitResult fourni dans l'écouteur. Pour ce faire, procédez comme suit :

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

L'AnchorNode correspond à l'endroit où vous allez joindre des objets de nœud enfant (instances PlaceNode) dans la scène gérée dans l'appel de méthode addPlaces.

Exécuter le code

Si vous exécutez l'application avec les modifications ci-dessus, regardez autour de vous jusqu'à ce qu'une surface plane soit détectée. Appuyez sur les points blancs indiquant la présence d'une surface plane. Vous devriez alors voir des repères sur la carte pour tous les parcs les plus proches. Toutefois, vous pouvez remarquer que certains objets virtuels sont bloqués sur l'ancre qui a été créée et ne sont pas placés par rapport à la position de ces parcs dans l'espace.

f93eb87c98a0098d.png

La dernière étape consiste à corriger ce problème à l'aide de la bibliothèque d'utilitaires du SDK Maps pour Android et de SensorManager sur l'appareil.

8. Positionnement des adresses

Pour pouvoir positionner l'icône d'adresse virtuelle en réalité augmentée selon un cap précis, vous avez besoin de deux informations :

  • Où se trouve le Nord géographique
  • L'angle entre le Nord et chaque adresse

Déterminer le Nord

Le Nord peut être déterminé à l'aide des capteurs de position (magnétomètre et accéléromètre) disponibles sur l'appareil. Ces deux capteurs vous permettent de collecter en temps réel des informations sur la position de l'appareil. Pour plus d'informations sur les capteurs de position, consultez l'article sur le calcul de l'orientation de l'appareil.

Pour accéder à ces capteurs, vous devez obtenir un SensorManager, puis enregistrer un SensorEventListener sur ces capteurs. Ces étapes sont déjà effectuées pour vous dans les méthodes du cycle de vie de 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)
}

Dans la méthode onSensorChanged, un objet SensorEvent est fourni. Il contient des détails sur le plan de référence d'un capteur à mesure qu'il change au fil du temps. Ajoutez le code suivant à cette méthode :

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

Le code ci-dessus vérifie le type du capteur et met à jour son relevé selon son type (accéléromètre ou magnétomètre). À partir des relevés de ces capteurs, la valeur du nombre de degrés entre le Nord et l'appareil peut désormais être déterminée (valeur de orientationAngles[0]).

Cap

Maintenant que le Nord est déterminé, l'étape suivante consiste à calculer l'angle entre celui-ci et chaque adresse, puis à utiliser ces informations afin de positionner les adresses correctement en réalité augmentée selon le cap approprié.

Pour calculer le cap, vous allez utiliser la bibliothèque d'utilitaires du SDK Maps pour Android, qui contient quelques fonctions d'assistance pour calculer les distances et les caps par géométrie sphérique. Pour en savoir plus, consultez cette présentation de la bibliothèque.

Vous allez ensuite utiliser la méthode sphericalHeading dans la bibliothèque d'utilitaires. Elle permet de calculer le cap et le relèvement entre deux objets LatLng. Cette information est nécessaire au sein de la méthode getPositionVector définie dans Place.kt. Cette méthode finira par afficher un objet Vector3, qui sera ensuite utilisé par chaque PlaceNode comme position locale dans l'espace de RA.

Remplacez la définition des caps dans cette méthode par ce qui suit :

val heading = latLng.sphericalHeading(placeLatLng)

Cela devrait donner la définition de méthode suivante :

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

Position locale

La dernière étape de la procédure permettant d'orienter correctement les adresses en RA consiste à utiliser le résultat de getPositionVector lorsque des objets PlaceNode sont ajoutés à la scène. Accédez à addPlaces dans MainActivity, juste en dessous de la ligne où le parent est défini sur chaque placeNode (juste au-dessous de placeNode.setParent(anchorNode)). Définissez le localPosition de du placeNode sur le résultat de l'appel de getPositionVector comme suit :

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

Par défaut, la méthode getPositionVector définit la distance Y du nœud sur 1 mètre, comme spécifié par la valeur y de la méthode getPositionVector. Si vous voulez ajuster cette distance (2 mètres et non 1, par exemple), modifiez cette valeur selon vos besoins.

Après cette modification, les objets PlaceNode ajoutés devraient maintenant être orientés selon le bon cap. Vous pouvez maintenant exécuter l'application pour afficher le résultat.

9. Félicitations

Vous êtes arrivés à la fin, félicitations !

En savoir plus