Porque e Como Utilizar Vetores no Android

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Porque e Como Utilizar Vetores no Android

Porque e Como Utilizar Vetores no Android

Vinícius Thiengo
(5222)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Primeiro, pegue um café ☕ 🥨. Pois o conteúdo é muito importante para desenvolvedores Android e está bem completo.

Neste artigo vamos passo a passo destrinchar, com um olhar de desenvolvedor, a API de drawables vetoriais estáticos no Android.

API que se utilizada de maneira consciente vai somente trazer ganhos a todo o projeto de aplicativo, independente do domínio de problema.

Um informe importante:

Não deixe de dar um olhar especial à seção em que abordaremos a API de suporte de vetores... pois neste ponto do conteúdo vou lhe explicar, passo a passo, o porquê de evita-la.

O artigo está dividido em duas partes:

  • Primeiro vamos ao estudo completo das imagens vetoriais no Android;
  • e Então vamos a um projeto de exemplo. Projeto que simula um app real de acampamento de verão. 

Animação do app Android de exemplo - Yosemite

Ao longo do artigo você também tem disponível os slides e as vídeo aulas, caso prefira alguns destes formatos. De qualquer forma, não deixe de também ler o conteúdo por completo.

Antes de prosseguir, não esqueça de se inscrever na lista de e-mails 📩 do Blog para receber os novos conteúdos Android em primeira mão e também em versão PDF (lembrando que os PDFs dos artigos são liberados somente aos inscritos da lista e-mails... e ela é gratuita).

A seguir os tópicos abordados neste "artigo aula":

O porquê das imagens vetoriais

Primeiro é importante saber que imagens vetoriais não foram criadas devido ao Android.

Elas são bem mais antigas e a popularidade delas em desenvolvimento de software, principalmente Web (aqui no formato SVG - Scalable Vector Graphics), se iniciou no final da década de 90.

Mais precisamente em 1999, com a possibilidade de uso de vetores também em páginas Web.

O livro "Fundamentos da SVG" de Maurício Samy Silva (o Maujor) apresenta bem a história das imagens vetoriais. Vale a leitura se você estiver com um tempo extra.

A real "força" (popularidade) das imagens vetoriais vem principalmente devido à capacidade de escalabilidade sem perda de qualidade.

Ou seja, uma mesma imagem vetorial pode ser utilizada em diferentes tamanhos que mesmo assim a qualidade de visualização será a mesma.

Imagem vetorial em diferentes dimensões no Android

Algo que contrasta muito quando comparando imagens vetoriais às imagens rasterizadas (JPEG, PNG, GIF, MPEG4, ...). Imagens rasterizadas que são os conhecidos bitmap (mapa de bits).

As raster images quando escaladas para abaixo das proporções originais... os olhos humanos praticamente não detectam problemas na visualização, pois estas imagens têm "perda de informação".

Então quando levando em conta os termos visuais a olhos humanos, acaba ficando "tudo ok" utilizar imagens rasterizadas em proporções menores do que as dimensões originais.

Porém quando as imagens rasterizadas são colocadas em proporções maiores do que as originais...

... neste caso é nítido a qualquer um que estas imagens perdem em qualidade. Isso por sofrerem com a "falta de informação".

Ou seja, pixels extras que faltam à imagem.

Imagem rasterizada em diferentes dimensões no Android

Mesmo que pouco perceptível, a terceira imagem de rádio da figura acima (com arquivo PNG - bitmap) sofre de pixalagem (distorção por falta de pixels).

Pois essa imagem está sendo carregada em proporções maiores do que a versão original.

A terceira imagem da primeira figura desta seção (com arquivo vetorial) não sofre do mesmo problema. Porque por ser vetorial, independente da proporção original definida em arquivo, ela continua com a mesma qualidade.

Versão PNG vs Versão vetorial da imagem

Imagens vetoriais não sofrem com pixalagem, pois elas são baseadas em:

Uso de pontos e conexões para desenho de formas (linhas, curvas, polígonos e outros) em um plano cartesiano.

Já as imagens rasterizadas são baseadas em:

Informações de pixel. Onde cada pixel sabe somente a cor que deve ter. Ou seja, não foram feitos para trabalhar a escalabilidade ou replicação de pixels.

Parece ótimo o mundo somente com imagens vetoriais, certo?

Mas não se engane.

Há alguns contextos comuns no dia a dia do desenvolvedor Android onde as imagens vetoriais não são nem de perto uma boa escolha.

Vamos falar mais sobre isso no decorrer do conteúdo, então continue com o artigo 🧐.

Imagens vetoriais no Android

O Google, mais precisamente no Google I/O 2014, liberou as primeiras APIs Android para trabalho com imagens vetoriais.

As APIs principais são as seguintes:

  • VectorDrawable;
  • e AnimatedVectorDrawable.

A liberação veio junto ao anúncio do Android Lollipop (API 21).

A seguir as APIs de suporte, para versões do Android abaixo da API 21:

  • VectorDrawableCompat;
  • e AnimatedVectorDrawableCompat.

Se você já é um conhecedor de vetores, principalmente se você tem experiência com interface com o usuário no mundo Web...

... se você é este tipo de profissional, então fique ciente que a biblioteca de vetores no Android tem certas limitações.

Na verdade essa biblioteca roda algumas características do SVG mobile.

Isso, pois como o Android é o sistema operacional de inúmeros aparelhos de diferentes marcas, foi identificado que vários devices não seriam capazes de apresentar em tela um vetor com todas as possibilidades de um SVG.

Na verdade até hoje há navegador Web que não dá 100% de suporte a imagens vetoriais.

A seguir o exemplo de duas características SVG que não são suportadas pelo conjunto de APIs de vetores do Android:

  • Não é possível vincular arquivos JavaScript;
  • e Não é possível vincular imagens rasterizadas ao XML de vetor.

De qualquer forma, as APIs de vetores Android ainda são de extrema importância a um desenvolvedor Android, independente do nível dele.

Principalmente por causa de algo chamado "ícones de sistema" onde o suporte de vetores Android é completo.

Quando utilizar e quando não utilizar

Sim. Nem tudo são flores.

Apesar da promessa incrível em torno da escalabilidade sem necessidade de ao menos quatro arquivos de imagens no Android. As versões drawable em mdpi, hdpi, xhdpi e xxhdpi.

Apesar disso o trabalho com imagens vetoriais em aplicativos deve ser moderado quando saímos do contexto "ícones de sistema".

Segundo a documentação oficial nós devemos utilizar imagens vetoriais quando:

  • For para ícones de sistema;
  • ou Quando a imagem não tiver dimensões em tela maiores que 200dp x 200dp.

Em qualquer outra situação é bem provável que o uso de imagens rasterizadas seja em disparado uma melhor escolha.

Isso devido ao peso da renderização de imagens vetoriais.

Ou seja, não deixe de também investir tempo para entender "como funciona" e "como utilizar" o antigo e ainda válido modelo de imagens rasterizadas em folders drawable

O processo de renderização

O processo de renderização de vetores no Android é mais pesado do que o processo de renderização de imagens rasterizadas.

Veja a seguir o fluxo deste processo:

Fluxo de renderização de um Drawable Vetorial no Android

Enquanto uma imagem rasterizada passa por apenas três passos:

  • Carregar arquivo de imagem;
  • Desenhar drawable no canvas;
  • Gerar Bitmap em tela.

A imagem vetorial passa por cinco:

  • Carregar arquivo de vetor;
  • Inflar estrutura XML de vetor;
  • Gerar VectorDrawable;
  • Desenhar drawable no canvas;
  • Gerar Bitmap em tela.

Devido a isso é importante respeitar as regras de "quando utilizar uma imagem vetorial", caso contrário será nítido ao usuário do aplicativo que ao menos a renderização está lenta...

... isso sem levar em conta a grande possibilidade de um OutOfMemoryException.

Fique atento ao tamanho do arquivo

Um ponto muito importante a levar em consideração é o "tamanho do arquivo" vetorial.

E certamente a documentação oficial não menciona isso, pois eles devem entender que essa limitação é óbvia e você desenvolvedor deve literalmente não cometer esse erro:

Achar que somente porque a ilustração vetorial respeita as dimensões máximas de 200dp x 200dp recomendadas na documentação oficial...

... achar que devido a isso qualquer vetor pode ser utilizado, pois será melhor escolha do que uma imagem rasterizada.

Quando falamos em vetores, no Android é possível importar arquivos SVG e arquivos PSD (já já falaremos mais sobre estes dois formatos).

E no caso dos arquivos PSD é comum termos imagens vetoriais com características e detalhes a nível de imagens rasterizadas (por consequência a estrutura XML desses vetores é bem mais complexa).

Veja a imagem a seguir:

Imagem PSD de uma lâmpada

Seria uma "beleza" uma imagem assim, vetorial, em nosso projeto. Sem necessidade de replicação em outros arquivos drawable, certo?

Não seria!

A versão JPG dessa imagem, sem nenhum tratamento de compressão de bits, tem 8,5 MB de tamanho.

E o processo de renderização, como comentado anteriormente, é mais simples devido a ser uma imagem rasterizada.

E eu não estou levando em consideração que se a imagem for colocada nos exatos tamanhos necessários em projeto...

... muito provavelmente todas as versões juntas dessa imagem não vão atingir nem mesmo um 1 MB em APK.

Pois bem.

A versão PSD dessa imagem tem o tamanho de... pasme... 157,1 MB 😱.

Ou seja, mesmo que a View, na qual a versão vetorial da imagem vai ser carregada, tenha proporção 15dp x 15dp...

... mesmo assim um XML vetorial de 157,1 MB primeiro terá que ser colocado em projeto e depois renderizado em tela.

Resumo:

Se o arquivo vetorial chegou na casa dos megabytes (MB), então é muito provável que o trabalho com imagens rasterizadas seja uma melhor opção.

Estrutura básica (o necessário)

Com uma visão de desenvolvedor (e não de design), a estrutura XML de um vetor Android que realmente é útil conhecer é a seguinte:

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
...>

<path
.../>
...
</vector>

 

Bem simples, certo?

Já lhe adianto que há algumas outras tags além de <vector> e <path>. E mais atributos além dos que apresentarei aqui.

Porém, com um certo tempo de experiência no uso de drawables vetoriais no desenvolvimento de aplicativos Android e conhecendo os contextos onde esses drawables realmente acrescentam valor ao projeto...

... com isso eu seguramente lhe afirmo:

O que será apresentado aqui é 99,99% do que você precisa, como desenvolvedor Android, saber sobre vetores no Android.

Sendo assim, vamos às tags e atributos da estrutura vetorial apresentada acima:

<vector>

Tag raiz, onde entram todas as outras tags de configuração de desenho (ou animação) vetorial.

Alguns importantes atributos de toda a estrutura também são definidos nesta tag.

A seguir os importantes atributos de <vector> para um vetor estático:

➙ android:viewportWidth:

Define a largura da ViewPort. A ViewPort é basicamente a área virtual (canvas) na qual o vetor será desenhado por completo.

Para facilitar o entendimento: é possível ter mais de uma imagem vetorial em um mesmo canvas.

O recomendado é que não se defina um tipo de unidade aqui (dp, cm, px, ...). Assim a unidade utilizada será a mesma definida em android:width e android:height.

➙ android:viewportHeight:

Define a altura da ViewPort.

E como no atributo anterior... o recomendado é que não se defina um tipo de unidade aqui (dpcmpx, ...).

➙ android:width:

Define a largura exata da imagem vetorial. Aceita qualquer tipo de unidade, mas no contexto Android o normal é o trabalho com dp.

➙ android:height:

Define a altura exata da imagem vetorial. Também aceita qualquer tipo de unidade, mas no contexto Android o normal é o trabalho com dp.

➙ android:tint:

Por padrão define a cor que será aplicada em toda a parte preenchida da imagem vetorial. Isso, pois o valor padrão de android:tintMode é src_in.

Note que independente das cores de preenchimento (android:fillColor) e borda (android:strokeColor) definidas nas tags filhas de <vector>.

Independente disso, se android:tint for utilizado a cor definida nele é que será aplicada em todo o vetor. As cores em tags filhas de <vector> serão ignoradas.

