Utilizando Intenções Para Mapas de Alta Qualidade no Android

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Utilizando Intenções Para Mapas de Alta Qualidade no Android

Utilizando Intenções Para Mapas de Alta Qualidade no Android

Vinícius Thiengo
(2884) (8) (185)
Go-ahead
"Os princípios de vida incluem grandemente a capacidade de enfrentar problemas com coragem, desapontamento com alegria e provação com humildade."
Thomas S. Monson
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, tudo bem?

Neste artigo vamos estudar, passo a passo, as Intenções específicas do Google Maps Android para a apresentação de mapas em alta qualidade, incluindo apresentações complexas que envolvem rotas com WayPoints e tecnologia "turn-by-turn".

Depois do estudo vamos a construção de um aplicativo que simula um app de domínio de problema real. Nele teremos também um algoritmo que fará uso da tecnologia de mapas via intenções:

Animação de rota no Google Maps Android via intenções

Antes de prosseguir, não esqueça de se inscrever 📩 na lista de emails do Blog para receber os conteúdos exclusivos e em primeira mão.

Abaixo os tópicos que estaremos abordando:

Maps Intents vs Maps API

Ok, mas qual a real vantagem em trabalharmos com intenções ao invés de trabalharmos diretamente com alguma API de mapas no Android?

Provavelmente a pergunta acima é a primeira que vem a sua cabeça, tendo em mente que o trabalho com intenções de mapas invoca o aplicativo do Google Maps ao invés de apresentar toda a configuração de rota, localidade, ... dentro do próprio aplicativo.

Até parece que estamos perdendo em qualidade e em glamour. Mas na verdade o uso da API do Google Maps é muito mais estrito do que se parece, digo, estrito em termos de domínios de problema.

A recomendação do Google é que: a API do Google Maps somente seja utilizada se parte da ideia central do aplicativo for a apresentação de mapas.

A seguir temos alguns contextos, de exemplo, que certamente se saem melhor com os mapas sendo utilizados dentro deles:

  • Aplicativo de passeadores de cachorros, onde o dono do pet poderá acompanhar a rota de passeio, em tempo real, pelo mapa apresentado no app;

Weg! Android App com uso do Google Maps API

  • Aplicativo de tracking de filho, onde os pais poderão acompanhar toda a movimentação do filho quando ele não estiver próximo a eles.

Para os aplicativos apresentados acima o trabalho com a API de mapas (junto a API de localização) se faz necessário. Isso também, pois o Google Maps App não permiti que aplicativos externos enviem a ele coordenadas para atualização de mapa em real-time.

A seguir temos a listagem de alguns contextos que não exigem o trabalho com a API de mapas, onde as Intents do Google Maps se saem melhores:

  • Aplicativo de classificados, em geral;
  • Aplicativo de portfólio, muito comum em domínios como:
    • Salão de beleza;
    • e Advogados.

As opções acima não exigem que a API do Google Maps seja utilizada, mesmo que a apresentação de rotas seja necessária.

O Google Maps Android fornece intenções que permitem a apresentação adequada de mapas pelo Google Maps App, isso com um simples acionamento de botão, por exemplo.

As principais vantagens quando trabalhando com intenções de mapas, são:

  • Facilidade de implementação, exigindo poucas linhas de código. Permitindo assim mais tempo de desenvolvimento focado no domínio do problema do aplicativo;
  • Alta qualidade na apresentação do mapa, pois o aplicativo utilizado será o do Google Maps, app específico para trabalho com mapas e localização.

Até aqui, como resumo, temos:

Se seu aplicativo atual não tem como uma das principais propostas dele o trabalho com mapas sendo atualizados em real-time, estude a possibilidade de utilizar intenções do Google Maps, pois é provável que até mesmo em termos de UX (experiência do usuário) seja uma melhor escolha.

Intenções do Google Maps Android

O Google Maps tem mais de uma categoria de intenções. Há intenções para aplicativos Android, para aplicativos iOS e intenções genéricas que atendem a todas as plataformas, são as intenções com domínios Web, que na verdade são denominadas: Maps URLs.

A recomendação do Google é que as Maps URLs sejam utilizadas caso o aplicativo em desenvolvimento atenda a mais de uma plataforma (Android, iOS, Web).

Como aqui no Blog nosso foco é no desenvolvimento Android, vamos prosseguir com as intenções para este sistema operacional mobile, porém, quando necessário, utilizaremos as Maps URLs para nos ajudar em limitações ainda presentes nas intenções específicas para o Android.

A seguir as possibilidades de mapas que temos quando utilizando Google Maps Intents:

  • Exibir um mapa com um local e zoom especificados;
  • Pesquisar e exibir em mapa locais próximos, incluindo pontos privados e públicos;
  • Solicitar e exibir rotas de um local para outro, incluindo rotas para os modos: dirigir (drive), caminhar (walk)e andar de bicicleta (bicycle);
  • Exibir imagens de panorama no Google Street View.

Com isso podemos prosseguir para os códigos que, acredite, são bem simples.

Apresentação simples de um local específico

A sintaxe de URI de intenção para a maneira mais simples de uso de intenções de Google Maps é a seguinte:

geo:latitude,longitude

geo:latitude,longitude?z=zoom /* O zoom é sempre opcional. */

 

A seguir um código de exemplo que quando acionado apresenta no mapa o local definido em latitudeLongitude:

...
/*
* A coordenada a seguir é a coordenada do Shopping Montserrat
* em Serra, Espírito Santo - Brasil.
* */
val latitudeLongitude = "-20.192710,-40.266240"
val zoom = 15
val geo = "geo:$latitudeLongitude?z=$zoom"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

/*
* Aqui estamos definindo que: se o aplicativo Google Maps estiver
* presente no aparelho, que ele seja utilizado para a apresentação
* do local em intent. Caso contrário qualquer aplicativo, que responda
* a Intent criada, pode ser utilizado.
* */
intent.setPackage( "com.google.android.apps.maps" )

startActivity( intent )
...

 

Importante: o zoom pode variar de 0, todo o planeta, a 21, locais individuais.

Note a sintaxe para a construção da intenção de mapa:

...
val intent = Intent( Intent.ACTION_VIEW, geoUri )
...
intent.setPackage( "com.google.android.apps.maps" )
...

 

