Muestra lugares cercanos en RA en Android (Kotlin)

1. Antes de comenzar

Resumen

En este codelab, aprenderás a usar datos de Google Maps Platform para mostrar lugares cercanos en realidad aumentada (RA) en Android.

2344909dd9a52c60.png

Requisitos previos

  • Conocimientos básicos sobre el desarrollo para Android mediante Android Studio
  • Conocimientos de Kotlin

Qué aprenderás

  • Solicitarás permiso al usuario para acceder a la cámara y la ubicación del dispositivo.
  • Integrarás la API de Places para recuperar los lugares cercanos alrededor de la ubicación del dispositivo.
  • Integrarás ARCore para encontrar superficies de planos horizontales a fin de que los objetos virtuales se puedan anclar y colocar en un espacio 3D mediante Sceneform.
  • Recopilarás información sobre la posición del dispositivo en el espacio usando SensorManager y usarás la Biblioteca de utilidades del SDK de Maps para Android a fin de posicionar los objetos virtuales con la orientación correcta.

Otros requisitos

2. Prepárate

Android Studio

En este codelab, se usa Android 10.0 (nivel de API 29) y requiere que instales los Servicios de Google Play en Android Studio. Para instalar ambas dependencias, completa los pasos siguientes:

  1. Accede a SDK Manager. Para ello, haz clic en Herramientas (Tools) > SDK Manager.

6c44a9cb9cf6c236.png

  1. Comprueba si Android 10.0 está instalado. De no ser así, selecciona la casilla de verificación junto a Android 10.0 (Q) para instalarlo. Luego, haz clic en Aceptar (OK) y, por último, vuelve a hacer clic en Aceptar (OK) en el cuadro de diálogo que aparece.

368f17a974c75c73.png

  1. Por último, para instalar los Servicios de Google Play, ve a la pestaña SDK Tools, selecciona la casilla de verificación junto a Servicios de Google Play (Google Play services), haz clic en Aceptar (OK) y, luego, selecciona Aceptar (OK) en el diálogo que aparece**.**

497a954b82242f4b.png

API requeridas

En el paso 3 de la siguiente sección, habilita el SDK de Maps para Android y la API de Places para este codelab.

Comienza a utilizar Google Maps Platform

Si nunca usaste Google Maps Platform, sigue la guía Cómo comenzar a utilizar Google Maps Platform o mira la lista de reproducción Cómo comenzar a utilizar Google Maps Platform para completar los siguientes pasos:

  1. Crear una cuenta de facturación
  2. Crear un proyecto
  3. Habilitar las API y los SDK de Google Maps Platform (enumerados en la sección anterior)
  4. Generar una clave de API

Opcional: Android Emulator

Si no tienes un dispositivo compatible con ARCore, puedes usar Android Emulator para simular una escena de RA y la ubicación de tu dispositivo. Dado que también usarás Sceneform en este ejercicio, deberás asegurarte de seguir los pasos que se enumeran en la sección "Configura el emulador para que admita Sceneform".

3. Inicio rápido

Para que puedas comenzar lo más rápido posible, te ofrecemos un código inicial que te ayudará a seguir este codelab. Puedes pasar directamente a la solución, pero si quieres ver todos los pasos, sigue leyendo.

Puedes clonar el repositorio si tienes git instalado.

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

También puedes hacer clic en el botón siguiente para descargar el código fuente.

Una vez que tengas el código, abre el proyecto del directorio starter.

4. Descripción general del proyecto

Explora el código que descargaste en el paso anterior. Dentro de este repositorio, deberías encontrar un solo módulo llamado app, que contiene el paquete com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

Los siguientes atributos se declaran en el archivo AndroidManifest.xml para permitirte usar las funciones que se requieren en este 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" />

Para uses-permission, que especifica qué permisos deben otorgarse al usuario para que pueda usar esas capacidades, se declara lo siguiente:

  • android.permission.INTERNET: Esto permite que tu app pueda realizar operaciones de red y recuperar datos por Internet, como la información de los lugares mediante la API de Places.
  • android.permission.CAMERA: Se requiere acceso a la cámara del dispositivo para mostrar objetos en realidad aumentada.
  • android.permission.ACCESS_FINE_LOCATION: Se necesita acceso a la ubicación para recuperar los lugares cercanos según la ubicación del dispositivo.