tint é o atributo que você mais vai atualizar em arquivo de vetor... isso nas raras vezes que você precisar trabalhar diretamente na estrutura do arquivo de vetor.

Por fim, se seu aplicativo também dá suporte à versões do Android abaixo da API 21, então o recomendado é que o valor de tint seja hard-coded. Ou seja, sem uso de referência, ele deve ser colocado na "unha".

Ao invés de utilizar @color/colorAccent, por exemplo. Utilize o hexadecimal da cor direto como valor (hard-coded), #FF0000 (vermelho).

Isso é necessário, pois o Android Studio junto ao plugin do Gradle, assim que o app é compilado, gera versões rasterizadas de cada drawable vetorial (mais para frente no artigo você terá uma explicação detalhada sobre o porquê disso).

E serão essas imagens rasterizadas geradas que vão ser utilizadas nas versões do Android abaixo da API 21.

E por algum motivo, quando as cores não são colocadas hard-coded, pode haver problemas na geração dessas imagens rasterizadas...

... podendo deixar o design do aplicativo, em versões do Android abaixo da API 21, totalmente desalinhado com o esperado.

Dica: se seu aplicativo dá suporte a versões do Android abaixo da API 21, então para todos os atributos de arquivos vetoriais forneça valores hard-coded. Não utilize referências.

➙ android:tintMode:

Define o modo como o valor de android:tint será aplicado. O valor padrão é src_in.

Os valores possíveis são:

↳ add:

android:tintMode com valor add

↳ multiply:

android:tintMode com valor multiply

↳ screen:

android:tintMode com valor screen

↳ src_atop:

android:tintMode com valor src_atop

src_in;

android:tintMode com valor src_in

↳ src_over:

android:tintMode com valor src_over

Nas imagens acima utilizamos o android:tint com a cor vermelha (#FF0000).

Note que se android:tint não for definido de maneira explícita, então o valor em android:tintMode é ignorado.

<path>

Tag responsável por definir os caminhos a serem desenhados.

Pode conter apenas parte do desenho vetorial total ou toda a configuração de caminho da imagem vetorial.

A seguir os importantes atributos de <path> para um vetor estático:

➙ android:pathData:

Define os dados do caminho. O tipo de desenho (linha, curva, círculo, ...) e as coordenadas de cada desenho no plano.

Os possíveis valores aqui são exatamente os mesmos possíveis no atributo d de um arquivo SVG.

Um conselho: não se preocupe com os possíveis valores deste atributo, pois como desenvolvedor Android será pouco provável que você tenha que fazer algo aqui na "unha".

➙ android:fillColor:

Define a cor de preenchimento do path, caminho.

Se seu aplicativo Android dá suporte abaixo da API 21 (Android Lollipop). Então, como informado em android:tint, não é recomendado utilizar referência a cores como valor de fillColor.

Referência como @color/colorAccent.

No caso de trabalho com a API de suporte o recomendado é que a cor seja definida de maneira hard-coded. Exemplo: #FF0000 (vermelho).

Note que se não houver definição de cor de preenchimento (android:fillColor) quando também não há definição de largura e cor de borda (android:strokeWidth e android:strokeColor).

Então nada desse <path> é desenhado em tela, mesmo que o atributo android:tint tenha sido definido em <vector> e o atributo android:pathData de <path> tenha valores válidos.

➙ android:strokeColor:

Define a cor usada para desenhar o contorno do caminho definido em android:pathData.

Como em android:fillColor...

... se seu aplicativo Android dá suporte abaixo da API 21, então não é recomendado utilizar referência a cores aqui.

A cor deverá ser definida de maneira hard-coded. Exemplo: #0000FF (azul escuro).

➙ android:strokeWidth:

Define a largura da borda. Os valores são em ponto flutuante. O padrão é 0.0.

Como falei anteriormente: existem mais tags e mais atributos.

Mas com um olhar de desenvolvedor Android que tem que utilizar imagens vetoriais e tendo em mente os contextos onde elas são realmente necessárias...

... sabendo disso, você viu acima 99,99% do que precisará em termos de estrutura XML de um arquivo vetorial.

Suporte ao SVG e ao PSD

A API de vetores do Android dá suporte a somente dois tipos de formatos de vetores:

  • SVG (Scalable Vector Graphics);
  • e PSD (Photoshop Document).

Na verdade o suporte é parcial. O suficiente para que imagens estáticas ou animações possam ser utilizadas em qualquer instalação Android.

Digo, qualquer instalação a partir do Android 14 (Ice Cream Sandwich), isso quando utilizando a API de suporte.

Ainda vamos falar um pouco mais sobre o Vector Asset Studio, mas já lhe adianto que é com essa ferramenta do Android Studio que podemos adicionar arquivos SVG ou PSD em projetos Android.

Ou seja, o popular "copiar e colar" que é comum com imagens rasterizadas não funciona com arquivos .svg e .psd.

Tem que haver a importação da maneira correta, pois há todo um processo de conversão de .svg (ou .psd) para .xml.

Referência em código

Enfim podemos partir para a codificação.

Acredite, apesar de parecer algo complexo devido à quantidade de explicações que tivemos até chegarmos aqui.

Apesar disso o trabalho com vetores é muito simples.

O mais comum é o carregamento direto em XML, principalmente quando levando em consideração que o contexto comum de uso de vetores é o contexto de "ícones de sistema".

O comum: carregamento direto no XML de layout

O trabalho com vetores em XML tem as exatas mesmas características quando trabalhando com imagens rasterizadas.

Ou seja, a referência à imagem vetorial pode ocorrer em qualquer View capaz de carregar imagens (ImageView, ImageButton, FloatingActionButton, ...).

Abaixo o código XML de uma simples imagem vetorial (controle de vídeo game) de rótulo ic_baseline_sports_esports.xml:

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FF0000"
android:viewportWidth="24"
android:viewportHeight="24">

<path
android:fillColor="@android:color/white"
android:pathData="M21.58,16.09l-1.09,-7.66C20.21,6.46 18.52,5 16.53,5H7.47C5.48,5 3.79,6.46 3.51,8.43l-1.09,7.66C2.2,17.63 3.39,19 4.94,19h0c0.68,0 1.32,-0.27 1.8,-0.75L9,16h6l2.25,2.25c0.48,0.48 1.13,0.75 1.8,0.75h0C20.61,19 21.8,17.63 21.58,16.09zM11,11H9v2H8v-2H6v-1h2V8h1v2h2V11zM15,10c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C16,9.55 15.55,10 15,10zM17,13c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C18,12.55 17.55,13 17,13z" />
</vector>

 

Então o trecho do layout XML que carrega o vetor acima:

...
<ImageView
android:id="@+id/iv_vector_sample"
android:layout_width="190dp"
android:layout_height="190dp"
android:layout_gravity="center"
android:contentDescription="@string/simple_vector"
android:src="@drawable/ic_baseline_sports_esports" />
...

 

Como resultado, temos:

Drawable vetorial sendo carregado direto em XML Android

Note que se as definições na View que está carregando o vetor entrarem em conflito com as definições em arquivo XML de vetor.

Então as definições em View é que serão levadas em consideração pelo sistema.

As definições conflitantes presentes no arquivo de vetor serão ignoradas.

No ImageView anterior, as definições android:layout_width e android:layout_height sobrescrevem respectivamente as definições android:width e android:height do arquivo de vetor.

Carregamento via Kotlin API

Apesar de ser pouco comum, também é possível o carregamento de imagem vetorial via código dinâmico.

Partindo do mesmo XML de vetor carregado na seção anterior, poderíamos ter em código Kotlin o seguinte:

...
iv_vector_sample.setImageDrawable(
resources.getDrawable(
R.drawable.ic_baseline_sports_esports,
null
)
)

iv_vector_sample.setColorFilter(
Color.BLUE,
PorterDuff.Mode.SRC_IN
)
...

 

Como aqui não precisamos trabalhar com algum tema em específico além do tema padrão definido em projeto (definido em /res/values/styles.xml)...

... devido a isso o segundo argumento de getDrawable() pode ser seguramente o valor null.

Como resultado, temos:

Drawable vetorial sendo carregado via código Kotlin

Note que no código Kotlin anterior nós também atualizamos a cor do vetor:

...
iv_vector_sample.setColorFilter(
Color.BLUE,
PorterDuff.Mode.SRC_IN
)
...

 

Fiz isso mais para mostrar que a atualização da imagem vetorial em componente visual é como faríamos se a imagem carregada fosse uma rasterizada.

Lembrando que quando a imagem já está no componente visual, sendo ela vetorial ou não, essa imagem é um Bitmap. Lembre dos passos de renderização discutidos em O processo de renderização.

Thiengo, imaginei que nesta seção utilizaríamos VectorDrawable para criarmos um vetor na "unha", via código dinâmico. O que houve?

Como falei já em alguns pontos até aqui:

Nosso olhar para o trabalho com vetores no Android será um olhar de desenvolvedor.

Querer trabalhar com as APIs Android de vetores em código dinâmico para criar um vetor do zero...

... isso não é apresentado nem mesmo nas páginas de vetores da documentação oficial Android. Confesso que nem mesmo na comunidade Android eu encontrei algoritmos mostrando como fazer isso.

Mas provavelmente deve ser possível 🤔.

Porém volto a afirmar que trabalhando neste nível de detalhes com vetores, então você estará seguindo mais como um designer do que como um developer. Esse não é o nosso objetivo aqui.

API de suporte

O conjunto de APIs para imagens e animações vetoriais foi adicionado a partir do Android 21, Lollipop.

Sendo assim, para que seja possível o trabalho com vetores mesmo em versões do Android anteriores à API 21 é preciso o uso das APIs de suporte.

APIs que permitem que o suporte inicie do Android API 14, Ice Cream Sandwich (será isso verdade?! We will see).

Gradle Nível de Aplicativo

O primeiro passo é configurar no Gradle Nível de Aplicativo, ou build.gradle (Module: app), a definição correta para trabalho com as API de suporte.

Segue:

android {
...

defaultConfig {
...

/*
* A definição abaixo é necessária para que a API de suporte
* de vetores do Android seja utilizada.
* */
vectorDrawables.useSupportLibrary = true
}
...
}
...

 

Sincronize o projeto.

A partir deste ponto as APIs VectorDrawableAnimatedVectorDrawable deixam de ser utilizadas e as APIs de suporte, VectorDrawableCompat e AnimatedVectorDrawableCompat, passam a ser usadas em projeto.

Mas será que este é o único e melhor caminho para suporte a vetores no Android?

O problema do vectorDrawables.useSupportLibrary = true

Utilizando em projeto a definição vectorDrawables.useSupportLibrary = true faz com que as APIs de suporte de vetores passem a ser utilizadas e assim nenhuma imagem rasterizada extra é gerada para as versões do Android anteriores a API 21.

Porém tem um grande problema:

Com a definição de vectorDrawables.useSupportLibrary = true em projeto não mais é possível referenciar arquivos vetoriais como recurso (Int). Somente como objetos Drawable.

Caso contrário, quando o aplicativo estiver rodando em aparelhos com o Android abaixo da API 21 haverá exceções e o app fechará de maneira abrupta.

Ou seja, "esquece" a possibilidade do simples vinculo de um vetor ao um TextView, por exemplo, com o simples atributo android:drawableStart.

Será preciso acessar o objeto TextView em código dinâmico, criar um objeto Drawable do vetor, utilizando o ResourceCompat, por exemplo.

E ai sim vincular o objeto Drawable como parte do TextView via drawableStart.

Do que foi explicado acima somente um ponto já é o suficiente para "remover muito" da vantagem do trabalho com vetores ao invés de imagens rasterizadas:

"Será preciso acessar o objeto (...) em código dinâmico" 😟.

Veja bem... busque deixar em código dinâmico (Kotlin, Java, PHP, Python, ...) somente os algoritmos de domínio de problema.

Conteúdo estático deve ficar em arquivo de conteúdo estático (XML, HTML, CSS, ...).

Ok, Thiengo. E se eu não definir vectorDrawables.useSupportLibrary = true em projeto, como vou dar suporte a versões do Android abaixo do Lollipop?

A partir do Android Studio 1.5 (certamente você já está com uma versão mais atual) foi colocado no IDE uma ferramenta que junto ao plugin do Gradle faz com que imagens vetoriais tenham suas próprias versões rasterizadas.

Ou seja, assim que o projeto é compilado, se ele dá suporte a versões do Android abaixo da API 21. Então é gerada seis versões de cada imagem vetorial presente em projeto.

Essas versões ficam no folder de arquivos gerados em compilação, mais precisamente o folder /build/generated/res/pngs:

Folder Android como imagens rasterizadas geradas em tempo de compilação

Cada drawable vetorial tem uma nova imagem rasterizada sendo gerada para cada um dos folders a seguir:

  • /drawable-ldpi;
  • /drawable-mdpi;
  • /drawable-hdpi;
  • /drawable-xhdpi;
  • /drawable-xxhdpi;
  • e /drawable-xxxhdpi.

Essas versões rasterizadas não devem nunca ser atualizadas diretamente. Você deve continuar atualizando somente os vetores. Na compilação as novas versões rasterizadas de cada vetor serão todas geradas.

Thiengo, desta forma o arquivo APK final ficará maior do que quando não utilizando somente imagens rasterizadas, certo?

Sim, você está certo.

Se o seu aplicativo também oferece suporte a versões do Android abaixo da API 21 e ele também faz uso de drawables vetoriais.

Então devido a geração de imagens rasterizadas o arquivo APK final ficará com alguns poucos bytes a mais.

Mas acredite:

Esses bytes a mais não são nada que venha realmente a prejudicar o aplicativo.

Pois o tamanho extra tende a ser inofensivo e o ganho na manutenção do projeto continua sendo o que faz os pontos prós serem muito melhores do que os pontos contra.

Note que o sistema Android, quando em versões abaixo da API 21, utiliza as imagens rasterizadas ao invés de vetores.

Por isso que não devemos nos preocupar em informar isso em algum lugar do código quando vectorDrawables.useSupportLibrary = true não for definido.

O próprio sistema saberá escolher a versão correta de cada imagem para a versão de Android que estiver executando o aplicativo.

Para fechar está seção:

Nos meus próprios projetos eu não utilizo a configuração vectorDrawables.useSupportLibrary = true.

E aqui nós vamos seguir sem ela nos dois projetos do conteúdo 🤝... e tudo funcionará como esperado.

Outro ponto que quase esqueci de mencionar:

Sem a definição de vectorDrawables.useSupportLibrary = true o suporte se inicia no Android 7 (Eclair) e não somente a partir do Android 14.

Carregamento direto no XML de layout

Aqui as regras de negócio já comentadas na seção O comum: carregamento direto no XML de layout são as mesmas.

A única diferença é que se você tiver optado por prosseguir com os exemplos com a configuração vectorDrawables.useSupportLibrary = true definida em projeto, então o seu código XML deverá utilizar o atributo app:srcCompat ao invés de android:src.

O arquivo XML de vetor apresentado na seção O comum: carregamento direto no XML de layout continua intacto, sem alterações. Aqui utilizaremos ele como referência.

Sendo assim vou lhe poupar de ter que ver todo aquele arquivo XML novamente.

Como aqui não estamos com a configuração vectorDrawables.useSupportLibrary = true ativa, nosso código XML de carregamento de vetor terá atualização somente na altura e largura do ImageView:

...
<ImageView
android:id="@+id/iv_vector_sample_support"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:contentDescription="@string/simple_vector"
android:src="@drawable/ic_baseline_sports_esports" />
...

 

Note que se você for utilizar o app:srcCompat é preciso colocar na raiz do layout (de preferência no ViewRoot raiz) a seguinte referência xmlns:app="http://schemas.android.com/apk/res-auto".

Antes de executar o código anterior, no menu de topo do IDE acesse "Build" e acione "Clear Projetct" para assim remover as imagens rasterizadas geradas na última compilação.

Executando o projeto de exemplo com o código anterior em um emulador com o Android API 16 (Jelly Bean), temos:

Drawable rasterizado (de suporte) sendo carregado direto em XML Android

Você deve estar se questionando:

Por que foi necessário mudar as dimensões do ImageView de 190dp para 24dp?

Nós poderíamos manter os 190dp, porém o resultado seria uma imagem pixalada:

Problema de imagem pixalada em drawable rasterizado de suporte

Isso, pois as imagens rasterizadas que são geradas para cada vetor em projeto...

... essas imagens são geradas com base nas configurações definidas em arquivo XML de cada vetor.

O vetor do exemplo desta seção tem as dimensões 24dp x 24dp:

...
<vector
...
android:width="24dp"
android:height="24dp"
...>
...

 

Se quiséssemos manter o tamanho de 190dp no ImageView, mesmo para versões do Android abaixo da API 21, e ainda sim não sofrer com o problema de pixalagem.

Teríamos então que atualizar os tamanhos no arquivo de vetor, de 24dp para 190dp.

Então fica a dica:

Se o seu aplicativo for dar suporte também para versões do Android abaixo da API 21 e você inteligentemente não acionar a configuração vectorDrawables.useSupportLibrary = true.

Então é necessário colocar nos arquivos de vetores as configurações corretas para que em versões do Android abaixo da API 21 o layout seja apresentado como esperado.

Vetor via suporte Kotlin API

Tudo que já foi discutido na seção Carregamento via Kotlin API também é válido aqui.

E o código de carregamento (e atualização) de drawable em componente visual é muito similar:

...
iv_vector_sample.setImageResource(
R.drawable.ic_baseline_sports_esports
)

iv_vector_sample.setColorFilter(
Color.MAGENTA,
PorterDuff.Mode.SRC_IN
)
...

 

Desta vez utilizamos o método setImageResource() para carregamento do drawable.

Lembrando que quando o app estiver rodando em uma versão do Android abaixo da API 21 os drawables carregados serão os drawables rasterizados que foram gerados em tempo de compilação.

Sendo assim as regras de configuração em arquivo de vetor discutidas na seção anterior, essas regras também são válidas aqui para que o layout do aplicativo seja apresentado como definido em protótipo estático.

Como resultado do código anterior, temos:

Drawable rasterizado de suporte sendo carregado via código Kotlin

Devido ao suporte a versões do Android anteriores à API 21 é possível que você queira carregar o drawable com o uso de ResourcesCompat.

Você pode fazer isso sem receios.

Segue um exemplo:

...
iv_vector_sample.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
R.drawable.ic_baseline_sports_esports,
null
)
)
...