Destrinchando ela, temos:

  • Intent( Intent.ACTION_VIEW, geoUri ):
    • A ação é sempre Intent.ACTION_VIEW;
    • O segundo argumento é uma URI específica do Google Maps, pode ser uma URI de domínio Android ou uma URI de domínio Web, pois mesmo assim o Google Maps App responderá.
  • setPackage( "com.google.android.apps.maps" ):
    • O único argumento possível indica qual o aplicativo preferencial para responder a Intent criada. Aqui sempre colocamos "com.google.android.apps.maps" que é o nome único de pacote do Google Maps Android App.

Executando o código anterior, temos:

Shopping Montserrat no Google Maps Android

Ok Thiengo, mas e se, mesmo que pouco provável, não haja nenhum aplicativo no aparelho que responda a Intent criada?

A resposta vem na próxima seção.

Evitando exceções quando a intenção não puder ser respondida

Sim, mesmo que pouco provável, pode acontecer de um usuário ou outro não ter nem o Google Maps e nem algum outro aplicativo que possa responder a intenção de mapa criada.

Para esse caso, se não houver um código de segurança, uma exceção será gerada, mais precisamente a exceção ActivityNotFoundException, e o aplicativo vai ser fechado, nada agradável para a experiência do usuário.

O código de segurança é simples, basta utilizar resolveActivity() como a seguir:

...
val latitudeLongitude = "-20.192710,-40.266240"
val zoom = 15
val geo = "geo:$latitudeLongitude?z=$zoom"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )

if( intent.resolveActivity( packageManager ) != null ){
startActivity( intent )
}
else{
/* TODO */
}
...

 

O que intent.resolveActivity( packageManager ) está fazendo é verificar se há algum aplicativo no aparelho do usuário que responda a intent, não precisa ser o Google Maps App, mesmo que o package dele tenha sido definido. Qualquer aplicativo que responda a intent é válido.

packageManager representa o conhecido método getPackageManager(), sintaxe comum no Java.

Confesso que somente coloquei em artigo o resolveActivity(), pois o código em /* TODO */ pode ser muito simples, apenas uma linha com um Toast, por exemplo.

Digo isso, pois a probabilidade de o usuário não ter o Google Maps App instalado é mínima, tendo em mente que este aplicativo tende a vir como instalação de fábrica.

Note que no código anterior também podemos ter a opção de abrir o mapa via Google Maps Web pelo navegador do aparelho, veja o algoritmo a seguir:

...
val latitudeLongitude = "-20.192710,-40.266240"
val zoom = 15
val geo = "geo:$latitudeLongitude?z=$zoom"

var geoUri = Uri.parse( geo )
var intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )

if( intent.resolveActivity( packageManager ) == null ){
/*
* Utilizando a sintaxe genérica do Google Maps, Maps URLs. Assim
* é certo que a versão Web do Google Maps será ao menos tentada no navegador
* do aparelho.
* */
val actionMap = "api=1&map_action=map"
val center = "center=$latitudeLongitude"
val z = "zoom=$zoom"
val web = "https://www.google.com/maps/@?$actionMap&$center&$z"

geoUri = Uri.parse( web )
intent = Intent( Intent.ACTION_VIEW, geoUri )
}

/* Verificação de segurança ainda se faz necessária. */
if( intent.resolveActivity( packageManager ) != null ){
startActivity( intent )
}
else{
/* TODO */
}
...

 

Veja que mesmo utilizando a sintaxe Web de apresentação de mapa, ainda é seguro verificar se há aplicativo que responde à nova Intent construída.

Neste artigo não entraremos no estudo aprofundado das Maps URLs, logo, caso seja de seu interesse, ao final do conteúdo acesse a documentação oficial em: Maps URLs - Developer Guide (em inglês).

Executando o último código apresentado, quando não há aplicativos para trabalho com mapas, temos:

Shopping Montserrat no Google Maps Web

Apresentação sendo carregada no navegador padrão do emulador, aqui: o navegador Chrome.

Busca por localizações: sintaxe e pontos mais próximos do usuário

Com a funcionalidade de busca também disponível via intenções de mapa, nós desenvolvedores até mesmo já podemos construir aplicativos úteis e estáticos, sem uso de back-end Web. Apps para turistas, por exemplo.

As sintaxes de URIs que podemos trabalhar em intenções de buscas para o Google Maps são as seguintes:

/*
* Apresente locais, busca, de acordo com a latitude e
* longitude definidos.
* */
geo:latitude,longitude?q=busca

/*
* Apresente o endereço / busca (endereco) de
* acordo com o posicionamento atual do aparelho, usuário.
* */
geo:0,0?q=endereco

/*
* Para a latitude e longitude especificados, apresente o ponto
* no mapa com o rótulo definido em "rotulo".
* */
geo:0,0?q=latitude,longitude(rotulo)

 

A seguir um exemplo que tem a responsabilidade de apresentar os restaurantes mais próximos ao usuário do aplicativo:

...
/*
* Buscando por restaurantes próximos ao usuário - no
* emulador de testes o usuário estava no Shopping
* Montserrat em Serra, Espírito Santo - Brasil.
* */
val query = "restaurantes"
val geo = "geo:0,0?q=$query"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Restaurantes próximos via Google Maps Android

Trabalhando o raio da busca por localização e pontos mais próximos

O raio de busca é baseado no valor do zoom. O código a seguir é o mesmo da seção anterior, porém com o zoom definido:

...
val query = "restaurantes"
val zoom = 20
val geo = "geo:0,0?q=$query&z=$zoom"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o algoritmo anterior, temos:

Restaurantes próximos via Google Maps Android - zoom aplicado

Lembrando que o zoom pode variar de 0 (todo o planeta) a 21 (locais individuais).

Pontos mais próximos de uma latitude e longitude definidos

Até este ponto do artigo já ficou evidente que as configurações de 0,0 para latitude e longitude informam ao Google Maps que é para utilizar a localização atual do aparelho como ponto de referência.

Caso fosse necessária a apresentação de estabelecimentos próximos a algum outro ponto, que não seja a posição atual do usuário, bastaria trabalhar a definição de latitude e longitude como no exemplo a seguir:

...
/*
* Buscando por boates próximas ao Shopping Vitória
* (-20.312800, -40.287858), Vitória, ES - Brasil.
* */
val query = "boates"
val latitudeLongitude = "-20.312800, -40.287858"
val geo = "geo:$latitudeLongitude?q=$query"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Boates próximas ao Shopping Vitória via Google Maps Android