Para uses-feature, que especifica las funciones de hardware que requiere esta app, se declara lo siguiente:

  • Se requiere la versión 3.0 de OpenGL ES.
  • Se requiere un dispositivo compatible con ARCore.

Además, las siguientes etiquetas de metadatos se agregan en el objeto 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 primera entrada de metadatos indica que ARCore es un requisito para que esta app se ejecute y la segunda le proporciona tu clave de API de Google Maps Platform al SDK de Maps para Android.

build.gradle

En build.gradle, se especifican las siguientes dependencias adicionales:

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

A continuación, se proporciona una breve descripción de cada dependencia:

  • Las bibliotecas con el ID de grupo com.google.android.gms, es decir, play-services-location y play-services-maps, se usan para acceder a información sobre la ubicación del dispositivo y a la funcionalidad de acceso relacionada con Google Maps.
  • com.google.maps.android:maps-utils-ktx es la biblioteca de extensiones de Kotlin (KTX) de la Biblioteca de utilidades del SDK de Maps para Android. Más adelante, se usará la funcionalidad ofrecida por esta biblioteca para posicionar los objetos virtuales en el espacio real.
  • com.google.ar.sceneform.ux:sceneform-ux es la biblioteca de Sceneform, que te permitirá renderizar escenas en 3D realistas sin tener que aprender OpenGL.
  • Las dependencias que están dentro del ID del grupo com.squareup.retrofit2 son las dependencias de Retrofit, que te permiten escribir rápidamente un cliente HTTP para interactuar con la API de Places.

Estructura del proyecto

Aquí encontrarás los siguientes paquetes y archivos:

  • **api:** Este paquete contiene clases que se usan para interactuar con la API de Places mediante Retrofit.
  • **ar:** Este paquete contiene todos los archivos relacionados con ARCore.
  • **model:** Este paquete contiene una sola clase de datos Place, que se usa para encapsular un único lugar tal como lo devuelve la API de Places.
  • MainActivity.kt: Este es el único objeto Activity dentro de tu app, que mostrará un mapa y una vista de cámara.

5. Cómo configurar la escena

Veamos en detalle los componentes principales de la app, comenzando por los bloques de código relacionados con la realidad aumentada.

MainActivity contiene un fragmento SupportMapFragment, que controlará la visualización del objeto de mapa, y una subclase de ArFragment (PlacesArFragment), que controla la visualización de la escena de realidad aumentada.

Configuración de la realidad aumentada

Además de mostrar la escena de realidad aumentada, PlacesArFragment también se ocupará de solicitarle al usuario el permiso para usar la cámara si todavía no lo concedió. Asimismo, es posible solicitar permisos adicionales anulando el método getAdditionalPermissions. Dado que también necesitas que se otorgue permiso de ubicación, especifica eso y anula el método getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Ejecuta el código

Busca el código base contenido en el directorio starter y ábrelo en Android Studio. Si haces clic en Ejecutar > Run 'app' en la barra de herramientas y, luego, implementas la app en tu dispositivo o emulador, primero se te solicitará que habilites el permiso de ubicación y uso de la cámara. Haz clic en Permitir y, a continuación, deberías ver una vista de cámara y una vista de mapa, una junto a la otra, de la siguiente manera:

e3e3073d5c86f427.png

Cómo detectar los planos

Cuando miras el entorno en el que te encuentras a través de tu cámara, es posible que notes algunos puntos blancos superpuestos sobre superficies horizontales, como los puntos blancos que se ven en la alfombra de la imagen.

2a9b6ea7dcb2e249.png

Estos puntos blancos son guías de ARCore para indicar que se detectó un plano horizontal. Estos planos detectados te permiten crear lo que se conoce como “ancla” para que puedas posicionar objetos virtuales en el espacio real.

Si deseas obtener más información sobre ARCore y cómo interpreta el entorno que te rodea, lee sobre sus conceptos fundamentales.

