Exibir lugares por perto em RA no Android (Kotlin)

1. Antes de começar

Resumo

Este codelab ensina como usar dados da Plataforma Google Maps para exibir lugares por perto em realidade aumentada (RA) no Android.

2344909dd9a52c60.png

Pré-requisitos

  • Conhecimento básico do desenvolvimento para Android usando o Android Studio
  • Familiaridade com o Kotlin

O que você aprenderá

  • Como solicitar a permissão do usuário para acessar a câmera e a localização do dispositivo dele.
  • Como fazer a integração com a API Places para buscar lugares por perto da localização do dispositivo.
  • Como fazer a integração com o ARCore para encontrar superfícies planas horizontais em que objetos virtuais possam ser ancorados e colocados em espaço 3D usando o Sceneform.
  • Como coletar informações sobre a posição do dispositivo no espaço com o SensorManager e como usar a biblioteca de utilitários do SDK do Maps para Android na hora de posicionar objetos virtuais na direção correta.

Pré-requisitos

2. Começar a configuração

Android Studio

Este codelab usa o Android 10.0 (nível da API 29) e requer a instalação do Google Play Services no Android Studio. Para instalar as duas dependências, faça o seguinte:

  1. Acesse o SDK Manager clicando em Ferramentas > SDK Manager.

6c44a9cb9cf6c236.png

  1. Verifique se o Android 10.0 está instalado. Se for preciso, instale-o marcando a caixa de seleção ao lado de Android 10.0 (Q), depois clique em OK e em OK novamente na caixa de diálogo que será exibida.

368f17a974c75c73.png

  1. Por fim, instale o Google Play Services. Para isso, vá até a guia Ferramentas do SDK, marque a caixa de seleção ao lado de Google Play Services, clique em OK e novamente em OK na caixa de diálogo exibida**.**

497a954b82242f4b.png

APIs necessárias

Na etapa 3 da seção a seguir, ative o SDK do Maps para Android e a API Places neste codelab.

Primeiros passos com a Plataforma Google Maps

Se você nunca usou a Plataforma Google Maps, siga o guia Primeiros passos com a Plataforma Google Maps ou assista à playlist Primeiros passos na Plataforma Google Maps para concluir as seguintes etapas:

  1. Criar uma conta de faturamento
  2. Criar um projeto
  3. Ativar as APIs e os SDKs da Plataforma Google Maps (listados na seção anterior)
  4. Gerar uma chave de API

Opcional: Android Emulator

Se você não tiver um dispositivo compatível com o ARCore, simule um cenário de RA e uma localização para seu dispositivo usando o Android Emulator. Como você também usará o Sceneform neste exercício, siga as etapas em "Configurar o emulador para ser compatível com o Sceneform".

3. Início rápido

Veja aqui alguns códigos para ajudar você a acompanhar este codelab e comece o mais rápido possível. Se preferir, você pode ir direto para a solução, mas continue lendo se quiser ver todas as etapas.

Você pode clonar o repositório se tiver o git instalado.

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

Caso prefira, clique no botão abaixo para fazer o download do código-fonte.

Depois de receber o código, abra o projeto no diretório starter.

4. Visão geral do projeto

Estude o código de que você fez o download na etapa anterior. Neste repositório, vai você encontrar um único módulo chamado app, que contém o pacote com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

Os atributos a seguir estão declarados no arquivo AndroidManifest.xml para que você possa usar os recursos necessários neste 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 quais permissões precisam ser concedidas pelo usuário antes que esses recursos possam ser usados, as seguintes condições são declaradas:

  • android.permission.INTERNET: com isso, seu app pode realizar operações de rede e buscar dados na Internet, como informações de locais com a API Places.
  • android.permission.CAMERA: o acesso à câmera é necessário para que, com ela, você possa exibir objetos em realidade aumentada.
  • android.permission.ACCESS_FINE_LOCATION: o acesso à localização é necessário para você buscar lugares por perto da localização do dispositivo.

Para uses-feature, que especifica quais recursos de hardware são necessários para esse app, as seguintes condições são declaradas:

  • O OpenGL ES versão 3.0 é obrigatório.
  • O dispositivo precisa ser compatível com o ARCore.

Além disso, são adicionadas ao objeto do app as seguintes tags de metadados:

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

A primeira entrada de metadados é para indicar que o ARCore é um requisito para a execução deste app. A segunda é para mostrar como você fornece sua chave de API da Plataforma Google Maps ao SDK do Maps para Android.

build.gradle

Em build.gradle, as seguintes dependências extras são especificadas:

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

Veja uma descrição breve de cada dependência:

  • As bibliotecas com o ID de grupo com.google.android.gms, isto é, play-services-location e play-services-maps, são usadas para acessar as informações de localização do dispositivo e acessar as funcionalidades do Google Maps.
  • com.google.maps.android:maps-utils-ktx é a biblioteca de extensões (KTX) do Kotlin para a biblioteca de utilitários do SDK do Maps para Android. A funcionalidade será usada nesta biblioteca posteriormente para posicionar objetos virtuais em espaços reais.
  • A com.google.ar.sceneform.ux:sceneform-ux é a biblioteca do Sceneform, com que você pode renderizar cenas 3D realistas sem precisar conhecer o OpenGL.
  • As dependências no ID do grupo com.squareup.retrofit2 são as dependências da Retrofit, que podem ser usadas para criar rapidamente um cliente HTTP que vai interagir com a API Places.