Vector Asset Studio

O Vector Asset Studio é a ferramenta responsável por facilitar consideravelmente o vida do desenvolvedor Android que precisa trabalhar com vetores.

Primeiro porque temos à nossa disposição um set com centenas de ícones dos mais variados estilos e prontos para serem incorporados ao projeto.

Segundo porque toda a necessidade de conversão de arquivos SVG ou PSD externos é delegada à essa ferramenta.

Ícones internos de sistema

Primeiro vamos a um passo a passo sobre como obter ícones já disponíveis na ferramenta.

Com o projeto (em visualização "Project") expandido...

Projeto Android expandido em visualização Project

... clique com o botão direito do mouse sobre o folder /drawable e então:

  • Clique em "New";
  • Logo depois em "Vector Asset".

Abrindo o Vector Asset Studio

Com a caixa de diálogo do Vector Asset Studio aberta:

  • Mantenha o "Asset Type" em "Clip Art";
  • Agora clique no pequeno ícone do campo "Clip Art" e selecione o ícone desejado (aplicando ou não algum filtro na barra de topo);

Selecionando ícone de sistema no Vector Asset Studio

  • Em "Name" defina o rótulo que seja mais conveniente ao tipo de ícone que foi escolhido caso o nome padrão não seja algo útil para a leitura e entendimento de seu projeto. Manter ic_ no início do rótulo de um ícone é uma convenção importante adotada pela comunidade Android;
  • Os campos "Size", "Color" e "Opacity" são intuitivos e fáceis de serem trabalhados;
  • O campo "Enable auto mirroring for RTL layout" deve ser marcado se você for criar uma versão de app que atende a um público que lê da direita para a esquerda;
  • Clique em "Next";
  • Por fim, na próxima caixa de diálogo, selecione o conjunto de origem de recurso. Onde ficará o drawable vetorial. Neste caso recomendo que você mantenha "Res directory" em /main, pois o recurso em /main faz com que ele esteja disponível em todas as possíveis variantes de compilação.

Note que os campos "Name" e "Size" devem ser modificados somente depois que o ícone já foi escolhido. Caso contrário será necessário atualiza-los novamente.

Então é isso.

Em questão de poucos minutos é possível ter ao menos toda a configuração de ícones de sistema de um aplicativo Android.

É aquilo, reforçando aqui:

Para ícones de sistema, não tem para onde ir. O uso de imagens vetoriais é um must e não um should.

Os ícones deverão ficar no folder /res/drawable:

Ícone dentro do folder Android /res/drawable

A seguir o XML do ícone que selecionamos (bateria completa) neste exemplo:

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">

<path
android:fillColor="@android:color/black"
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V5.33C17,4.6 16.4,4 15.67,4zM11,20v-5.5H9L13,7v5.5h2L11,20z" />
</vector>

 

Note que até o momento da construção deste artigo não era incomum o valor definido em "Color", no Vector Asset Studio, não ser respeitado no XML do ícone vetorial escolhido.

Isso devido ao uso de android:tint="?attr/colorControlNormal".

Basta abrir o XML do vetor e remover o android:tint ou colocar o valor de cor esperado nele.

Importação de arquivos externos (SVG e PSD)

Com necessidade de utilizar ícones ou ilustrações externas, a importação de vetor deve ser via Vector Asset Studio.

Pois a conversão é feita somente por está ferramenta. Digo, quando é possível realizar a conversão.

Pois caso o vetor externo esteja utilizando características não atendidas pela API de vetores do Android...

... neste caso o vetor não é importado.

Vamos novamente ao passo a passo.

Com o projeto expandido, clique com o botão direito do mouse sobre o folder drawable e então:

  • Clique em "New";
  • Logo depois em "Vector Asset".

Com a caixa de diálogo do Vector Asset Studio aberta:

  • Selecione "Local File (SVG, PSD)" em "Asset Type";
  • Em "Path" clique em "Browse" (o ícone de pasta);

Vector Asset Studio para seleção de vetor externo ao aplicativo Android

  • Navegue até o local aonde está o vetor e o selecione;

Navegando até o vetor (ícone de um médico) externo

  • Para os campos "Name", "Size", "Color", "Opacity" e "Enable auto mirroring for RTL layout" as regras de negócio são exatamente as mesmas já apresentadas na seção anterior;
  • Clique em "Next";
  • Para a próxima caixa de diálogo as regras são também as mesmas já discutidas na seção anterior. Sempre que possível mantenha "Res directory" em /main, pois o recurso em /main faz com que ele esteja disponível em todas as possíveis variantes de compilação.

Não esqueça que os campos "Name" e "Size" devem ser modificados somente depois que o vetor já tiver sido escolhido. Caso contrário será necessário atualiza-los novamente.

Então é isso. A importação de arquivos SVG ou PSD de vetores é tão simples quanto o uso de ícones já presentes na ferramenta.

Como ocorre com ícones de sistema selecionados via Vector Asset Studio, aqui os vetores selecionados também deverão ficar no folder /res/drawable.

A seguir o XML de vetor que selecionamos (um ícone de médico):

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">