Busca por um local específico 

Quando o endereço completo de um local é utilizado como item de busca, o pin do Google Maps é colocado nesse local. Veja o exemplo a seguir:

...
/*
* Uri.encode() garante que o conteúdo utilizado como
* parte da URI de mapa não utilizará caracteres não
* aceitos em uma URI. Uri.encode() é muito importante
* principalmente quando é o usuário do aplicativo que
* fornece a entrada que será utilizada na busca em mapa.
* */
val vilaVelhaMall = "2418 Shopping Vila Velha, Vila Velha, Espírito Santo"
val location = Uri.encode( vilaVelhaMall )
val geo = "geo:0,0?q=$location"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Note a importância de Uri.encode(). Com ele nem mesmo precisamos saber quais são os caracteres permitidos em uma URI, pois encode() nos garante que os caracteres de escape, se necessários, serão utilizados.

Executando o código anterior, temos:

Local específico no Google Maps Android

Buscando endereço de acordo com a posição atual do usuário 

Para buscar por determinado endereço mais próximo ao usuário, utilize uma definição de intenção de mapa como a seguir:

...
val street = "Rua das Maritacas"
val location = Uri.encode( street )
val geo = "geo:0,0?q=$location"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Endereços de acordo com o posicionamento do usuário - Google Maps Android

Note que não há muita precisão, pois o mapa está mostrando mais de uma opção de acordo com a posição atual do usuário.

De qualquer forma, caso o endereço de seu domínio de problema seja realmente único, então pode utilizar com segurança o modelo de código anterior, somente não esqueça de realizar testes para garantir que não haverá ambiguidade em mapa.

Buscando endereço de acordo com a latitude e longitude fornecidos 

Para ter maior precisão em uma apresentação de local por meio de endereço, utilize os dados de latitude e longitude de maneira explicita. Veja o exemplo a seguir:

...
/*
* O algoritmo abaixo buscará pela mais próxima "Rua das Maritacas" partindo
* da localização -20.200002, -40.227420 (Hospital Estadual Dr. Jayme
* Santos Neves).
* */
val street = "Rua das Maritacas"
val location = Uri.encode( street )
val latitudeLongitude = "-20.200002, -40.227420"
val geo = "geo:$latitudeLongitude?q=$location"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

O local utilizado como referência, Hospital Estadual Dr. Jayme Santos Neves, é o que comumente fornecemos como "ponto de referência" em formulários de cadastro, por exemplo.

Executando o código anterior, temos:

Endereços de acordo com o posicionamento definido - Google Maps Android

Segundo meus testes, se os locais encontrados forem próximos a posição atual do usuário, mesmo com a latitude e longitude definidos, a prioridade passa a ser: apresentar todos os locais, de mesmo nome, próximos ao usuário e não os locais próximos ao ponto definido em latitude e longitude.

Definindo rótulo para o endereço buscado

Para uma apresentação de local específico, com a latitude e a longitude definidos em q, é possível colocar um rótulo customizado, respeitando a regra: geo:0,0?q=latitude,longitude(rotulo).

Veja o código a seguir:

...
val label = Uri.encode( "Instituto de Ciências e Tecnologia da Cidade de Serra" )
val latitudeLongitude = "-20.197531, -40.217126" /* IFES - Campus Serra */
val geo = "geo:0,0?q=$latitudeLongitude($label)"