Estrutura do projeto

Este projeto traz os seguintes pacotes e arquivos:

  • **api:** este pacote contém classes que são usadas para interagir com a API Places usando a Retrofit.
  • **ar:** este pacote contém todos os arquivos relacionados ao ARCore.
  • **modelo:** este pacote contém uma única classe de dados Place, que é usada para encapsular um único local da maneira que é retornado pela API Places.
  • MainActivity.kt: é o único Activity incluído no seu app, que será usado para exibir um mapa e uma visualização da câmera.

5. Como preparar o cenário

Conheça a fundo os principais componentes do aplicativo, começando com as partes de realidade aumentada.

O MainActivity contém um SupportMapFragment, que tratará da exibição do objeto do mapa, e uma subclasse de um ArFragmentPlacesArFragment, que processa a exibição do cenário da realidade aumentada.

Configuração da realidade aumentada

Além de exibir o cenário da realidade aumentada, o PlacesArFragment também tratará da solicitação de permissão da câmera para o usuário, caso ainda não tenha sido concedida. Ao substituir o método do getAdditionalPermissions, outras permissões também podem ser solicitadas. Como você também precisa da permissão de localização, deixe isso especificado e altere o método getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Executar

Abra o código esqueleto no diretório starter do Android Studio. Antes de você clicar em Executar > Executar "app" na barra de ferramentas e implantar o aplicativo no seu dispositivo ou emulador, será solicitada a permissão de uso da localização e da câmera. Clique em Permitir. Ao fazer isso, você verá a seguinte visualização de câmera e uma de mapa lado a lado:

e3e3073d5c86f427.png

Como detectar superfícies planas

Se usar sua câmera para ver o ambiente em que está, você verá alguns pontos brancos sobrepostos em superfícies horizontais, como mostrado no carpete desta imagem.

2a9b6ea7dcb2e249.png

Eles são usados pelo ARCore para indicar que uma superfície plana horizontal foi detectada. Ao usar essas superfícies, você consegue criar o que chamamos de "âncora" e posicionar objetos virtuais em espaços reais.

Para mais informações sobre o ARCore e como ele enxerga o ambiente ao seu redor, leia sobre os conceitos fundamentais.

6. Ver os lugares por perto

Agora você precisa acessar e exibir a localização atual do dispositivo e, depois, buscar os lugares por perto usando a API Places.

Configuração do Maps

Chave de API da Plataforma Google Maps

Você já criou uma chave de API da Plataforma Google Maps para ativar a consulta da API Places e usar o SDK do Maps para Android. Agora, abra o arquivo gradle.properties e substitua a string "YOUR API KEY HERE" pela chave de API que foi criada.

Exibir localização do dispositivo no mapa

Depois de adicionar a chave de API, coloque um assistente no mapa para mostrar aos usuários onde eles estão na tela exibida. Para fazer isso, vá até o método setUpMaps e, dentro da chamada mapFragment.getMapAsync, defina googleMap.isMyLocationEnabled como true. para mostrar o ponto azul no mapa.

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

Ver a localização atual

Para obter a localização do dispositivo, será preciso usar a classe FusedLocationProviderClient. A instância para isso já foi estabelecida no método onCreate de MainActivity. Para usar esse objeto, preencha o método getCurrentLocation, que aceita um argumento lambda para que um local seja transmitido ao autor desse método.

Para concluir esse método, você pode acessar a propriedade lastLocation do objeto FusedLocationProviderClient e, depois, adicionar um addOnSuccessListener como o abaixo:

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

O método getCurrentLocation é chamado no lambda fornecido em getMapAsync, no setUpMaps, de onde os lugares por perto são buscados.

Iniciar chamada de rede de locais

Na chamada do método getNearbyPlaces, os seguintes parâmetros são transmitidos ao método placesServices.nearbyPlaces: uma chave de API, a localização do dispositivo, um raio em metros (definido como 2 km) e um tipo de lugar (atualmente definido como park).

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

Para concluir a chamada de rede, transmita a chave de API definida no arquivo gradle.properties. O snippet de código a seguir é definido pelo arquivo build.gradle na configuração android > defaultConfig:

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

Isso disponibilizará o valor do recurso de string do google_maps_key no tempo de compilação.

Para concluir a chamada de rede, basta ler este recurso de string usando o getString no objeto Context.

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

7. Lugares em RA

Até aqui, você já fez o seguinte:

  1. Solicitou ao usuário permissões de uso da câmera e da localização quando o app foi executado pela primeira vez
  2. Configurou o ARCore para buscar superfícies planas horizontais
  3. Configurou o SDK do Maps com sua chave de API
  4. Obteve a localização atual do dispositivo
  5. Buscou os lugares por perto (especificamente parques) usando a API Places