6. Obtén los lugares cercanos

Ahora, deberás primero acceder a la ubicación actual del dispositivo y mostrarla. Luego, recuperarás los lugares cercanos con la API de Places.

Configuración de Maps

Clave de API de Google Maps Platform

Anteriormente, creaste una clave de API de Google Maps Platform para habilitar las consultas a la API de Places y poder usar el SDK de Maps para Android. Abre el archivo gradle.properties y reemplaza la string "YOUR API KEY HERE" por la clave de API que creaste.

Muestra la ubicación del dispositivo en el mapa

Una vez que hayas agregado tu clave de API, incluye una ayuda en el mapa para que los usuarios puedan ver cuál es su ubicación real en el mapa y, así, orientarse. Para hacerlo, navega hasta el método setUpMaps y, dentro de la llamada a mapFragment.getMapAsync, configura googleMap.isMyLocationEnabled en true. Con eso, se mostrará el punto azul en el mapa.

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

Obtén la ubicación actual

Para obtener la ubicación del dispositivo, deberás usar la clase FusedLocationProviderClient. Ya generaste una instancia de esto con el método onCreate de MainActivity. A fin de usar este objeto, completa el método getCurrentLocation, que acepta un argumento lambda para pasarle una ubicación al llamador de este método.

Para finalizar este método, puedes acceder a la propiedad lastLocation del objeto FusedLocationProviderClient y agregarle un addOnSuccessListener de la siguiente manera:

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

Se llama al método getCurrentLocation desde la expresión lambda proporcionada en getMapAsync en el método setUpMaps, que permite recuperar los lugares cercanos.

Inicia la llamada de red a Places

En la llamada de método getNearbyPlaces, observa que los siguientes parámetros se pasan al método placesServices.nearbyPlaces: una clave de API, la ubicación del dispositivo, un radio en metros (que se establece en 2 km) y un tipo de lugar (actualmente establecido en park).

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

Para completar la llamada de red, pasa la clave de API que definiste en el archivo gradle.properties. El siguiente fragmento de código se define en tu archivo build.gradle en la configuración android > defaultConfig:

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

Esto hará que el valor del recurso de strings google_maps_key esté disponible en el momento de la compilación.

Para completar la llamada de red, simplemente puedes leer este recurso de strings con getString en el objeto Context.

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

7. Lugares en RA

Hasta ahora, hiciste lo siguiente:

  1. Le solicitaste al usuario permisos de ubicación y uso de la cámara cuando ejecuta la app por primera vez.
  2. Configuraste ARCore para iniciar el seguimiento de planos horizontales.
  3. Configuraste el SDK de Maps con tu clave de API.
  4. Obtuviste la ubicación actual del dispositivo.
  5. Recuperaste los lugares cercanos (específicamente, los parques) mediante la API de Places.

El paso que resta para completar este ejercicio es posicionar los lugares que recuperas en realidad aumentada.

Interpretación de la escena

ARCore puede interpretar la escena real a través de la cámara del dispositivo mediante la detección de puntos interesantes y distintivos, llamados puntos del entorno en cada fotograma. Cuando estos puntos del entorno están agrupados y parecen estar en un plano horizontal común, como mesas y pisos, ARCore puede ponerlos a disposición de la app como un plano horizontal.

Como viste anteriormente, ARCore muestra puntos blancos cuando se detecta un plano para guiar al usuario.

2a9b6ea7dcb2e249.png

Cómo agregar anclas

Una vez que se detecta un plano, puedes adjuntarle un objeto llamado ancla. Esto te permite colocar objetos virtuales y asegurarte de que parezcan permanecer en la misma posición en el espacio. Modifica el código para adjuntar un ancla cuando se detecte un plano.

En setUpAr, se adjunta un objeto OnTapArPlaneListener al fragmento PlacesArFragment. Este objeto de escucha se invoca cada vez que se presiona un plano en la escena de RA. Dentro de esta llamada, puedes crear un Anchor y un AnchorNode a partir del HitResult proporcionado en el objeto de escucha, de la siguiente manera:

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