<path
android:fillColor="#fde588"
android:fillType="evenOdd"
android:pathData="m351.299,154.987v0.057c0,2.126 -0.113,4.167 -0.34,6.18 0.227,1.871 0.34,3.77 0.34,5.726v32.258c0,18.51 -6.321,35.575 -16.894,49.21 -14.768,19.077 -37.87,31.408 -63.721,31.408h-14.684,-14.683c-24.207,0 -46.034,-10.8 -60.83,-27.836 -12.302,-14.173 -19.785,-32.655 -19.785,-52.781v-32.258c0,-2.013 0.113,-3.969 0.368,-5.896 -0.227,-1.956 -0.34,-3.969 -0.34,-6.009v-0.057,-21.232c0,-10.743 6.746,-11.565 21.628,-17.405 9.864,-3.26 21.94,-5.669 35.12,-7.228 12.217,-1.446 25.369,-2.183 38.522,-2.183s26.305,0.737 38.55,2.183c13.152,1.559 25.256,3.969 35.12,7.228 0.822,0.34 1.616,0.624 2.381,0.935 13.209,5.017 19.247,6.321 19.247,16.469v21.231zM398.182,154.335c0,-92.211 -63.919,-146.835 -142.182,-146.835 -78.291,0 -142.211,54.624 -142.211,146.835v36.028c0,60.18 17.971,86.684 40.96,108.709 -46.799,11.538 -109.273,48.7 -109.273,113.387v92.041h72.594,137.93 133.792,76.732v-92.041c0,-64.687 -62.502,-101.849 -109.273,-113.386 22.96,-22.025 40.931,-48.529 40.931,-108.709v-35.376zM291.772,396.641c0,-22.025 17.858,-39.884 39.883,-39.884s39.883,17.858 39.883,39.884c0,22.025 -17.858,39.884 -39.883,39.884s-39.883,-17.858 -39.883,-39.884z" />
<path
android:fillColor="#a1d4ff"
android:fillType="evenOdd"
android:pathData="m161.07,161.053c-0.255,1.928 -0.368,3.883 -0.368,5.896v32.258c0,20.154 7.483,38.608 19.785,52.781 14.796,17.036 36.623,27.836 60.83,27.836h14.683,14.683c25.851,0 48.953,-12.331 63.721,-31.408 10.573,-13.635 16.894,-30.699 16.894,-49.21v-32.258c0,-1.956 -0.113,-3.855 -0.34,-5.726 -3.033,27.129 -186.94,27.072 -189.888,-0.169z" />
<path
android:fillColor="#fdd76f"
android:fillType="evenOdd"
android:pathData="m398.182,190.364v-35.376h-26.305v34.498c0,53.717 -22.308,82.12 -40.62,108.255 -2.211,3.146 -7.937,13.181 6.151,17.065 46.771,11.537 102.81,49.323 102.81,103.748v85.946h26.305v-92.041c0,-64.687 -62.502,-101.849 -109.273,-113.386 22.961,-22.026 40.932,-48.53 40.932,-108.709z" />
<path
android:fillColor="#fdd76f"
android:fillType="evenOdd"
android:pathData="m398.182,154.335c0,-92.211 -63.919,-146.835 -142.182,-146.835 -2.551,0 -5.074,0.057 -7.568,0.17 69.476,8.05 123.446,61.058 123.446,145.758v1.559h26.305v-0.652z" />
<path
android:fillColor="#ffeee6"
android:fillType="evenOdd"
android:pathData="m294.55,109.122c-12.245,-1.446 -25.398,-2.183 -38.55,-2.183s-26.305,0.737 -38.522,2.183c-13.181,1.559 -25.256,3.969 -35.12,7.228 -14.882,5.839 -21.628,6.661 -21.628,17.405v21.232,0.057c0,2.041 0.113,4.054 0.34,6.009 2.948,27.241 25.823,44.646 51.391,44.362 21.203,-0.198 27.212,-5.187 33.873,-13.493 2.494,-3.09 6.094,-4.649 9.666,-4.62 3.6,-0.028 7.2,1.531 9.694,4.62 6.661,8.306 12.671,13.295 33.873,13.493 25.511,0.283 48.358,-17.065 51.391,-44.192 0.227,-2.013 0.34,-4.054 0.34,-6.18v-0.057,-21.232c0,-10.148 -6.038,-11.452 -19.247,-16.469 -0.765,-0.312 -1.559,-0.595 -2.381,-0.935 -9.864,-3.259 -21.967,-5.669 -35.12,-7.228z" />
<path
android:fillColor="#eff6ff"
android:fillType="evenOdd"
android:pathData="m331.655,356.758c-22.025,0 -39.883,17.858 -39.883,39.884 0,22.025 17.858,39.884 39.883,39.884s39.883,-17.858 39.883,-39.884c-0.001,-22.026 -17.858,-39.884 -39.883,-39.884z" />
<path
android:fillColor="#ffdfcf"
android:fillType="evenOdd"
android:pathData="m350.958,161.223c0.227,-2.013 0.34,-4.054 0.34,-6.18v-0.057,-21.232c0,-10.148 -6.038,-11.452 -19.247,-16.469l-2.381,-0.935c-0.51,-0.142 -0.992,-0.312 -1.474,-0.482 2.466,2.126 3.657,4.876 3.657,9.411v21.232,0.057c0,2.126 -0.113,4.167 -0.34,6.18 -3.005,27.128 -25.851,44.476 -51.363,44.192 -4.053,-0.028 -7.54,-0.255 -10.601,-0.624 5.669,5.641 12.869,8.929 30.018,9.099 25.512,0.284 48.358,-17.064 51.391,-44.192z" />
<path
android:fillColor="#86beec"
android:fillType="evenOdd"
android:pathData="m334.404,248.417c10.573,-13.635 16.894,-30.699 16.894,-49.21v-32.258c0,-1.956 -0.113,-3.855 -0.34,-5.726 -1.644,14.542 -8.929,26.277 -19.303,33.988v5.811c0,18.482 -6.293,35.575 -16.866,49.21 -11.48,14.797 -27.949,25.54 -46.827,29.594h2.721c25.851,-0.001 48.953,-12.332 63.721,-31.409z" />
<path
android:fillColor="#FF000000"
android:pathData="m217.081,157.295c-4.143,0 -7.5,3.358 -7.5,7.5v2.977c0,4.142 3.357,7.5 7.5,7.5s7.5,-3.358 7.5,-7.5v-2.977c0,-4.142 -3.357,-7.5 -7.5,-7.5z" />
<path
android:fillColor="#FF000000"
android:pathData="m202.773,146.739 l24.235,3.401c0.354,0.05 0.704,0.074 1.052,0.074 3.68,0 6.892,-2.71 7.417,-6.458 0.576,-4.102 -2.282,-7.894 -6.385,-8.47l-24.235,-3.401c-4.096,-0.579 -7.894,2.283 -8.469,6.385 -0.575,4.101 2.283,7.893 6.385,8.469z" />
<path
android:fillColor="#FF000000"
android:pathData="m294.749,157.295c-4.143,0 -7.5,3.358 -7.5,7.5v2.977c0,4.142 3.357,7.5 7.5,7.5s7.5,-3.358 7.5,-7.5v-2.977c0,-4.142 -3.357,-7.5 -7.5,-7.5z" />
<path
android:fillColor="#FF000000"
android:pathData="m283.798,150.214c0.348,0 0.698,-0.024 1.052,-0.074l24.235,-3.401c4.103,-0.576 6.961,-4.368 6.385,-8.47 -0.575,-4.103 -4.371,-6.96 -8.469,-6.385l-24.235,3.401c-4.103,0.576 -6.961,4.368 -6.385,8.47 0.525,3.749 3.737,6.459 7.417,6.459z" />
<path
android:fillColor="#FF000000"
android:pathData="m246.249,260.056h19.474c4.143,0 7.5,-3.358 7.5,-7.5s-3.357,-7.5 -7.5,-7.5h-19.474c-4.143,0 -7.5,3.358 -7.5,7.5s3.357,7.5 7.5,7.5z" />
<path
android:fillColor="#FF000000"
android:pathData="m246.249,234.998h19.474c4.143,0 7.5,-3.358 7.5,-7.5s-3.357,-7.5 -7.5,-7.5h-19.474c-4.143,0 -7.5,3.358 -7.5,7.5s3.357,7.5 7.5,7.5z" />
<path
android:fillColor="#FF000000"
android:pathData="m284.272,396.642c0,26.127 21.256,47.383 47.383,47.383 26.126,0 47.382,-21.256 47.382,-47.383s-21.256,-47.384 -47.382,-47.384c-26.127,0 -47.383,21.256 -47.383,47.384zM364.037,396.642c0,17.856 -14.526,32.383 -32.382,32.383s-32.383,-14.527 -32.383,-32.383 14.527,-32.384 32.383,-32.384 32.382,14.527 32.382,32.384z" />
<path
android:fillColor="#FF000000"
android:pathData="m324.155,382.979v6.163h-6.163c-4.143,0 -7.5,3.358 -7.5,7.5s3.357,7.5 7.5,7.5h6.163v6.134c0,4.142 3.357,7.5 7.5,7.5s7.5,-3.358 7.5,-7.5v-6.134h6.162c4.143,0 7.5,-3.358 7.5,-7.5s-3.357,-7.5 -7.5,-7.5h-6.162v-6.163c0,-4.142 -3.357,-7.5 -7.5,-7.5s-7.5,3.357 -7.5,7.5z" />
<path
android:fillColor="#FF000000"
android:pathData="m456.733,355.48c-2.288,-3.455 -6.94,-4.4 -10.394,-2.113s-4.399,6.941 -2.112,10.395c9.818,14.827 14.797,31.211 14.797,48.697v84.541h-61.732v-46.472c0,-4.142 -3.357,-7.5 -7.5,-7.5s-7.5,3.358 -7.5,7.5v46.472h-118.792v-161.606c14.678,-1.287 38.304,-5.572 50.187,-19.631 2.674,-3.164 2.276,-7.896 -0.887,-10.569 -3.167,-2.674 -7.897,-2.276 -10.57,0.887 -6.157,7.284 -20.634,12.535 -38.729,14.264v-33.019h7.184c48.587,0 88.115,-39.529 88.115,-88.118v-32.258c0,-1.41 -0.065,-2.866 -0.193,-4.462h32.076v27.876c0,55.163 -14.778,80.422 -38.624,103.297 -1.973,1.893 -2.759,4.708 -2.053,7.35s2.793,4.688 5.447,5.344c24.596,6.072 49.021,18.238 67.012,33.38 1.406,1.184 3.12,1.762 4.825,1.762 2.137,0 4.259,-0.908 5.742,-2.67 2.667,-3.169 2.26,-7.9 -0.909,-10.568 -16.653,-14.016 -38.276,-25.686 -60.735,-32.947 20.905,-23.11 34.295,-51.885 34.295,-104.946v-36.029c0,-46.024 -15.215,-85.224 -44,-113.36 -27.033,-26.425 -64.565,-40.977 -105.683,-40.977 -42.516,0 -80.928,15.424 -108.159,43.431 -2.888,2.97 -2.821,7.718 0.148,10.605 2.971,2.889 7.72,2.82 10.605,-0.148 24.385,-25.077 58.977,-38.888 97.406,-38.888 77.073,0 131.547,54.127 134.546,132.487h-31.747v-13.732c0,-11.856 -6.483,-16.426 -14.974,-19.992l5.286,-7.349c2.418,-3.362 1.653,-8.049 -1.709,-10.468 -3.364,-2.418 -8.05,-1.655 -10.469,1.709l-7.698,10.702c-7.66,-2.33 -16.39,-4.205 -25.783,-5.628l2.652,-13.476c0.801,-4.064 -1.846,-8.007 -5.91,-8.807 -4.063,-0.797 -8.007,1.846 -8.807,5.911l-2.861,14.539c-8.125,-0.781 -16.539,-1.259 -25.025,-1.436v-15.169c0,-4.142 -3.357,-7.5 -7.5,-7.5s-7.5,3.358 -7.5,7.5v15.17c-8.477,0.178 -16.881,0.655 -24.997,1.435l-2.861,-14.539c-0.799,-4.063 -4.733,-6.709 -8.807,-5.911 -4.064,0.8 -6.711,4.743 -5.91,8.807l2.652,13.476c-8.645,1.31 -16.726,3.004 -23.934,5.08l-8.527,-11.854c-2.419,-3.363 -7.107,-4.127 -10.469,-1.709s-4.127,7.105 -1.709,10.468l6,8.342c-9.329,3.692 -16.709,8.047 -16.709,20.699v13.732h-31.802c0.897,-24.708 6.664,-47.038 17.207,-66.448 1.977,-3.64 0.629,-8.193 -3.011,-10.17 -3.642,-1.979 -8.194,-0.628 -10.171,3.011 -12.716,23.412 -19.164,50.48 -19.164,80.455v36.029c0,53.057 13.389,81.828 34.294,104.925 -52.891,16.892 -102.607,57.995 -102.607,117.17v92.041c0,4.142 3.357,7.5 7.5,7.5h421.049c4.143,0 7.5,-3.358 7.5,-7.5v-92.041c-0.001,-20.482 -5.818,-39.653 -17.292,-56.979zM168.229,133.755c0,-3.765 0,-4.072 11.102,-8.227 1.713,-0.641 3.568,-1.336 5.569,-2.119 36.637,-12.025 105.589,-12.026 142.228,0 2.001,0.783 3.855,1.478 5.569,2.119 11.102,4.155 11.102,4.462 11.102,8.227v21.289c0,11.957 -4.212,22.445 -12.18,30.331 -8.07,7.987 -19.521,12.543 -31.493,12.543 -0.162,0 -0.326,-0.001 -0.488,-0.002 -19.67,-0.184 -23.203,-4.589 -28.107,-10.704 -3.807,-4.715 -9.423,-7.41 -15.432,-7.41 -0.066,0 -0.133,0 -0.197,0 -5.99,0 -11.598,2.695 -15.418,7.429 -4.89,6.097 -8.423,10.501 -28.101,10.686 -0.162,0.001 -0.324,0.002 -0.487,0.002 -11.964,0 -23.416,-4.557 -31.486,-12.543 -6.712,-6.643 -10.746,-15.137 -11.855,-24.795 -0.017,-0.27 -0.046,-0.538 -0.091,-0.801 -0.151,-1.551 -0.233,-3.128 -0.233,-4.734v-21.291zM168.201,194.311c0.541,0.584 1.088,1.163 1.656,1.725 10.862,10.75 26.152,16.882 42.035,16.882 0.212,0 0.426,-0.001 0.639,-0.003 22.639,-0.211 31.116,-5.655 39.639,-16.281 0.94,-1.166 2.3,-1.832 3.734,-1.832 0.031,0 0.065,0.001 0.096,0h0.097c1.451,0 2.82,0.666 3.747,1.813 8.537,10.645 17.015,16.088 39.646,16.3 0.213,0.002 0.425,0.003 0.639,0.003 15.889,0 31.181,-6.133 42.042,-16.882 0.558,-0.553 1.096,-1.122 1.628,-1.696v4.867c0,40.317 -32.8,73.118 -73.115,73.118h-29.367c-40.315,0 -73.115,-32.8 -73.115,-73.118v-4.896zM248.5,320.344c-18.113,-1.728 -32.589,-6.976 -38.723,-14.256 -2.668,-3.167 -7.399,-3.572 -10.568,-0.902 -3.167,2.669 -3.571,7.401 -0.902,10.568 11.854,14.067 35.502,18.353 50.193,19.64v161.606h-122.931v-46.472c0,-4.142 -3.357,-7.5 -7.5,-7.5s-7.5,3.358 -7.5,7.5v46.472h-57.593v-84.541c0,-63.603 65.102,-96.622 103.568,-106.104 2.656,-0.655 4.744,-2.703 5.451,-5.346 0.706,-2.643 -0.082,-5.46 -2.058,-7.352 -23.86,-22.861 -38.648,-48.115 -38.648,-103.293v-27.876h32.094c-0.118,1.439 -0.182,2.912 -0.182,4.462v32.258c0,48.588 39.528,88.118 88.115,88.118h7.184z" />
</vector>

 

