Utilizando Intenções Para Mapas de Alta Qualidade no Android
(5879) (8)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Kent Beck
EditoraNovatec
Edição1ª
Ano2024
Páginas112
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:
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;
- Intenções do Google Maps Android:
- Apresentação simples de um local específico;
- Evitando exceções quando a intenção não puder ser respondida;
- Busca por localizações: sintaxe e pontos mais próximos do usuário;
- Trabalhando o raio da busca por localização e pontos mais próximos;
- Pontos mais próximos de uma latitude e longitude definidos;
- Busca por um local específico;
- Buscando endereço de acordo com a posição atual do usuário;
- Buscando endereço de acordo com a latitude e longitude fornecidos;
- Definindo rótulo para o endereço buscado;
- Apresentando rota e guia turn-by-turn (GPS);
- Apresentando rota com definição de meio de transporte;
- Apresentando rota com definição de itens a evitar;
- Apresentando rotas com o uso de WayPoints (pontos obrigatórios em trajeto);
- Trabalhando com o Street View;
- Utilizando Plus Code em intenções de mapa;
- Ponto negativo;
- Pontos positivos;
- Considerações finais.
- Projeto Android:
- Slides;
- Vídeos;
- Conclusão;
- Fontes.
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;
- 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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á:
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:
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:
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:
Visualizando o mapa completo da rota com WayPoints, temos:
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:
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:
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:
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:
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 com o menu gaveta aberto |
Tela de contato via WhatsApp e endereço | Tela de apresentação ampla 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:
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–9988–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:
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:
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:
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):
A seguir o simples diagrama de 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() e 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:
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:
Testando a geração de rota no emulador com o Google Maps, temos:
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 permite 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
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
Comentários Facebook