Agora, para concluir este exercício, basta posicionar os lugares que você está buscando na realidade aumentada.

Entender o local

O ARCore consegue ler o cenário real usando a câmera do dispositivo e detectando pontos interessantes e diferentes, chamados de pontos de recurso, em cada frame de imagem. Quando esses pontos de recurso são agrupados e parecem estar em uma superfície plana horizontal comum, como mesas e pisos, o ARCore consegue disponibilizar esse recurso para o app como um plano horizontal.

Como vimos anteriormente, o ARCore mostra ao usuário que uma área plana foi identificada ao exibir pontos brancos.

2a9b6ea7dcb2e249.png

Adicionar âncoras

Quando uma superfície plana é detectada, você pode anexar um objeto, que chamamos de âncora. Com uma âncora, é possível colocar artigos virtuais e garantir que eles permaneçam na mesma posição no espaço. Modifique o código para anexar uma delas assim que detectar uma superfície plana.

No setUpAr, um OnTapArPlaneListener é anexado ao PlacesArFragment. Esse listener é invocado sempre que há um toque em uma superfície plana no cenário em RA. Nessa chamada, você pode criar um Anchor e um AnchorNode com o HitResult fornecido no listener da seguinte forma:

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

O AnchorNode é onde você anexará objetos de nó filhos (instâncias do PlaceNode) no cenário que é exibido na chamada do método addPlaces.

Executar

Se você executar o app com as modificações acima, olhe ao seu redor até que uma superfície plana seja detectada. Toque nos pontos brancos que indicam uma dessas áreas. Ao fazer isso, você verá marcadores no mapa para todos os parques mais próximos da sua localização. No entanto, será possível notar que os objetos virtuais estão fixados na âncora que foi criada, em vez de estarem posicionados com relação à localização desses parques no espaço.

f93eb87c98a0098d.png

Na última etapa, você vai corrigir este problema usando a biblioteca de utilitários do SDK do Maps para Android e o SensorManager no dispositivo.

8. Posicionamento de lugares

Para posicionar o ícone de lugar virtual na realidade aumentada em uma direção precisa, são necessárias duas informações:

  • Onde está o norte verdadeiro
  • O ângulo entre o norte e cada lugar

Como determinar o norte

É possível determinar a direção norte usando os sensores de posição (geomagnético e acelerômetro) disponíveis no dispositivo. Com esses dois sensores, você consegue coletar informações em tempo real sobre a posição do seu aparelho no espaço. Para mais informações sobre os sensores de posição, acesse Determinar a orientação do dispositivo.

Para acessar esses sensores, você precisará ter um SensorManager e registrar um SensorEventListener nesses sensores. Estas etapas já foram concluídas para você nos métodos de ciclo de vida do 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)
}

No método do onSensorChanged, é fornecido um objeto SensorEvent com detalhes sobre determinado sensor conforme ele muda ao longo do tempo. Adicione o código a seguir nesse 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)
}

O código acima verifica o tipo do sensor e, dependendo do resultado, atualiza a leitura adequada do sensor, ou seja, a leitura do acelerômetro ou do magnetômetro. Usando essas leituras de sensor, é possível determinar o valor dos graus do norte em relação ao dispositivo, isto é, o valor de orientationAngles[0].

Título esférico

Agora que o norte foi definido, a próxima etapa é determinar o ângulo entre o norte e cada lugar e, então, usar essas informações para posicionar os locais na direção correta na realidade aumentada.

Para determinar a direção, você usará a biblioteca de utilitários do SDK do Maps para Android, que contém várias funções auxiliares para calcular distâncias e destinos por meio da geometria esférica. Para mais informações, acesse esta visão geral da biblioteca.

Em seguida, você utilizará o método do sphericalHeading na biblioteca de utilitários, que calcula a direção/ponto de referência entre dois objetos LatLng. Essas informações são necessárias para o método do getPositionVector definido no Place.kt. Esse método acabará retornando um objeto Vector3, que será usado individualmente pelos PlaceNode como sua posição local no espaço de RA.

Substitua a definição da direção nesse método por:

val heading = latLng.sphericalHeading(placeLatLng)

Isso deve resultar na seguinte definição de método:

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

Posição local

A última etapa para orientar os lugares corretamente na RA é usar o resultado do getPositionVector quando objetos PlaceNode estiverem sendo adicionados ao cenário. Vá até o addPlaces no MainActivity, logo abaixo da linha em que o pai está definido em cada placeNode, bem abaixo de placeNode.setParent(anchorNode). Defina o localPosition do placeNode para o resultado da chamada getPositionVector desta forma:

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

Por padrão, o método getPositionVector define a distância y do nó como 1 metro, conforme especificado pelo valor y no método getPositionVector. Se quiser ajustar essa distância para 2 metros, por exemplo, modifique esse valor conforme necessário.

Com essa alteração, os objetos PlaceNode estarão orientados na direção certa. Execute o app para ver o resultado.

9. Parabéns

Parabéns por chegar até aqui!

Saiba mais