Caso não seja possível importar um arquivo, a ferramenta Vector Asset Studio apresenta uma mensagem como a em destaque a seguir:

Problemas no carregamento de um vetor não compatível com o Android

Excelentes repositórios de arquivos vetoriais

Para ícones e ilustrações das mais variadas, existem inúmeros repositórios online que também têm recursos gratuitos.

A seguir listo alguns que certamente vão lhe ajudar a colocar melhores recursos visuais, vetores, em seus projetos Android sem que você tenha que ser um expert em design:

  • Flaticon - excelente para encontrar os mais variados ícones SVG;
  • Freepick - excelente para encontrar não somente ilustrações SVG, mas também PSD.

Faça cadastro em todos que você for utilizar. Caso contrário o limite de downloads gratuitos é muito baixo e logo é alcançado.

Sinta-se convidado a colocar na área de comentários deste artigo aula os sites de vetores que você conhece e não foram listados aqui.

Carregamento via API de imagens

Note que, mesmo não sendo mencionado de maneira explícita em documentação oficial. Mesmo com esse "não mencionamento", os drawables vetoriais tendem a ser uma opção para gráficos locais no aplicativo Android.

Então se você projeta carregar, em seu app, gráficos visuais de uma fonte externa junto a alguma API de carregamento remoto de imagens (Picasso ou Universal Image Loader, por exemplo)...

... se você planeja isso, então trabalhe com imagens rasterizadas.

Pois é muito provável que essas APIs de carregamento remoto de imagens não sejam capazes de carregar e renderizar imagens vetoriais em tela.

Ao menos a documentação oficial das principais APIs de carregamento de imagens não dão a opção para carregamento de vetores XML.

E as animações Android com vetores?

Sim. É possível criar animações com a API de vetores Android.

Mais precisamente com a API AnimatedVectorDrawable ou a API de suporte AnimatedVectorDrawableCompat.

Porém eu achei prudente não falar de animações de vetores já neste conteúdo.

Motivos?

  • Primeiro porque todo o conteúdo ficaria exaustivo, tendo em mente que somente a parte de vetores estáticos já é grande o suficiente para um único e longo artigo aula;
  • Segundo porque os vetores estáticos, principalmente no contexto "ícones de sistema", são os vetores que realmente fazem diferença em projeto, diferença para a melhora dele;
  • Terceiro porque a API de vetores animados não é tão robusta quanto a Lottie API, por exemplo, que permite animações mais complexas em arquivos mais leves.

As animações em vetores serão sim o conteúdo de um artigo aula futuro aqui do Blog, não se preocupe com isso.

Mas já lhe adianto que se você entender bem tudo que está sendo apresentado neste conteúdo e também conhecer ou a API ObjectAnimator ou a API AnimatorSet (ambas nativas Android)...

... conhecendo isso, então o seu trabalhado com animações de vetores no Android não precisará aguardar esse novo conteúdo.

Pontos negativos

Mesmo que já informado anteriormente, vamos recapitular alguns dos pontos negativos.

Na verdade vamos recapitular algumas das barreiras que fazem com que vetores não sejam sempre uma melhor solução frente a imagens rasterizadas:

  • A renderização em tela é mais pesada do que quando comparada à renderização de imagens rasterizadas;
  • Quando as proporções são maiores do que 200dp x 200dp o consumo de memória disponível e o tempo de renderização faz com que vetores não sejam nem de perto uma boa opção;
  • Imagens com muitos detalhes, como fotos de pessoas, por exemplo. Essas imagens vetoriais têm um tamanho em bytes muito maior do que quando comparadas às suas versões rasterizadas;
  • Quando atendendo à versões do Android abaixo da API 21, então para cada imagem vetorial é criada seis novas versões rasterizadas (ldpi, mdpi, hdpi, xhdpi, xxhdpi e xxxhdpi). Essas versões rasterizadas é que serão utilizadas em versões abaixo do Android Lollipop. Isso, mesmo que pouco, aumenta o tamanho final do APK.

Pontos positivos

E assim os pontos positivos.

Quando realmente vale a pena (se torna um must e não um should) o uso de vetores estáticos:

  • Para o contexto "ícones de sistema" o uso de vetores é sem sombra de dúvidas a melhor opção. Lembrando que este contexto é comum a qualquer aplicativo, pois todos têm ícones de sistema;
  • É necessário somente um arquivo que pode ser utilizado com eficiência e eficácia em qualquer configuração de tela;
  • Devido à necessidade de somente um arquivo de vetor para cada imagem, a manutenção do projeto fica bem mais simples;
  • Os arquivos de vetores costumam ter uma fração do tamanho em bytes do conjunto de ao menos quatro imagens rasterizadas (mdpi, hdpi, xhdpi e xxhdpi) que seriam utilizadas para cada imagem não vetorial presente em projeto.

Repositório do projeto

O projeto Android que foi utilizado para rodar os algoritmos apresentados até este ponto do artigo...

... esse projeto pode ser acessado no repositório público a seguir:

Slides

A seguir você tem a versão em slides de todo o conteúdo de Vetores Android que foi apresentado até este ponto do artigo.

É exatamente o mesmo conteúdo:

Projeto Android

Como projeto de exemplo, para facilitar o entendimento de como e quando aplicar vetores estáticos em projeto...

... esse exemplo será parte de um aplicativo Android de acampamento de verão, onde a tela de itinerário (um grid menu) estará disponível para o usuário poder construir o roteiro dele em camping.

Apresentação do aplicativo Android de exemplo

Nosso objetivo será trocar os ícones rasterizados, PNG, por ícones de vetores. Consequentemente tendo como ganho ao menos:

  • Menor arquivo APK;
  • E o principal ➙ facilidade em manutenções futuras na parte de design do projeto.

Todo o projeto de exemplo será desenvolvido em duas partes:

  • Primeiro vamos a configuração total de projeto, porém utilizando imagens rasterizadas;
  • Depois apenas vamos trocar os tipos de ícones de sistema para vetores... e realizar testes.

Você pode acessar toda a configuração do projeto no GitHub público dele em:

De qualquer forma, recomendo que você estude todo o projeto pelo artigo para entender cada um dos detalhes abordados.

Pois é aqui que eles são explicados passo a passo.

Protótipo estático

O protótipo estático é, se pouco, a parte mais importante do lado estratégico de um projeto de aplicativo Android.

O lado estratégico contém todas as tarefas que precisam ser feitas antes mesmo da primeira linha de código ser colocada em IDE.

Com o protótipo estático, como já venho falando em inúmeros artigos do Blog, a fase de codificação (lado tático do projeto) é facilitada consideravelmente.

Um protótipo estático bem construído faz com que as dúvidas em fase de desenvolvimento sejam praticamente nulas.

Então a seguir o protótipo estático de nosso projeto Android de exemplo:

Tela de opções de itinerário (sem seleção)

Tela de opções de itinerário (sem seleção).

Tela de opções de itinerário (com seleção)

Tela de opções de itinerário (com seleção).

Iniciando um novo aplicativo

Em seu ambiente de desenvolvimento, sua instalação do Android Studio, inicie um novo projeto Kotlin Android como a seguir:

  • Nome da aplicação: Yosemite Summer Camp;
  • API mínima: 16 (Android Jelly Bean) - mais de 99% dos aparelhos Android em mercado sendo atendidos;
  • Atividade inicial: Basic Activity;
  • Nome da atividade inicial: ItineraryActivity;
  • Para todos os outros campos, mantenha os valores já definidos por padrão.

Configurações Gradle

A seguir as configurações do nosso Gradle Nível de Projeto, ou build.gradle (Project:YosemiteSummerCamp):

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

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

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

 

E então as configurações do nosso Gradle Nível de Aplicativo, ou build.gradle (Module: app):

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