AnchorNode es el elemento en el que adjuntarás los objetos de nodo secundario (instancias de PlaceNode) en la escena controlada mediante la llamada de método addPlaces.

Ejecuta la app

Si ejecutas la app con las modificaciones anteriores, mira a tu alrededor hasta que se detecte un plano. Presiona los puntos blancos que indican la presencia de un plano. Al hacerlo, ahora verás marcadores en el mapa de todos los parques más cercanos a tu alrededor. Sin embargo, puedes observar que los objetos virtuales están fijos en el ancla que se creó y no se ubican de forma relativa al lugar en el que se encuentran esos parques.

f93eb87c98a0098d.png

En tu último paso, debes corregir esto con la biblioteca de utilidades del SDK de Maps para Android y SensorManager en el dispositivo.

8. Cómo posicionar los lugares

Para que el ícono de lugar virtual en la realidad aumentada tenga una orientación precisa, necesitarás dos datos:

  • Dónde se encuentra el norte verdadero
  • El ángulo entre el norte y cada lugar

Cómo determinar el norte

Para determinar el norte, se utilizan los sensores de posición (geomagnéticos y acelerómetros) que se encuentran disponibles en el dispositivo. Con estos dos sensores, puedes recopilar información en tiempo real sobre la posición del dispositivo en el espacio. Para obtener más información sobre los sensores de posición, consulta Cómo calcular la orientación del dispositivo.

Para acceder a estos sensores, deberás obtener un SensorManager seguido del registro de un objeto de escucha SensorEventListener en esos sensores. Ya completamos estos pasos por ti en los métodos del ciclo de vida 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)
}

En el método onSensorChanged, se proporciona un objeto SensorEvent, que contiene detalles sobre el dato de un sensor determinado a medida que cambia con el tiempo. Agrega el siguiente código a ese método:

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

El código anterior verifica el tipo de sensor y, según cuál sea, actualizará la lectura del sensor adecuado (ya sea la del acelerómetro o la del magnetómetro). Mediante estas lecturas del sensor, se puede determinar el valor de orientationAngles[0], es decir a cuántos grados respecto del norte se encuentra el dispositivo.

Orientación esférica

Ahora que se determinó el norte, el paso siguiente es establecer el ángulo entre el norte y cada lugar. Luego, se usará esa información para posicionar los sitios con la orientación adecuada en la realidad aumentada.

Para calcular la orientación, usarás la Biblioteca de utilidades del SDK de Maps para Android, que contiene un conjunto de funciones auxiliares para calcular distancias y orientaciones mediante la geometría esférica. Para obtener más información, consulta esta descripción general de la biblioteca.

A continuación, usarás el método sphericalHeading de la biblioteca de utilidades, que calcula la orientación o el rumbo entre dos objetos LatLng. Esta información se necesita dentro del método getPositionVector definido en Place.kt. Al final, este método devolverá un objeto Vector3, que cada PlaceNode utilizará como su posición local en el espacio de RA.

Ahora, reemplaza la definición de heading (orientación) en ese método por lo siguiente:

val heading = latLng.sphericalHeading(placeLatLng)

La definición del método debería ser la siguiente:

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

Posición local

El último paso para orientar correctamente los lugares en RA es utilizar el resultado de getPositionVector cuando se agregan objetos PlaceNode a la escena. Navega hasta addPlaces en MainActivity, justo debajo de la línea donde se encuentra el elemento superior de cada placeNode (inmediatamente debajo de placeNode.setParent(anchorNode)). Configura el valor de localPosition de placeNode para que tome el resultado de la llamada a getPositionVector, de la siguiente manera:

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

De forma predeterminada, el método getPositionVector establece la distancia y del nodo en 1 metro, tal como se especifica en el valor y del método getPositionVector. Si deseas ajustar esta distancia, por ejemplo, a 2 metros, modifica el valor según sea necesario.

Con este cambio, los objetos PlaceNode que se agreguen deberían tener la orientación correcta. Ahora, ejecuta la app para ver el resultado.

9. Felicitaciones

Felicitaciones por haber llegado hasta aquí.

Más información