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.
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
- Android Studio 2020.3.1 ou uma versão mais recente
- Uma máquina de desenvolvimento compatível com o OpenGL ES 3.0 ou uma versão mais recente
- Um dispositivo compatível com o ARCore ou um Android Emulator com ARCore ativado (instruções na próxima etapa)
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:
- Acesse o SDK Manager clicando em Ferramentas > SDK Manager.
- 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.
- 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**.**
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:
- Criar uma conta de faturamento
- Criar um projeto
- Ativar as APIs e os SDKs da Plataforma Google Maps (listados na seção anterior)
- 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
eplay-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 ArFragment
—PlacesArFragment
, 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:
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.
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:
- Solicitou ao usuário permissões de uso da câmera e da localização quando o app foi executado pela primeira vez
- Configurou o ARCore para buscar superfícies planas horizontais
- Configurou o SDK do Maps com sua chave de API
- Obteve a localização atual do dispositivo
- 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.
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.
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!