val geoUri = Uri.parse( geo )
val intent = Intent( Intent.ACTION_VIEW, geoUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o algoritmo anterior, temos:

IFES no Google Maps Android

Apresentando rota e guia turn-by-turn (GPS) 

Para a apresentação de rota temos de seguir alguma das sintaxes de URI a seguir:

google.navigation:q=endereco+em+texto

google.navigation:q=latitude,longitude

 

A rota será definida de acordo com a posição atual do usuário.

A seguir um código de exemplo, onde o usuário está no Shopping Montserrat, em Serra - ES, e precisa de uma rota para o Shopping Vitória, em Vitória - ES:

...
val address = "Shopping Vitória, Vitória, Espírito Santo, Brasil"
val location = Uri.encode( address )
val navigation = "google.navigation:q=$location"

val navigationUri = Uri.parse( navigation )
val intent = Intent( Intent.ACTION_VIEW, navigationUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Rota para o Shopping Vitória via Google Maps Android

Se o usuário quiser ver a rota completa, pois ele inicia já com a tecnologia turn-by-turn em uso, ele precisa acessar a tela de rota ampla, acessar esta tela acionando o back button do Android, por exemplo. Assim ele terá:

Rota completa para o Shopping Vitória via Google Maps Android

Apresentando rota com definição de meio de transporte

A apresentação de rota com definição de meio de transporte tem as mesmas regras de sintaxe apresentadas na seção anterior, porém aqui ainda temos o parâmetro mode, parâmetro que aceita os seguintes valores:

  • d para indicar que o trajeto será seguido em um automóvel (drive). d é o valor padrão;
  • w para indicar que o trajeto será seguido a pé (walk);
  • b para indicar que o trajeto será seguido de bicicleta (bicycle).

Veja o código a seguir:

...
val address = "Shopping Vitória, Vitória, Espírito Santo, Brasil"
val location = Uri.encode( address )
val mode = "b" /* Bicicleta */
val navigation = "google.navigation:q=$location&mode=$mode"

val navigationUri = Uri.parse( navigation )
val intent = Intent( Intent.ACTION_VIEW, navigationUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o algoritmo anterior, temos:

Rota de bicicleta para Shopping Vitória - Google Maps Android

Apresentando rota com definição de itens a evitar

É possível solicitar a apresentação de rota com itens a evitar. Para isso utilizamos o parâmetro avoid. A seguir os valores possíveis em avoid:

  • t para evitar pedágios (tolls);
  • h para evitar rodovias (highways);
  • f para evitar balsas (ferries).

Em itens a evitar o valor padrão é o "não uso" de avoid. Veja o código a seguir:

...
val address = "Ibitiquara, Cachoeiro de Itapemirim, Espírito Santo, Brasil"
val location = Uri.encode( address )
val avoid = "ht" /* Evitar rodovias e pedágios */
val navigation = "google.navigation:q=$location&avoid=$avoid"

val navigationUri = Uri.parse( navigation )
val intent = Intent( Intent.ACTION_VIEW, navigationUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o algoritmo anterior, temos:

Rota para Shopping Vitória evitando pedágios e rodovias - Google Maps Android

Em avoid é possível definir mais de um valor, diferente de mode. Também é possível utilizar mode e avoid na mesma intenção de Google Maps.

Apresentando rotas com o uso de WayPoints (pontos obrigatórios em trajeto)

Mesmo não tendo uma intenção Google Maps Android específica para a apresentação de rota com pontos obrigatórios de passagem, utilizando a versão genérica, com Maps URLs, é possível ainda acionar o Google Maps App com essa característica.

Veja o código a seguir:

...
val address = "Ibitiquara, Cachoeiro de Itapemirim, Espírito Santo, Brasil"
val destination = "destination=${Uri.encode( address )}"

/*
* travelmode aceita os valore: driving (dirigindo); walking (andando);
* bicycling (ciclismo); ou transit (transporte de viagem - ônibus,
* por exemplo).
* */
val mode = "travelmode=driving"

/*
* O caractere | é utilizado para separar os pontos aos quais o trajeto
* tem que passar até o destino (destination).
*
* Em plataformas onde WayPoints não são suportados, os pontos serão ignorados.
*
* É possível fornecer de 3 a 9 pontos, sendo que as plataformas / apps Google aceitam
* valores entre 3 e 9.
*
* Os WayPoints podem ser fornecidos como: endereços; nomes de locais; e
* coordenadas, "latitude,longitude".
* */
val laranjeirasMall = Uri.encode( "Shopping Laranjeiras, Serra, Espírito Santo, Brasil" )
val vitoriaMall = Uri.encode( "Shopping Vitória, Vitória, Espírito Santo, Brasil" )
val vilaVelhaMall = Uri.encode( "Shopping Vila Velha, Vila Velha, Espírito Santo, Brasil" )
val wayPoints = "waypoints=$laranjeirasMall|$vitoriaMall|$vilaVelhaMall"

val navigation = "https://www.google.com/maps/dir/?api=1&$destination&$mode&$wayPoints"
val navigationUri = Uri.parse( navigation )
val intent = Intent( Intent.ACTION_VIEW, navigationUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o algoritmo anterior, temos:

Rota para Cachoeiro de Itapemirim com WayPoints - Google Maps Android

Visualizando o mapa completo da rota com WayPoints, temos:

Rota ampla para Cachoeiro de Itapemirim com WayPoints - Google Maps Android

Note que a definição anterior utilizando sintaxe Maps URLs também permite o uso do parâmetro origin, mas como o objetivo era apresentar a rota de acordo com o posicionamento atual do usuário, origin foi omitido, algo que para o Google Maps App indica: utilize como ponto de origem de rota o posicionamento atual do aparelho.

Trabalhando com o Street View

Caso seu domínio do problema exija, é possível invocar o Google Maps com o Street View acionado. Para isso primeiro vamos a sintaxe de URI para uso da tecnologia Street View via intenções de Google Maps:

google.streetview:cbll=latitude,longitude&cbp=0,bearing,0,zoom,tilt

google.streetview:panoid=id&cbp=0,bearing,0,zoom,tilt

 

A sintaxe aqui é um pouco mais complicada do que as apresentadas em seções anteriores. Logo, vamos a discussão sobre as opções:

  • cbll aceita uma latitude e longitude como valores separados por vírgula. O aplicativo exibirá o panorama fotografado mais próximo ao local definido;
  • panoid é um ID de panorama. O Google Maps usará o panoid se caso um cbll também for informado. Os panoids estão disponíveis para aplicativos Android por meio da classe StreetViewPanoramaLocation;
  • cbp é um parâmetro opcional que ajusta a orientação inicial da câmera. O cbp recebe 5 valores separados por vírgulas, todos eles são opcionais, mas precisam estar presentes se ao menos um tiver de ser definido. Os valores mais significativos são o segundo, o quarto e o quinto, que definem o rumo (bearing), o zoom e a inclinação (tilt), respectivamente. O primeiro e terceiro valores não são suportados e devem ser definidos como 0. Agora um pouco mais sobre os três valores que importam:
    • bearing indica o rumo da bússola da câmera, em graus, no sentido horário a partir do norte. O norte é 0º, o leste é 90º, o sul é 180º e o oeste é 270º. Os valores passados para bearing são encerráveis, ou seja: 0°, 360° e 720° apontam para a mesma direção. bearing é o segundo de cinco valores separados por vírgulas;
    • zoom define o nível de zoom da câmera. O nível de zoom padrão é definido em 0. Um zoom em 1 duplica a ampliação. O zoom é fixado entre 0 e o nível máximo de zoom do panorama atual. Isso significa que qualquer valor que esteja fora desse intervalo será definido como o extremo mais próximo dentro desse intervalo. Por exemplo, um valor de -1 será definido como 0. zoom é o quarto de cinco valores separados por vírgulas;
    • tilt especifica o ângulo, para cima ou para baixo, da câmera. O alcance é de -90 a 0 a 90, com 90 apontando para baixo, 0 centrado no horizonte e -90 apontando para cima.

Agora alguns exemplos. Primeiro o Street View com coordenadas definidas, buscando o panorama mais próximo do Shopping Vitória, no Espírito Santo:

...
/*
* Coordenadas do Shopping Vitória, Vitória, ES - Brasil
* */
val vitoriaMall = "-20.312800,-40.287858"
val streetView = "google.streetview:cbll=$vitoriaMall"

val streetViewUri = Uri.parse( streetView )
val intent = Intent( Intent.ACTION_VIEW, streetViewUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Stree View do Shopping Vitória - Google Maps Android

Agora o Street View com um identificador único de panorama, panoid, definido. Segue código:

...
/*
* PanoID da praia de Maroubra em Sydney, Austrália
* */
val maroubraBeach = "Iaa2JyfIggYAAAQfCZU9KQ"
val streetView = "google.streetview:panoid=$maroubraBeach"


val streetViewUri = Uri.parse( streetView )
val intent = Intent( Intent.ACTION_VIEW, streetViewUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Street View da praia de Maroubra em Sydney - Google Maps Android

E por fim um Street View entre as pirâmides de Giza, dessa vez fazendo uso de parâmetros cbp:

...
/*
* Abre o Street View entre duas pirâmides em Giza. Os valores
* passados para o parâmetro cbp farão o ângulo da câmera um
* pouco para cima e para o leste.
* */
val pyramidsInGiza = "29.9774614,31.1329645"
val cbp = "0,30,0,0,-15"
val streetView = "google.streetview:cbll=$pyramidsInGiza&cbp=$cbp"

val streetViewUri = Uri.parse( streetView )
val intent = Intent( Intent.ACTION_VIEW, streetViewUri )

intent.setPackage( "com.google.android.apps.maps" )
startActivity( intent )
...

 

Executando o código anterior, temos:

Street View entre duas pirâmides em Giza - Google Maps Android

Utilizando Plus Code em intenções de mapa 

Como uma alternativa a fornecer endereço ou latitude e longitude nas intenções do Google Maps, temos o plus code. A seguir a definição de plus code, direto do site oficial.

Um plus code é como um endereço de rua para pessoas ou lugares que não têm um.

Os plus codes fornecem endereços para todos, em qualquer lugar, permitindo que eles recebam entregas, acessem serviços de emergência, registrem-se para votar e muito mais.

Um endereço de plus code mais parece um endereço regular, mas com um código curto, onde o nome da rua e o número estariam presentes caso existissem. Esses endereços existem para qualquer local, mesmo aqueles onde não há estradas.

Mais sobre o plus code você encontra em: https://plus.codes/.

A seguir um código de exemplo que faz uso de um plus code, mais precisamente o plus code que aponta para Rincon Hill, São Francisco, Califórnia:

...
val plusCodeRinconHill = "http://plus.codes/849VQJQ5+XX"
val uri = Uri.parse( plusCodeRinconHill )
val intent = Intent(Intent.ACTION_VIEW, uri)

intent.setPackage("com.google.android.apps.maps")
startActivity( intent )
...

 

Executando o código anterior, temos:

Plus code via Google Maps Android

Ponto negativo

  • Não há intenções Google Maps para definições de formas geométricas em mapa.

Pontos positivos

  • Com poucas linhas de código e com auxílio do Google Maps App é possível ter a maior qualidade em apresentação de mapas;
  • A resposta do Google Maps App mesmo quando é uma Maps URL em uso é algo que aumenta ainda mais a abrangência das intenções de mapa no Android.

Considerações finais

Mesmo que o acordo em projeto tenha exigido a apresentação de mapas dentro do aplicativo solicitado, estude bem o domínio do problema do app e então verifique se uma intenção de Google Maps se sairia melhor.

Confirmando que sim: intenção de mapa é a melhor opção. Não hesite em mostrar aos stakeholders as vantagens das Intents Google Maps, onde a principal delas acaba sendo a qualidade do Google Maps App para o trabalho com mapas, dos mais simples aos mais complexos.

Lembrando que algumas limitações nas intenções exclusivas do Google Maps Android podem ser resolvidas utilizando as Maps URLs, como fizemos na seção que aborda WayPoints.

Projeto Android

Para o melhor entendimento da importância do trabalho com intenções do Google Maps, vamos a construção de uma parte de um aplicativo que simula um domínio de problema real.

Mais precisamente: vamos construir a área de endereço de um aplicativo de salão de beleza, área que terá a opção de apresentar em mapa a rota até o salão, isso partindo do posicionamento atual do usuário.

Caso você queira acessar o projeto completo para ao menos obter os dados estáticos (imagens) e então seguir com a explicação da construção do aplicativo de exemplo, entre no GitHub dele em: https://github.com/viniciusthiengo/mariah-salao-de-beleza-kotlin-android.

Protótipo estático

A seguir as imagens do protótipo estático, desenvolvido antes mesmo de se iniciar um novo projeto no Android Studio:

Tela de entrada

Tela de entrada

Tela com o menu gaveta aberto

Tela com o menu gaveta aberto

Tela de contato via WhatsApp e endereço

Tela de contato via WhatsApp e endereço

Tela de apresentação ampla de rota

Tela de apresentação ampla de rota

Tela de apresentação detalhada de rota

Tela de apresentação detalhada de rota

 

Assim podemos partir para a criação e codificação do projeto Android.

Iniciando o projeto

Em seu Android Studio inicie um novo projeto Kotlin (pode ser Java se preferir seguir com esta linguagem):

  • Nome da aplicação: Mariah Salão de Beleza;
  • API mínima: 16 (Android Jelly Bean). Mais de 99% dos aparelhos Android em mercado sendo atendidos;
  • Atividade inicial: Navigation Drawer Activity;
  • Para todos os outros campos, deixe-os com os valores padrões.

Ao final teremos a seguinte arquitetura de projeto:

Arquitetura física do projeto Android Mariah Salão de Beleza

Configurações Gradle

A seguir a configuração do Gradle Project Level, ou build.gradle (Project: MariahSalodeBeleza):

buildscript {
ext.kotlin_version = '1.2.60'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Então o Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 27
defaultConfig {
applicationId "thiengo.com.br.mariahsalodebeleza"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
}

 

Para ambos os arquivos Gradle, sempre utilize as versões mais atuais de configuração e APIs.

Configurações AndroidManifest

A seguir as configurações para o AndroidManifest.xml do projeto, que por sinal serão as mesmas até o fim dele, exatamente como com os arquivos Gradle. Segue:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.mariahsalodebeleza">

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Configurações de estilo

Para as configurações de tema e estilo, vamos iniciar com a arquivo de definições de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9670CE</color>
<color name="colorPrimaryDark">#7954AF</color>
<color name="colorAccent">#7AD15A</color>

<color name="colorNavigationView">#F5F5F6</color>
<color name="colorItemNormal">#777777</color>

<color name="colorDarkTextLabel">#222222</color>
<color name="colorPurpleTextLabel">#714BA4</color>
<color name="colorGreyIcon">#969FAA</color>
</resources>

 

Então o arquivo de dimensões, /res/values/dimens.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>

<dimen name="content_margin_top">14dp</dimen>
<dimen name="text_size">16sp</dimen>
</resources>

 

Agora o arquivo de Strings, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Mariah Salão de Beleza</string>

<string name="navigation_drawer_open">Abrir menu gaveta</string>
<string name="navigation_drawer_close">Fechar menu gaveta</string>

<string name="label_services">Serviços</string>
<string name="label_bride_day">Dia de noiva</string>
<string name="label_packages">Pacotes</string>
<string name="label_schedule">Agendamento</string>
<string name="label_address">Endereço (rota em mapa)</string>

<string name="whatsapp_needed_info">
Instale o WhatsApp em seu aparelho.
</string>

<string name="label_located_in">Estamos localizados em:</string>
<string name="desc_icon_address">Ícone de endereço</string>
<string name="address">
Av. Américo Buaiz, nº 200, Serra - Espírito Santo. 2º andar,
loja 352 - ao lado da Los Neto.
</string>
<string name="desc_background_address">
Imagem de background da área de endereço.
</string>

<string name="label_are_you_in_mall">
Chegou no shopping? Pode nos acionar pelo WhatsApp se quiser:
</string>
<string name="desc_icon_whatsapp">Ícone do WhatsApp</string>
<string name="whatsapp_number">
+55 (27) 9&#8211;9988&#8211;7766
</string>

<string name="label_difficulty_finding_us">
Está tendo dificuldades em nos encontrar? Acione o botão
abaixo para que a rota seja apresentada a você.
</string>
<string name="label_view_route">Visualizar rota</string>

<string name="apps_needed_info">
Instale o Google Maps ou algum navegador para poder visualizar
a rota.
</string>
</resources>

 

No XML acima de Strings já temos todas as Strings estáticas que serão utilizadas em projeto, seguindo as boas práticas indicadas pelo Google Android, assim facilitando, por exemplo, a internacionalização de aplicativo e também diminuindo o número de linhas de códigos repetidas, isso quando há Strings sendo utilizadas em mais de um ponto do projeto.

Agora os arquivos de definição de tema de projeto. Primeiro o /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- Estilo padrão, aplicado em todo o projeto. -->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados.
-->
<style name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<!-- Para o correto enquadramento do AppBarLayout. -->
<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<!--
Utilizado para a correta apresentação de menus de pop-up
em barra de topo.
-->
<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light" />
</resources>

 

Nada diferente do padrão, exceto o uso de uma imagem de background via item android:windowBackground.

Por fim a versão styles.xml para aparelhos com o Android API 21 ou superior, /res/values-v21/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados. Somando a isso a aplicação de transparência na
statusBar.
-->
<style name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

Atividade principal

Para a atividade principal temos alguns arquivos XML para serem apresentados antes do código Kotlin de projeto.

Vamos iniciar com o arquivo de conteúdo, /res/layout/content_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="@layout/app_bar_main">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/tv_we_are_located_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/label_located_in"
android:textColor="@color/colorDarkTextLabel"
android:textSize="@dimen/text_size" />

<ImageView
android:id="@+id/iv_ic_address"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_alignLeft="@+id/tv_we_are_located_in"
android:layout_alignStart="@+id/tv_we_are_located_in"
android:layout_below="@+id/tv_we_are_located_in"
android:layout_marginLeft="26dp"
android:layout_marginStart="26dp"
android:layout_marginTop="@dimen/content_margin_top"
android:contentDescription="@string/desc_icon_address"
android:src="@drawable/ic_address"
android:tint="@color/colorGreyIcon" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_ic_address"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/iv_ic_address"
android:layout_toRightOf="@+id/iv_ic_address"
android:text="@string/address"
android:textSize="@dimen/text_size" />

<!--
O ImageView abaixo está sendo utilizado como hackcode
para que a imagem de background fique corretamente
posicionada na segunda parte de conteúdo da tela de
endereço. Este tipo de estratégia foi necessária, pois
a imagem quando definida como background no RelativeLayout
é na verdade parte do conteúdo dele, fazendo com que este
ViewRoot fique com as dimensões erradas em relação ao
protótipo estático apresentado para projeto.
-->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/rv_container"
android:layout_alignEnd="@+id/rv_container"
android:layout_alignLeft="@+id/rv_container"
android:layout_alignRight="@+id/rv_container"
android:layout_alignStart="@+id/rv_container"
android:layout_alignTop="@+id/rv_container"
android:contentDescription="@string/desc_background_address"
android:scaleType="centerCrop"
android:src="@drawable/background_address" />

<RelativeLayout
android:id="@+id/rv_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:padding="20dp">

<TextView
android:id="@+id/tv_whatsapp_contact_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/label_are_you_in_mall"
android:textColor="@color/colorPurpleTextLabel"
android:textSize="@dimen/text_size" />

<ImageView
android:id="@+id/iv_ic_whatsapp"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_alignLeft="@+id/tv_whatsapp_contact_label"
android:layout_alignStart="@+id/tv_whatsapp_contact_label"
android:layout_below="@+id/tv_whatsapp_contact_label"
android:layout_marginLeft="26dp"
android:layout_marginStart="26dp"
android:layout_marginTop="@dimen/content_margin_top"
android:contentDescription="@string/desc_icon_whatsapp"
android:onClick="whatsAppHelp"
android:src="@drawable/ic_whatsapp"
android:tint="@color/colorGreyIcon" />

<TextView
android:id="@+id/tv_whatsapp_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_ic_whatsapp"
android:layout_marginBottom="50dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/iv_ic_whatsapp"
android:layout_toRightOf="@+id/iv_ic_whatsapp"
android:onClick="whatsAppHelp"
android:text="@string/whatsapp_number"
android:textSize="@dimen/text_size" />

<TextView
android:id="@+id/tv_difficulty_finding_us"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_whatsapp_number"
android:text="@string/label_difficulty_finding_us"
android:textColor="@color/colorPurpleTextLabel"
android:textSize="@dimen/text_size" />

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_difficulty_finding_us"
android:layout_marginTop="@dimen/content_margin_top"
android:background="@color/colorAccent"
android:onClick="showRoute"
android:text="@string/label_view_route"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="18sp" />
</RelativeLayout>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>

 

A seguir o diagrama do layout anterior:

Diagrama do layout content_main.xml

Assim podemos partir para o layout que referencia o content_main.xml, o /res/layout/app_bar_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_main" />

</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout app_bar_main.xml

A seguir o XML do arquivo de menu que contém as opções que aparecerão no menu gaveta, arquivo /res/menu/activity_main_drawer.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_service"
android:icon="@drawable/ic_services"
android:title="@string/label_services" />
<item
android:id="@+id/nav_bride_day"
android:icon="@drawable/ic_bride_day"
android:title="@string/label_bride_day" />
<item
android:id="@+id/nav_packages"
android:icon="@drawable/ic_packages"
android:title="@string/label_packages" />
<item
android:id="@+id/nav_schedule"
android:icon="@drawable/ic_schedule"
android:title="@string/label_schedule" />
<item
android:id="@+id/nav_address"
android:checked="true"
android:icon="@drawable/ic_address"
android:title="@string/label_address" />
</group>
</menu>

 

Abaixo o diagrama do XML anterior:

Diagrama do menu activity_main_drawer.xml

Com isso podemos ir ao XML principal, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/colorNavigationView"
android:fitsSystemWindows="true"
app:itemBackground="@drawable/nav_item_background"
app:itemIconTint="@color/nav_icon_text"
app:itemTextColor="@color/nav_icon_text"
app:menu="@menu/activity_main_drawer" />

</android.support.v4.widget.DrawerLayout>

 

Note que em NavigationView estamos utilizando alguns XMLs extras para que a aparência do aplicativo seja como definida em protótipo estático.

Em /res/color (crie este diretório caso ele não esteja presente em seu projeto) crie o arquivo nav_icon_text.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">

<!--
A ordem dos itens de um arquivo <selector> deve ser
seguida de forma estrita, pois caso contrário os efeitos
esperados não ocorrerão.
-->

<!-- Estado "Selecionado" -->
<item
android:color="@android:color/white"
android:state_checked="true" />

<!-- Estado "Normal", não selecionado -->
<item android:color="@color/colorItemNormal" />
</selector>

 

Agora em /res/drawable crie o arquivo nav_item_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Estado "Selecionado" -->
<item
android:drawable="@color/colorAccent"
android:state_checked="true" />

<!-- Estado "Normal", não selecionado -->
<item android:drawable="@android:color/transparent" />
</selector>

 

Você deve ter notado que em um arquivo utilizamos android:color e em outro utilizamos android:drawable, isso, pois no Android a definição de tingimento de imagem (tint) e de cor de texto (textColor) é como esperado, utilizando atributos color.

Porém para a definição de cor de background, se android:color for utilizado uma exceção será gerada. Logo, a saída é o trabalho com android:drawable.

Na imagem a seguir temos: o "menu gaveta aberto sem o uso dos dois últimos arquivos XMLs apresentados" (imagem à esquerda) vs "menu gaveta aberto com o uso dos dois últimos arquivos XMLs apresentados" (imagem à direita):

Menus gaveta Android em comparação

A seguir o simples diagrama de activity_main.xml:

Diagrama do layout activity_main.xml

Com isso podemos ir ao código Kotlin inicial da MainActivity:

class MainActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_main )
setSupportActionBar( toolbar )

val toggle = ActionBarDrawerToggle(
this,
drawer_layout,
toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close )
drawer_layout.addDrawerListener( toggle )
toggle.syncState()

nav_view.setNavigationItemSelectedListener( this )
}

override fun onResume() {
super.onResume()
/*
* Hackcode para que seja possível atualizar o título
* da barra de topo sem que seja necessário mudar o
* nome do aplicativo.
* */
toolbar.title = getString( R.string.label_address )
}

override fun onBackPressed() {
if( drawer_layout.isDrawerOpen( GravityCompat.START ) ) {
drawer_layout.closeDrawer( GravityCompat.START )
}
else{
super.onBackPressed()
}
}

override fun onNavigationItemSelected( item: MenuItem ): Boolean {
/*
* Foi deixado aqui dentro somente o necessário para
* fechar o menu gaveta quando algum item for acionado.
* */
drawer_layout.closeDrawer( GravityCompat.START )
return false /* Para não mudar o item selecionado em menu gaveta */
}

fun whatsAppHelp( view: View ){
/* TODO */
}

fun showRoute(view: View){
/* TODO */
}
}

 

Os códigos de whatsAppHelp()showRoute() colocaremos nas próximas seções.

Você deve ter notado que os códigos da MainActivity são quase os mesmo de quando criamos um projeto Android com uma "Navigation Drawer Activity". Apenas retiramos o FloatingActionButton e adicionamos um algoritmo junto ao método onResume().

Definindo o WhatsApp helper

Mesmo fugindo de nosso domínio de estudo neste artigo, para incrementar ainda mais o app de exemplo, vamos colocar um simples código que permitirá o rápido contato com o salão de beleza via WhatsApp.

Ainda na MainActivity, coloque o código em destaque:

...
/*
* Método ouvidor para permitir que o usuário entre em contato
* com o WhatsApp correto com apenas um acionamento em tela.
* */
fun whatsAppHelp( view: View ){
/* O número abaixo é fictício. */
val whatsAppUri = Uri.parse( "smsto:27999887766" )
val intent = Intent( Intent.ACTION_SENDTO, whatsAppUri )

intent.setPackage( "com.whatsapp" )

/*
* Garantindo que a Intent somente será acionada se o
* aplicativo WhatsApp estiver presente no aparelho.
* */
if( intent.resolveActivity( packageManager ) != null ){
startActivity( intent )
}
else{
Toast
.makeText(
this,
getString( R.string.whatsapp_needed_info ),
Toast.LENGTH_SHORT
)
.show()
}
}
...

Definindo o algoritmo de apresentação de rota em mapa via Intent

Antes de apresentar o algoritmo, vamos ao fluxograma esperado nele:

Fluxograma do algoritmo de ativação do Google Maps

Com isso, ainda na MainActivity, adicione os códigos em destaque:

...
/*
* Método listener de toque (clique) no botão "VISUALIZAR ROTA",
* responsável por invocar o Google Maps App para apresentar ao
* usuário a rota que ele terá de percorrer até o salão de
* beleza, isso partindo do ponto atual dele. Como o salão de
* beleza é fictício, está sendo utilizada uma estética presente
* em Morada de Laranjeiras, Serra, ES.
* */
fun showRoute( view: View ){
var beautySalon = "Rebecca Miranda Centro Estético, " +
"Morada de Laranjeiras, Serra, Espírito Santo, Brasil"
beautySalon = Uri.encode( beautySalon )

var navigation = "google.navigation:q=$beautySalon"

var navigationUri = Uri.parse( navigation )
var intent = Intent( Intent.ACTION_VIEW, navigationUri )

intent.setPackage( "com.google.android.apps.maps" )

/*
* Caso o aplicativo do Google Maps não esteja presente no
* aparelho (algo difícil de acontecer), partimos para a
* apresentação de rota pelo Google Maps Web, via navegador
* mobile.
* */
if( intent.resolveActivity( packageManager ) == null ){

val dirAction = "dir_action=navigate"
val destination = "destination=$beautySalon"
navigation = "https://www.google.com/maps/dir/?api=1&$dirAction&$destination"

navigationUri = Uri.parse( navigation )
intent = Intent( Intent.ACTION_VIEW, navigationUri )
}

if( intent.resolveActivity( packageManager ) != null ){

startActivity( intent )
}
else{
/*
* Se nem Google Maps e nem navegador mobile estiverem
* presentes no aparelho, informe ao usuário para
* instalar ao menos um dos dois.
* */
Toast
.makeText(
this,
getString( R.string.apps_needed_info ),
Toast.LENGTH_LONG
)
.show()
}
}
...

 

Assim podemos partir para os testes.

Testes e resultados

Acesse o menu de topo do Android Studio. Acione Build e em seguida Rebuild project. Assim execute o aplicativo em seu emulador ou aparelho de testes.

Testando o algoritmo do WhatsApp, em um emulador sem este aplicativo, temos:

Animação da mensagem Toast sobre o WhatsApp

Testando a geração de rota no emulador com o Google Maps, temos:

Animação da apresentação de rota via intenções do Google Maps Android

Com isso terminamos o estudo das intenções do Google Maps Android.

Como emulador de testes utilizamos um com as seguintes configurações:

  • Aparelho: Nexus 5x;
  • Resolução: 1080 x 1920 - 420dpi;
  • API: 28 (Android P);
  • Alvo: Android null (Google Play) - o aplicativo da Play Store já vem instalado;
  • CPU: x86;
  • Espaço em disco: 10 GB.

Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.

Se inscreva também no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a apresentação completa das intenções de mapas no Android:

Vídeos

A seguir os vídeos com a construção passo a passo do algoritmo que faz uso de intenções de mapa no aplicativo Android de salão de beleza:

Para acessar o projeto de exemplo, entre no GitHub a seguir: https://github.com/viniciusthiengo/mariah-salao-de-beleza-kotlin-android.

Conclusão

Com as intenções de mapa do Google Maps conseguimos obter o máximo de qualidade que um aplicativo, que precisa apresentar mapas, poderia ter. Isso com poucas linhas de código.

O trabalho com intenções de mapas permiti que o desenvolvedor do app dedique ainda mais tempo no domínio do problema do aplicativo, resolvendo algoritmos de lógicas de negócio específicas de domínio, pois ao menos o trabalho com mapas é delegado para o Google Maps App.

Então é isso, caso você tenha alguma dica ou dúvida sobre o conteúdo de mapa no Android, deixe logo abaixo nos comentários.

E se curtiu o conteúdo, não esqueça de compartilha-lo. E por fim, não deixe de se inscrever na 📩 lista de emails.

Abraço.

Fontes

Google Maps Intents for Android

Maps URLs - Developer Guide

Maps URLs - URL Encoding

Change the color of a checked menu item in a navigation drawer - Resposta de Sash_KP e bond

Change the color of a checked menu item in a navigation drawer - Resposta de Ankush

Scale background image to wrap content of layout - Resposta de Squatting Bear

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Freelancer AndroidFreelancer AndroidAndroid
Trabalhando Análise Qualitativa em seu Aplicativo AndroidTrabalhando Análise Qualitativa em seu Aplicativo AndroidAndroid
Leitor de Códigos no Android com Barcode Scanner API - ZXingLeitor de Códigos no Android com Barcode Scanner API - ZXingAndroid

Compartilhar

Comentários Facebook (1)

Comentários Blog (7)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Marcela Oliveira (1) (0)
21/09/2018
Olá, Thiengo!
Se for um app de turismo, cada local, teria que ter uma intent para setar no maps o local e o endereço ?
Responder
Vinícius Thiengo (1) (0)
22/09/2018
Marcela, tudo bem?

Sim, para cada lugar deverá haver a própria configuração de Intent dele.

Acredito que em seu caso vale ainda mais a pena, pois o Google Maps App também permitirá a funcionalidade de rota para o usuário, guiando ele via GPS.

Abraço.
Responder
Heraldo Gama (1) (0)
20/08/2018
Muito bom ter você de volta! Espero que tudo esteja bem porae. Como sempre "show de bola" o conteúdo.
Só uma pergunta: Quanto de memória vc tá usando no teu MacBook ? e vc usa tbm SSD? Te pergunto isso porque consegui comprar meu primeiro Pro 13" (usado) mas tá valendo !!! E o AndroidStudio fica um pouco lento com a configuração de 4Gb RAM.
Responder
Vinícius Thiengo (0) (0)
20/08/2018
Heraldo, tudo bem?

Por aqui está tudo ok. Trabalhando para manter a frequência semanal de conteúdos.

Sobre o Mac Pro:

-> 16GB RAM DDR3;
-> 512GB SSD (não tenho HD).

Roda muito bem com o Android Studio e outros softwares. O problema é a persistência, 512GB é pouco, preciso remover arquivos com frequência.

Abraço.
Responder
Heraldo Gama (1) (0)
21/08/2018
Bom dia!
Obrigado. Estarei providenciando o upgrade urgente.
Aguardando aqui pra ver o próximo post.
Grande abraço.
T+
Responder
10/08/2018
Gostei muito do Artigo, só preferia que fosse em Java SE, não em kottlin, pois para min não serviu muito já que não uso, e prefiro o java.
Responder
Vinícius Thiengo (0) (0)
10/08/2018
Carlos, tudo bem?

Show de bola que você curtiu o conteúdo.

O Kotlin é bem próximo do Java, principalmente por também ser JVM...

Esse é um dos motivos de eu continuar os conteúdos em Kotlin: os desenvolvedores Java tendem a não sentirem dificuldades em implementar os mesmos algoritmos, porém em Java.

De qualquer forma, a dúvida surgindo, pode perguntar.

Abraço.
Responder