android {
compileSdkVersion 30
buildToolsVersion "30.0.0"

defaultConfig {
applicationId "thiengo.com.br.yosemitesummercamp"
minSdkVersion 16 /* Android Jelly Bean */
targetSdkVersion 30
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'

implementation 'com.google.android.material:material:1.1.0'
}

 

Se no tempo que você estiver consumindo este artigo já houver versões mais atuais das bibliotecas, plugins e Android API referenciadas acima.

Então utilize as versões mais atuais, pois o projeto deverá continuar executando sem problemas.

Strings de sistema

A seguir o arquivo de Strings estáticas de nosso projeto, mais precisamente o arquivo /res/values/strings.xml:

<resources>
<string name="app_name">
Yosemite Summer Camp
</string>

<string name="new_itinerary">
Criar um novo itinerário
</string>

<string name="activity_schedule">
Escolha as atividades que você adicionará
à sua agenda de acampamento de verão.
</string>

<string name="tourist">
Ícone turista
</string>

<string name="item_selected">
Ícone item selecionado
</string>

<string name="button_continue">
Continuar
</string>
</resources>

 

Lembrando da importância deste arquivo principalmente para a manutenção do projeto.

Se a String é estática, não sofre atualizações por parte do usuário, então ela deve estar no arquivo de Strings.

Isso pensando também no momento de internacionalização do aplicativo, onde outros idiomas serão necessários.

Configurações AndroidManifest

Abaixo a configuração completa do AndroidManifest.xml de nosso projeto Android de exemplo:

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

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

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

<intent-filter>

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

 

Como a atividade ItineraryActivity é basicamente uma tela de menu vertical, onde alguns componentes visuais são apresentáveis somente na vertical...

... como isso já está definido desde o protótipo estático, então achei prudente permitir o uso dessa atividade somente no modo portrait, colocando em <activity> o atributo android:screenOrientation="portrait".

Mas é aquilo, para travas de tela em portrait ou em landscape é aconselhável que você tenha boas explicações.

Pois travando a tela você estará interferindo diretamente na experiência do usuário (user experience - UX).

Ícones de abertura do app

Ainda é preciso colocar em projeto os ícones de abertura de aplicativo. No caso os ícones que aparecem na tela de apps do aparelho Android.

Primeiro o ícone em versão retangular, ic_launcher.png:

Ícone retangular de lançamento do aplicativo Android

Então o ícone em versão circular, ic_launcher_round.png:

Ícone circular de lançamento do aplicativo Android

Aqui não trabalharemos com ícones adaptativos de aplicativo. Mesmo sabendo que eles também podem fazer uso de drawables vetoriais.

Ícones adaptativos vão ser assunto de um artigo aula futuro.

Configurações de estilo

Agora os arquivos responsáveis pelo estilo, tema, do projeto Yosemite Android.

Cores

Primeiro o arquivo de definição de cores, mais precisamente o arquivo /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Branca. -->
<color name="colorPrimary">#FFFFFF</color>

<!-- Branca. -->
<color name="colorPrimaryDark">#FFFFFF</color>

<!-- Laranja. -->
<color name="colorAccent">#FF774B</color>

<!-- Cinza escuro. -->
<color name="colorDarkGrey">#414651</color>

<!-- Cinza médio. -->
<color name="colorMediumGrey">#BBBBBB</color>

<!-- Roxo escuro. -->
<color name="colorDarkPurple">#422E8C</color>
</resources>

Dimensões

Agora o arquivo de dimensões, mais precisamente o arquivo /res/values/dimnes.xml:

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

 

Note que neste arquivo coloquei somente a medição que é repetida inúmeras vezes em projeto. Repetida no mesmo contexto e em lugares distintos:

  • /res/layout/menu_item.xml;
  • /res/drawable/cv_background_normal.xml;
  • e /res/drawable/cv_background_selected.xml.

Continue com a leitura. Logo logo chegaremos nos arquivos XML acima.

Tema

Por fim os arquivos de estilo, tema.

Primeiro o arquivo de definição de estilo comum a todas as versões do Android que podem executar o aplicativo.

Mais precisamente o arquivo /res/values/styles.xml:

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

<!-- Tema base do aplicativo. -->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

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

<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light" />

<!--
O tema abaixo foi criado para que fosse possível mudar
a cor do ícone ao lado esquerdo na barra de topo.

O Jetpack Compose vai acabar com essa "dor de cabeça".
-->
<style
name="AppTheme.ToolbarTheme"
parent="@style/ThemeOverlay.AppCompat.ActionBar">

<item name="colorControlNormal">@color/colorDarkGrey</item>
</style>

<!--
O tema abaixo foi criado para as Views do layout que
têm a função de colocar efeito de fade no framework
de lista (aqui o RecyclerView).
-->
<style name="AppTheme.Shadow">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">18dp</item>
<item name="android:background">@drawable/white_gradient_fade</item>
</style>
</resources>

 

Não se preocupe agora com o entendimento do arquivo white_gradient_fade.xml que está definido no tema AppTheme.Shadow.

Logo vamos à construção dele, assim que chegarmos ao layout de item de menu.

Agora vamos a versão de styles.xml que sobrescreve o que é necessário no tema para um melhor fit do aplicativo em aparelhos Android a partir da versão 23, Marshmallow.

Segue configuração de /res/values-v23/styles.xml:

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

<!--
Configuração extra de tema que será aplicada somente
em aparelhos com o Android 23 (Marshmallow) ou superior.
-->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

<!--
Coloca a StatusBar com fundo branco e ícones em
um tom de cinza escuro.
-->
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>

Ícones de sistema

Com o protótipo estático já definido, na fase estratégica do projeto, é possível colocar inúmeros recursos em projeto Android antes mesmo da primeira linha de código dinâmico.

E um desses recursos é todo o pacote de ícones de sistema.

E nesta primeira parte do projeto nós estaremos utilizando somente imagens rasterizadas, como informado logo no início dele.

Sendo assim, devido à não padronização das densidades de telas de aparelhos Android...

... devido a isso, ao invés de ter cada imagem rasterizada em quatro folders drawable, teremos na verdade cinco versões de cada imagem.

São elas:

  • /res/drawable-mdpi;
  • /res/drawable-hdpi;
  • /res/drawable-xhdpi;
  • /res/drawable-xxhdpi;
  • /res/drawable-xxxhdpi.

Assim é certo, enquanto não temos o trabalho com vetores, que nosso aplicativo não sofrerá com o problema de pixalagem.

Como assim: "(...) não padronização das densidades de telas"

É isso mesmo.

Na própria documentação oficial da API de vetores do Android está explícito que:

Há inúmeros modelos de aparelhos que rodam o Android e que a densidade de tela pode estar entre as classes de DPIs padrões definidos em sistema.

É possível ter aparelho rodando o seu aplicativo one a densidade de tela dele esteja entre xhdpi e xxhdpi, mas não exatamente em nenhuma das duas classes de DPI.

Neste caso as imagens em xxhdpi é que serão utilizadas, pois assim não há possibilidade de pixalagem e consequentemente quebra de design.

E está aí outra vantagem de uso de vetores ao invés de imagens rasterizadas:

Os drawables vetoriais vão se encaixar perfeitamente em qualquer densidade de pixels em tela. Mesmo as telas de densidades não padrões.

Origem e download

Todos os ícones de sistema foram baixados (as versões gratuitas deles) do Flaticon.

Os ícones foram baixados no tamanho 512px x 512px e depois convertidos cada um nas cinco versões DPIs necessárias utilizando a ferramenta Generic icon generator do site Android Asset Studio.

Ainda nesta primeira parte do projeto não colocarei aqui o link para download dos ícones de sistema, pois eles permanecerão por tempo limitado em projeto.

Na segunda parte, ainda neste artigo, você terá os links para download dos ícones vetoriais.

Pois quando você estiver lendo este artigo o projeto no GitHub já será o projeto final, sem imagens rasterizadas.

Então vamos aos ícones (rasterizados).

Ícones de topo

Seguindo o protótipo estático, temos na barra de topo o ícone ic_yosemite.png, de 24dp x 24dp:

Ícone ic_yosemite

Logo abaixo do principal rótulo do aplicativo temos o ícone ic_tourist.png, de 34dp x 34dp:

Ícone ic_tourist

Posteriormente neste artigo aula você vai entender o porquê do ícone acima ser o único que não é na cor cinza claro (#BBBBBB).

Ícones do menu "opções de itinerário"

Sim, são exatos 12 ícones em 5 versões cada um. Um total de 60 imagens rasterizadas 😭. Todos com densidade em 34dp x 34dp.

O primeiro é o ícone Acampamento, ic_camp.png:

Ícone Acampamento

O segundo é o ícone Pescaria, ic_fishing.png:

Ícone Pescaria

O terceiro é o ícone Fazer as malas, ic_packing.png:

Ícone Fazer as malas

O quarto é o ícone Trilha, ic_forest.png:

Ícone Trilha

O quinto é o ícone Transporte, ic_transport.png:

Ícone Transporte

O sexto é o ícone Rafting, ic_rafting.png:

Ícone Rafting

O sétimo é o ícone Rádio, ic_radio.png:

Ícone Rádio

O oitavo é o ícone Café, ic_coffee.png:

Ícone Café

O nono é o ícone Telescópio, ic_telescope.png:

Ícone Telescópio

O décimo é o ícone Rapel, ic_rapel.png:

Ícone Rapel

O décimo primeiro é o ícone Cart, ic_cart.png:

Ícone Cart

E por fim, o décimo segundo, é o ícone Surf, ic_surfboard.png:

Ícone Surf

Ícone de item selecionado

Ainda tem o ícone que aparece em tela para cada item que foi selecionado pelo usuário:

Opção selecionada de itinerário

Este é o ícone ic_selected.png, de 28dp x 28dp:

Ícone ic_selected

Classes de domínio

Teremos duas classes de domínio para representar respectivamente:

  • Um item de menu;
  • e O status do item de menu em projeto.

Uma dessas classes na verdade é um enum.

Escolhi um enum, pois o código fica com as responsabilidades melhor divididas e não há perda na leitura de código.

No pacote /domain (crie este pacote na raiz de classes do projeto) coloque o enum MenuItemStatus como a seguir:

package thiengo.com.br.yosemitesummercamp.domain

enum class MenuItemStatus {
SELECTED,
NOT_SELECTED
}

 

E então a classe que representa os itens de menu, MenuItem:

package thiengo.com.br.yosemitesummercamp.domain

class MenuItem(
val label: String,
val icon: Int,
var isSelected: MenuItemStatus = MenuItemStatus.NOT_SELECTED )

Pacote de dados

Como estamos trabalhando com apenas parte de um aplicativo real, então vamos utilizar dados simulados (mock data).

Nossa "base de dados" estará no pacote /data (crie este pacote na raiz de classes do projeto).

Essa persistência será a classe Menu, como a seguir:

class Menu {
companion object{

const val NUMBER_COLUMNS = 3

fun getItems()
= listOf(
MenuItem(
label = "Acampamento",
icon = R.drawable.ic_camp
),
MenuItem(
label = "Pescaria",
icon = R.drawable.ic_fishing
),
MenuItem(
label = "Fazer as malas",
icon = R.drawable.ic_packing
),
MenuItem(
label = "Trilha",
icon = R.drawable.ic_forest
),
MenuItem(
label = "Transporte",
icon = R.drawable.ic_transport
),
MenuItem(
label = "Rafting",
icon = R.drawable.ic_rafting
),
MenuItem(
label = "Rádio",
icon = R.drawable.ic_radio
),
MenuItem(
label = "Café",
icon = R.drawable.ic_coffee
),
MenuItem(
label = "Telescópio",
icon = R.drawable.ic_telescope
),
MenuItem(
label = "Rapel",
icon = R.drawable.ic_rapel
),
MenuItem(
label = "Cart",
icon = R.drawable.ic_cart
),
MenuItem(
label = "Surf",
icon = R.drawable.ic_surfboard
)
)
}
}

 

Note que estou utilizando parâmetros nomeados:

...
MenuItem(
label = "Acampamento",
icon = R.drawable.ic_camp
)
...

 

Sempre que possível estarei fazendo isso em código, pois facilita a leitura de todo o algoritmo.

Quando parâmetros nomeados não estiverem sendo utilizados neste projeto de exemplo é porque o código nativo do método está em Java, linguagem que não dá suporte e está característica.

Pacote adapter

Neste projeto vamos trabalhar com um pacote adapter.

É prudente separar a classe adaptadora de itens da classe ViewHolder, isso quando o código começa a ficar grande.

E sim, enquanto o Jetpack Compose não é liberado ao menos em versão beta (depende da época em que você estiver consumindo este conteúdo) nós vamos manter o uso da antiga e conhecida Toolkit UI via XML.

Aliás, já vai se despedindo do RecyclerView e de outros populares frameworks de lista.

Pois ao menos a API AdapterList já está pronta no Jetpack Compose e o boilerplate code (incluindo toda a configuração do ViewHolder) em pouco tempo não mais fará parte de nossa vida como profissional desenvolvedor de aplicativos Android.

É isso... vamos aos códigos.

Layout de item

O layout de item tem algumas Views aninhadas. Isso soa ruim quando temos a possibilidade de dezenas de itens em lista.

Mas aqui teremos somente 12, então não haverá riscos eminentes de um OutOfMemoryException.

Segue o layout XML /res/layout/menu_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginBottom="25dp"
android:orientation="vertical">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:id="@+id/cv_icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="21dp"
app:cardCornerRadius="@dimen/card_view_radius"
app:cardElevation="12dp">

<ImageView
android:id="@+id/iv_icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_gravity="center"
android:layout_marginTop="28dp"
android:layout_marginBottom="28dp"
android:tint="@color/colorDarkPurple" />
</androidx.cardview.widget.CardView>

<View
android:id="@+id/vv_gradient"
style="@style/AppTheme.Shadow"
android:layout_width="match_parent"
android:layout_height="41dp"
android:layout_gravity="top"
android:elevation="13dp" />

<ImageView
android:id="@+id/iv_selected"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="top|end"
android:contentDescription="@string/item_selected"
android:elevation="14dp"
android:src="@drawable/ic_selected" />
</FrameLayout>

<TextView
android:id="@+id/tv_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/colorDarkGrey"
android:textStyle="bold" />
</LinearLayout>

 

Note que no layout acima o atributo android:elevation é utilizado em:

...
<View
android:id="@+id/vv_gradient"
...
android:elevation="13dp" />

<ImageView
android:id="@+id/iv_selected"
...
android:elevation="14dp" />
...

 

Para que estas visualizações fiquem sobre o CardView.

Caso contrário, a partir do Android 21, o uso do atributo app:cardElevation no CardView com qualquer valor maior do que zero faria com que a regra de empilhamento de Views do FrameLayout fosse ignorada e assim o Cardview ficaria em cima das outras visualizações...

... mesmo ele sendo a primeira visualização filha dentro do ViewRoot, FrameLayout.

Note que o uso de app:cardElevation foi necessário, pois é este atributo que coloca a sombra no CardView como definido em protótipo estático.

A seguir o diagrama do layout anterior:

Diagrama do layout menu_item.xml

A sombra no CardView

Ainda em /res/layout/menu_item.xml, mais precisamente na View com função de eliminar parte da sombra do card.

Nessa View temos o estilo AppTheme.Shadow sendo referenciado via atributo style.

A seguir vamos recapitular este estilo:

...
<style name="AppTheme.Shadow">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">18dp</item>
<item name="android:background">@drawable/white_gradient_fade</item>
</style>
...

 

Visualizando o tema acima, temos o background white_gradient_fade.xml.

Background definido em /res/drawable/white_gradient_fade.xml com a seguinte estrutura:

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

<gradient
android:type="linear"
android:angle="270"
android:startColor="#fff"
android:endColor="@android:color/transparent" />
</shape>

 

Com esse background definido em uma View posicionada de maneira estratégica em layout, conseguimos uma visualização suave do item:

Item de menu com efeito de fade na sombra

Sem ele teríamos:

Item de menu sem efeito de fade na sombra

A sombra na parte de cima do card ficaria mais intensa, fugindo do que foi definido em protótipo.

ViewHolder

Agora o código do ViewHolder, dentro do pacote /adapter (crie este pacote na raiz de classes do projeto).

Aqui o ViewHoder terá o rótulo MenuViewHolder:

package thiengo.com.br.yosemitesummercamp.adapter

import android.graphics.Color
import android.graphics.PorterDuff
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import thiengo.com.br.yosemitesummercamp.R
import thiengo.com.br.yosemitesummercamp.domain.MenuItem
import thiengo.com.br.yosemitesummercamp.domain.MenuItemStatus

class MenuViewHolder(
val adapter: MenuAdapter,
val changeButtonStatusCallback: (List<MenuItem>)->Unit,
itemView: View ) : RecyclerView.ViewHolder( itemView ), View.OnClickListener {

var cvIcon: CardView
var ivSelected: ImageView
var vvGradient: View
var ivIcon: ImageView
var tvLabel: TextView

init {
itemView.setOnClickListener( this )

cvIcon = itemView.findViewById( R.id.cv_icon )
ivSelected = itemView.findViewById( R.id.iv_selected )
vvGradient = itemView.findViewById( R.id.vv_gradient )
ivIcon = itemView.findViewById( R.id.iv_icon )
tvLabel = itemView.findViewById( R.id.tv_label )
}

fun setModel( item: MenuItem ) {
tvLabel.text = item.label
ivIcon.setImageResource( item.icon )
ivIcon.contentDescription = item.label

setStyle( item = item )
}

private fun setStyle( item: MenuItem ){
/*
* Até a última declaração de variável mutável (var)
* tem toda a definição de estilo de um
* "item não selecionado" (isSelected == false) que
* é o valor inicial de cada item de itinerário em
* tela.
* */
var cvBackgroundResource = R.drawable.cv_background_normal
var ivSelectedVisibility = View.INVISIBLE
var vvGradientVisibility = View.VISIBLE
var tvLabelColor = R.color.colorDarkGrey
var ivIconColor = ResourcesCompat.getColor(
adapter.context.resources,
R.color.colorDarkPurple,
null
)

if( item.isSelected == MenuItemStatus.SELECTED ){
/*
* A seguir toda a definição de valores de um
* "item selecionado" (isSelected == true).
* */
cvBackgroundResource = R.drawable.cv_background_selected
ivSelectedVisibility = View.VISIBLE
vvGradientVisibility = View.INVISIBLE
ivIconColor = Color.WHITE
tvLabelColor = R.color.colorDarkPurple
}

cvIcon.setBackgroundResource( cvBackgroundResource )
ivSelected.visibility = ivSelectedVisibility
vvGradient.visibility = vvGradientVisibility

/*
* A invocação setColorFilter( ivIconColor, PorterDuff.Mode.SRC_IN )
* informa que somente os pixels não transparentes da
* imagem (dentro do ImageView) é que devem receber a
* cor definida em ivIconColor.
*
* A constante PorterDuff.Mode.SRC_IN é responsável pela
* regra de negócio "somente os pixels não transparentes (...)".
* */
ivIcon.setColorFilter( ivIconColor, PorterDuff.Mode.SRC_IN )

tvLabel.setTextColor(
ResourcesCompat.getColor(
adapter.context.resources,
tvLabelColor,
null
)
)
}

override fun onClick( view: View ) {
/*
* O algoritmo a seguir é responsável por colocar, no
* item acionado pelo usuário, o status e estilo adequados
* de acordo com o status atual do item.
*
* Com a função with() sendo utilizada nós podemos abreviar
* o código. Dentro do bloco desta função podemos utilizar
* "this" ao invés de "adapter.items[ adapterPosition ]".
* */
with( adapter.items[ adapterPosition ] ){

this.isSelected = when( this.isSelected ){
MenuItemStatus.SELECTED -> MenuItemStatus.NOT_SELECTED
else -> MenuItemStatus.SELECTED
}
}
adapter.notifyItemChanged( adapterPosition )

changeButtonStatusCallback( adapter.items )
}
}

 

Parece que o código é complexo, mas se você notar bem é muito código "b-aba". Só é grande.

Os arquivos de background do CardView

Ainda no código da classe MenuViewHolder.

Mais precisamente nos dois possíveis valores de background do CardView de item, valores de recurso que são colocados na variável cvBackgroundResource do método setStyle()...

... esses arquivos de background...

... primeiro o /res/drawable/cv_background_normal.xml:

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

<corners android:radius="@dimen/card_view_radius" />
<solid android:color="@android:color/white" />
</shape>

 

... depois o /res/drawable/cv_background_selected.xml:

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

<corners android:radius="@dimen/card_view_radius" />
<solid android:color="@color/colorDarkPurple" />
</shape>

 

Se esses backgrounds não forem utilizados dessa forma, incluindo a definição de android:radius (curvatura das pontas da figura retangular)...

... se isso não ocorrer e somente houver a mudança da cor de background do CardView, então o resultado será:

CardView sem drawable de background

Com a alternância dos backgrounds que têm também a definição do atributo radius, temos um resultado como definido em protótipo estático:

CardView com drawable de background

É isso mesmo:

Sem está estratégia de background o valor do atributo app:cardCornerRadius do CardView é ignorado.

Classe adaptadora

Por fim, ainda no pacote /adapter, crie a simples classe adaptadora MenuAdapter como a seguir:

package thiengo.com.br.yosemitesummercamp.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import thiengo.com.br.yosemitesummercamp.R
import thiengo.com.br.yosemitesummercamp.domain.MenuItem

class MenuAdapter(
val context: Context,
val items: List<MenuItem>,
val changeButtonStatusCallback: (List<MenuItem>)->Unit )
: RecyclerView.Adapter<MenuViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : MenuViewHolder {

val layout = LayoutInflater
.from( context )
.inflate(
R.layout.menu_item,
parent,
false
)

return MenuViewHolder(
adapter = this,
changeButtonStatusCallback = changeButtonStatusCallback,
itemView = layout
)
}

override fun onBindViewHolder(
holder: MenuViewHolder,
position: Int ){

holder.setModel(
item = items[ position ]
)
}

override fun getItemCount() = items.size
}

Atividade principal

A seguir os trechos finais de código desta primeira parte de nosso projeto Android de exemplo.

Layout de conteúdo

Abaixo o layout de conteúdo, mais precisamente o layout /res/layout/content_itinerary.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:text="@string/new_itinerary"
android:textColor="@color/colorDarkGrey"
android:textSize="34sp"
android:textStyle="bold" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="25dp"
android:drawableStart="@drawable/ic_tourist"
android:drawableLeft="@drawable/ic_tourist"
android:drawablePadding="8dp"
android:drawableTint="@color/colorMediumGrey"
android:text="@string/activity_schedule"
android:textColor="@color/colorMediumGrey" />

<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_menu_items"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="15dp"
android:paddingLeft="15dp"
android:paddingEnd="15dp"
android:paddingRight="15dp" />

<!--
A View abaixo faz o papel de "fade" ao topo do
FrameLayout, para que os itens em RecyclerView
tenham a aparência de que estão "saindo ou
entrando aos poucos" em tela e não de maneira
abrupta.
-->
<View
style="@style/AppTheme.Shadow"
android:layout_gravity="top" />

<!--
A View abaixo faz o mesmo papel de "fade de lista"
da View acima.

Porém devido aos atributos android:rotation="180"
e android:layout_gravity="bottom" o fade é no
fundo do FrameLayout.
-->
<View
style="@style/AppTheme.Shadow"
android:layout_gravity="bottom"
android:rotation="180" />
</FrameLayout>

<Button
android:id="@+id/bt_continue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorMediumGrey"
android:enabled="false"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/button_continue"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="normal" />
</LinearLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout content_itinerary.xml

Imagem embutida ao texto

Ainda no layout content_itinerary.xml, temos o uso de uma imagem embutida ao texto.

Tudo isso consumindo somente uma View, mais precisamente um TextView:

...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="25dp"
android:drawableStart="@drawable/ic_tourist"
android:drawableLeft="@drawable/ic_tourist"
android:drawablePadding="8dp"
android:drawableTint="@color/colorMediumGrey"
android:text="@string/activity_schedule"
android:textColor="@color/colorMediumGrey" />
...

 

Resultado:

TextView Android com um Drawable embutido

Note que o uso de android:drawableTint é eficaz somente a partir do Android API 23, Marshmallow.

Devido a essa limitação; devido ao minSdkVersion do projeto ser a API 16; e devido também ao uso de uma imagem rasterizada em android:drawableStart e em android:drawableStart...

... temos que também fornecer a imagem na cor correta (aqui, cinza médio, #BBBBBB).

Caso contrário, ao menos em aparelhos Android abaixo da API 23, o design não será fiel ao que foi definido em protótipo estático.

Com uma imagem vetorial, em caso de atualização de cor, nós somente mudaríamos o android:tint no arquivo de vetor.

Efeito fade nas extremidades verticais de lista

Continuando no layout content_itinerary.xml, temos novamente o efeito de fade.

Exatamente como já havíamos estudado lá na definição de layout de item de menu.

Aqui a proposta é que o efeito de fade não permita que os itens saiam ou entrem na tela de maneira abrupta.

Sem as duas Views ao final do FrameLayout que contém o RecyclerView:

...
<FrameLayout
...>

<androidx.recyclerview.widget.RecyclerView
... />

...
<View
style="@style/AppTheme.Shadow"
android:layout_gravity="top" />
...
<View
style="@style/AppTheme.Shadow"
android:layout_gravity="bottom"
android:rotation="180" />
</FrameLayout>
...

 

Sem estas Views o layout ficaria:

➙ Topo de lista:

Todo de lista de itens sem efeito fade

➙ Fundo de lista:

Fundo de lista de itens sem efeito fade

Com elas o layout fica:

➙ Topo de lista:

Todo de lista de itens com efeito fade

➙ Fundo de lista:

Fundo de lista de itens com efeito fade

Layout principal

Agora o layout principal, mais precisamente o layout /res/layout/activity_itinerary.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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"
android:background="@android:color/white"
tools:context=".ItineraryActivity">

<!--
O app:elevation="0dp" faz com que a barra de topo
não tenha uma sombra abaixo dela.

Exatamente como definido no protótipo estático.
-->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
app:elevation="0dp">

<!--
A definição android:theme="@style/AppTheme.ToolbarTheme"
tem o código para mudar a cor do ícone button que está
no lado esquerdo da barra de topo.

A definição app:navigationIcon="@drawable/ic_yosemite"
coloca em barra de topo, mais precisamente no lado
esquerdo dela, o ícone personalizado do aplicativo.

Exatamente como definido no protótipo estático.
-->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/AppTheme.ToolbarTheme"
app:navigationIcon="@drawable/ic_yosemite"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>

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

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

A seguir o diagrama do layout principal:

Diagrama do layout activity_itinerary.xml

ItineraryActivity

E enfim o código Kotlin da atividade principal, ItineraryActivity:

package thiengo.com.br.yosemitesummercamp

import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.android.synthetic.main.activity_itinerary.*
import kotlinx.android.synthetic.main.content_itinerary.*
import thiengo.com.br.yosemitesummercamp.adapter.MenuAdapter
import thiengo.com.br.yosemitesummercamp.data.Menu
import thiengo.com.br.yosemitesummercamp.domain.MenuItem
import thiengo.com.br.yosemitesummercamp.domain.MenuItemStatus

class ItineraryActivity : AppCompatActivity() {

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

/*
* HackCode para colocar um ícone no canto superior
* esquerdo da tela (lado esquerdo da Toolbar).
* */
supportActionBar?.setDisplayHomeAsUpEnabled( true );
supportActionBar?.setDisplayShowHomeEnabled( true );

initItineraryMenu()
}

private fun initItineraryMenu(){

val layoutManager = GridLayoutManager(
this,
Menu.NUMBER_COLUMNS
)
rv_menu_items.layoutManager = layoutManager

rv_menu_items.setHasFixedSize( true )
rv_menu_items.adapter = MenuAdapter(
context = this,
items = Menu.getItems(),
changeButtonStatusCallback = {
items -> changeButtonStatus( items )
}
)
}

/*
* Método responsável por atualizar o status de clique e o
* background do botão "CONTINUE".
*
* Se houver ao menos um item selecionado, então o botão
* fica como "disponível para clique", com o background
* laranja (bt_orange_ripple) aplicado a ele.
* */
private fun changeButtonStatus( items: List<MenuItem> ){

var isEnabled = false
var backgroundId = R.color.colorMediumGrey

/*
* O método any() precisa encontrar somente um item que
* retorne true para o predicato
* "it.isSelected == MenuItemStatus.SELECTED" que assim
* ele para com a execução e retorna true para a variável
* status.
* */
val status = items.any{
it.isSelected == MenuItemStatus.SELECTED
}

if( status ){
isEnabled = true

/*
* Abaixo da API 21 (Lollipop) do Android não é possível
* utilizar a API Ripple. Uma exceção será gerada caso
* isso ocorra.
*
* Por isso o bloco condicional a seguir.
* */
backgroundId = if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ){
R.drawable.bt_orange_ripple
}
else{
R.color.colorAccent
}
}

bt_continue.isEnabled = isEnabled
bt_continue.background = ResourcesCompat.getDrawable(
resources,
backgroundId,
null
)
}
}

 

E para simplificar...

... segue o código do arquivo /res/drawable/bt_orange_ripple.xml que é utilizado como possível background de botão no método changeButtonStatus():

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

<item>
<shape android:shape="rectangle">
<solid android:color="@color/colorAccent" />
</shape>
</item>
</ripple>

 

Assim nossa configuração de classes de projeto está como a seguir:

Estrutura física final do projeto Android de exemplo

Com isso podemos citar, novamente, alguns problemas nesta primeira versão de app Android.

Arquivo executável e manutenção

Rodando o "Optimize Imports". Logo depois acionando o "Clean Project" e por fim executando o projeto.

Temos um executável com o tamanho de 3.126.712 bytes (3.1 MB) 💽.

Ou seja, ao menos para essa única tela de projeto não tem problema algum em manter todos os ícones como imagens rasterizadas.

Ok! Mas e a manutenção do aplicativo?

E se for necessário atualizar o tamanho dos "botões opções" em menu itinerário? Deixando eles maiores, pois teremos duas colunas e não mais três colunas.

Neste caso será necessário atualizar pelo menos 65 imagens 😩. Isso levando em consideração somente uma tela de todo o aplicativo e somente as imagens presentes no layout de lista de itens dessa tela.

Sendo assim... vamos à melhoria.

Imagens rasterizadas por vetores

Vamos realizar está atualização.

Ao menos em tempo de manutenção de projeto teremos uma melhora considerável quando utilizando, para ícones de sistema, drawables vetoriais ao invés de drawables rasterizados.

Colocando os drawables vetoriais

O download das imagens vetoriais (todas SVG) foi feito no mesmo local que as imagens rasterizadas. No Flaticon.

Todas as imagens têm os mesmos rótulos que suas versões rasterizadas.

Vou lhe poupar o trabalho de importar cada imagem SVG em projeto utilizando o Vector Asset Studio. No caso importar cada imagem com a configuração correta.

Isso, pois temos que ter em mente que o nosso aplicativo dá suporte a partir da API 16 do Android e que com a não utilização da configuração vectorDrawables.useSupportLibrary = true...

... com essa náo utilização os vetores terão suas versões rasterizadas sendo criadas com base no que está definido em cada um dos XMLs estruturais deles.

Vou lhe poupar isso e abaixo já deixo os links para download de cada vetor (já convertido) que será utilizado em projeto.

Coloque todos os vetores XML baixados no folder /res/drawable:

Com todos os vetores em projeto, você seguramente pode remover por completo os folders a seguir. Pois neste aplicativo somente tem nesses folders os ícones de sistema:

  • /res/drawable-mdpi;
  • /res/drawable-hdpi;
  • /res/drawable-xhdpi;
  • /res/drawable-xxhdpi;
  • /res/drawable-xxxhdpi.

Done!

Felizmente, devido à possibilidade de trabalho com vetores utilizando as APIs de Drawable e de recursos (Int), não precisamos mudar absolutamente nada em código.

Com a atualização finalizada podemos ir aos testes.

Testes e resultados

Antes de executar o aplicativo siga os passos:

  • Acesse o menu de topo do Android Studio;
  • Vá em "Build" e logo depois clique em "Clean Project";
  • Ao final do clean vá em "Build" novamente e agora clique em "Rebuild Project".

Agora sim execute o projeto em seu emulador ou aparelho real de testes:

Animação do app de exemplo Android Yosemite

O executável final tem 3.117.254 bytes (3.1 MB) 💽 😤.

É isso mesmo, muito próximo de quando utilizando somente imagens rasterizadas.

Para entender melhor isso, caso ainda restem dúvidas, volte a seção O problema do vectorDrawables.useSupportLibrary = true.

Com isso finalizamos o estudo de toda a parte de vetores Android necessária a um engenheiro desenvolvedor mobile.

Não deixe de se inscrever na 📩 lista de emails do Blog para receber em primeira mão conteúdos Android exclusivos e também em suas versões PDF (liberadas somente aos inscritos da lista de e-mails).

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

GitHub do projeto

O projeto de exemplo, já atualizado com a segunda parte do artigo, pode ser acessado no seguinte GitHub:

Vídeos

A seguir um mini-curso em vídeo sobre Vetores Android...

... mini-curso com o exato mesmo projeto Android acima, sendo atualizado para uso de drawables vetoriais ao invés de drawables rasterizados:

Conclusão

Precisando de ilustrações e, principalmente, ícones de sistema?

Não titubeie! Ao menos para ícones de sistema você certamente terá que utilizar drawables vetoriais.

Não por causa apenas da possível diminuição em bytes do APK final, mas principalmente devido a facilidade de atualização de ícones em projeto.

Quanto às ilustrações:

Para aquelas que não têm muitos detalhes, que em bytes são menores do que suas versões rasterizadas e que não serão carregadas em proporções maiores do que 200dp x 200dp...

... para essas, mantenha o uso de vetores.

Caso contrário, siga com imagens rasterizadas, cinco versões para cada imagem.

Ao menos a sua renderização será "saudável".

Antes de finalizar, primeiro quero lhe parabenizar por ter concluído o conteúdo. Isso diz muito sobre o seu comprometimento como profissional desenvolvedor 💎🥇.

Acredite, são poucos que chegam até o final de qualquer artigo de desenvolvimento.

Então é isso.

Caso você tenha dúvidas ou sugestões sobre vetores no Android, deixe logo abaixo nos comentários.

Curtiu o conteúdo? Não esqueça de compartilha-lo.

E, por fim, não deixe de se inscrever na 📩 lista de emails para também garantir a versão em PDF de cada novo "artigo mini-curso".

Abraço.

Fontes

Visão geral de drawables vetoriais
Documentação oficial Android - VectorDrawable
Documentação oficial Android - Drawable
Documentação oficial Android - colorControlNormal
Animar gráficos drawable
Documentação oficial Android - PorterDuff.Mode
Documentação oficial Kotlin - Enum
DevBytes: Android Vector Graphics
Draw Me a Rainbow: Advanced VectorDrawable Rendering (Android Dev Summit '18)
Documentação oficial Mozilla Developer - Paths SVG
Understanding Android’s vector image format: VectorDrawable
Draw a Path: Rendering Android VectorDrawables
Vector drawable is the best practices for Android development
Android vectorDrawables.useSupportLibrary = true is stopping app - Resposta de Vipul Asri e Community
Unit of viewportWidth and viewportHeight in case VectorDrawable - Resposta de Bryan Herbst
Remove android.widget.Toolbar shadow - Resposta de Ferdous Ahamed
How to change color of Toolbar back button in Android? - Resposta Rajen Raiyarela
Display Back Arrow on Toolbar - Resposta de MrEngineer13 e Brais Gabin
Simple Android grid example using RecyclerView with GridLayoutManager (like the old GridView) - Resposta de Suragch
Android studio automatically open's documentation view - Resposta de Clocker
Android: How to change the ActionBar "Home" Icon to be something other than the app icon? - Resposta de Joe
How to make gradient background in android - Resposta de Suragch
How to set tint for an image view programmatically in android? - Resposta de Hardik e Vadim Kotov
layout_margin in CardView is not working properly - Resposta de zonda e pumpkinpie65
android layout: This tag and its children can be replaced by one <TextView/> and a compound drawable - Resposta de NPike
Android statusbar icons color - Resposta de Ritesh
How to set VectorDrawable as an image for ImageView programmatically - Resposta de Pramod Baggolli e Farbod Salamat-Zadeh
Add ripple effect to my button with button background color? - Resposta de Pei
Canvas (GUI)
Vector graphics
PSD File Format
Data Binding Para Vinculo de Dados na UI Android
Fundamentos da SVG, 1ª edição, Maurício Samy Silva (Novatec - 2012)

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Data Binding Para Vinculo de Dados na UI AndroidData Binding Para Vinculo de Dados na UI AndroidAndroid
Como Impulsionar o App Android - Compartilhamento NativoComo Impulsionar o App Android - Compartilhamento NativoAndroid
Annotation Span Para Estilização de Texto no AndroidAnnotation Span Para Estilização de Texto no AndroidAndroid
Android Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid

Compartilhar

Comentários Facebook

Comentários Blog

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