Lottie API Para Animações no Android
(12593) (4)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos ao estudo completo da Lottie API, biblioteca criada pelo AirBnb com o objetivo de processar com maior eficiência e eficácia animações em sistemas mobile e Web. Mais precisamente, aqui estudaremos a Lottie API no Android.
Para melhor aproveitamento do estudo da API vamos, ao final da apresentação dela, a construção de um aplicativo de fuso horário mundial, app similar ao desenvolvido no conteúdo sobre a True Time API.
Neste novo projeto teremos trechos importantes com animações que aumentarão o "glamour" dele:
Neste artigo o termo "API" será utilizado como sinônimo de "biblioteca" (library).
Antes de prosseguir, não esqueça de se inscrever 📩 na lista de emails do Blog para receber os conteúdos exclusivos e em primeira mão.
Abaixo os tópicos que estaremos estudando:
- Animação no Android;
- Lottie API:
- Não desanime, temos a LottieFiles.com;
- Instalação da API;
- Animação local, folder /assets, com LottieAnimationView;
- Animação local, folder /raw;
- Animação remota, utilizando lottie_url;
- Listener de atualização de animação;
- Listener de início, de fim, de repetição e de cancelamento de animação;
- Progresso de animação via propriedade progress;
- Trabalhando com frames;
- Trabalhando a velocidade (duração) da animação;
- Número e modelo de repetições;
- Modificando a cor com lottie_colorFilter;
- Animação com LottieDrawable;
- KeyPath para a animação de partes separadas;
- Mais opções de configuração;
- Tamanho (altura x largura) da animação;
- Pontos negativos;
- Pontos positivos;
- Considerações finais;
- Tutorias para criação de animações no After Effects.
- Projeto Android:
- Colocando animação no projeto:
- Slides;
- Vídeos;
- Conclusão;
- Fontes.
Animação no Android
Há inúmeras APIs, nativas e não nativas, somente para animação no Android. A ObjectAnimator e a Facebook Keyframes provavelmente estão entre as mais populares, digo, populares depois da Lottie API.
Com exceção da Lottie, todas as outras APIs para animação no Android vêm carregadas de limitações:
- Ou não é nada trivial a codificação para conseguir até mesmo simples animações sincronizadas;
- Ou as funcionalidades da API não permitem que tenhamos em projeto nada melhor do que um GIF (ou PNG animado) com qualidade abaixo da esperada.
Então a Lottie API é perfeita, não tem limitações?
Sim, ela tem limitações. Mas o suporte é bem mais completo do que, por exemplo, o Android Vector Drawable (AVD), API nativa que não suporta o mesmo número de características do After Effects que a Lottie API suporta.
Lottie API
A biblioteca Lottie foi criada pelos engenheiros de desenvolvimento do AirBnb com o propósito de permitir animações mais completas em sistemas mobile e Web.
Aqui o termo "mais completas" é sinônimo de: maior suporte às animações criadas no Adobe After Effects, hoje um dos principais softwares no mercado para a criação de animações digitais.
O nome Lottie é uma homenagem a uma diretora alemã, Lotte Reiniger, que é a criadora da mais antiga filmagem de longa metragem ainda existente, o filme The Adventures of Prince Achmed de 1926.
As animações que rodarão junto aos códigos Lottie de seu projeto devem ser criadas no After Effects e então exportadas como arquivos JSON, exportação realizada junto ao uso do plugin open source Bodymovin.
Não desanime, temos a LottieFiles.com
Essa parte de: criar animação; e exportar da maneira correta. Isso não é nada trivial para nós desenvolvedores de software.
Eu particularmente não acredito ser um bom investimento em carreira focar também nos estudos de criação de animações, até porque não é algo que, para gerar qualidade, será aprendido da noite para o dia, terá uma curva de aprendizagem de médio longo prazo nessa jornada.
Mas não se preocupe caso você não tenha o conhecimento sobre o After Effects e nem mesmo um profissional de animação ao seu lado.
Nós programadores temos um site somente com animações open source: LottieFiles.com
As animações são todas preparadas para rodar na API Lottie, animações já exportadas em JSON com o uso do After Effects e Bodymovin.
É possível até mesmo editar as cores da animação escolhida depois de acionar "Customize with Bodymovin Editor":
Não deixe de acompanhar o projeto de exemplo deste artigo, pois nele utilizaremos duas animações diretamente da LottieFiles.
Instalação da API
A API está no repositório jcenter(), logo, temos apenas que adicionar a referência a ela no Gradle App Level, ou build.gradle (Module: app):
...
dependencies {
implementation "com.airbnb.android:lottie:2.6.0-beta19"
}
Na época da construção deste artigo a versão mais atual e que rodava sem problemas as funcionalidades propostas pela API era a versão beta: 2.6.0-beta19.
Mas recomendo que somente utilize uma versão beta em seu projeto oficial, que irá a produção, se depois de uma bateria de testes for constatado que a API mesmo em versão beta roda sem problema algum.
Animação local, folder /assets, com LottieAnimationView
Agora a parte que mais empolga na API: os códigos simples para ter animações completas.
A LottieAnimationView é uma das classes presentes para executar animações, além de ser a mais recomendada na documentação oficial da Lottie API.
Antes de apresentar o código de exemplo vale informar que a LottieAnimationView herda de ImageView, ou seja, é possível utilizar todas as características de um ImageView utilizando somente a LottieAnimationView, mesmo para carregamento de imagens estáticas.
Assim um código XML de exemplo:
...
<!--
Animação local com arquivo JSON no folder /assets
-->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_android_wave_json"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_fileName="android_wave.json"
app:lottie_loop="true" />
...
Como com qualquer outra View, podemos trabalhar toda a criação e configuração da LottieAnimationView também em código dinâmico, em programação. Mas eu fortemente recomendo que você deixe o que é estático em arquivo de conteúdo estático, como um file XML.
Os atributos lottie_ são muitos, vamos a explicação dos utilizados em código anterior:
- lottie_autoPlay: inicia a animação dispensando a necessidade de invocação de playAnimation() em código;
- lottie_fileName: permite que tanto animações .json quanto animações em .zip possam ser carregadas diretamente do folder /assets. Caso a animação android_wave.json estivesse, por exemplo, dentro do folder /assets/minhas_animacoes então a referência em lottie_fileName seria "minhas_animacoes/android_wave.json";
- lottie_loop: indica se a animação vai continuar em repetição assim que finalizada.
Executando o código anterior, temos:
Confesso que fiquei confuso quando você informou sobre animações também em .zip. O que seria isso?
Um exemplo são as animações criadas com o uso de imagens, neste caso será exportado do After Effects, junto ao Bodymovin, um .zip contendo o arquivo JSON da animação vetorial e as imagens referenciadas neste arquivo JSON.
Animação local, folder /raw
Também é possível carregar animações locais diretamente do folder /drawable/raw, mas neste diretório somente animações .json é que serão carregadas. Veja o código de exemplo:
...
<!--
Animação local com arquivo JSON no folder /raw
-->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_windmill"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/windmill" />
...
Dessa vez o atributo de referência a animação é o lottie_rawRes. Executando o código anterior, temos:
Animação remota, utilizando lottie_url
Para facilitar ainda mais a vida do desenvolvedor Android, temos a possibilidade de carregar animações diretamente da rede.
Toda a configuração de cache e requisição remota é abstraída do domínio do desenvolvedor, somente temos de colocar a url correta e ver a animação acontecer. Segue código de exemplo:
...
<!--
Animação JSON remota. Animações em .zip também podem ser
carregadas de origem remota.
-->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_bullseye"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_url="https://raw.githubusercontent.com/airbnb/lottie-android/master/LottieSample/src/main/res/raw/bullseye.json" />
...
O atributo lottie_url também aceita referencias a arquivos .zip. Executando o código anterior, temos:
Thiengo, então se eu quiser carregar alguma imagem remota utilizando o lottie_url conseguirei sem problemas?
Não. O atributo lottie_url é somente para animações After Effects em .json ou .zip. Imagens ainda têm de ser carregadas via android:src, digo, imagens locais. Para imagens remotas você pode utilizar o Universal Image Loader.
Lembrando que para carregamento remoto de animação é preciso ao menos a permissão de Internet no AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest
...>
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
Importante: somente escolha por animações remotas se essa realmente for a melhor opção em seu domínio de problema, pois animações remotas dependem até mesmo da disponibilidade de banda do usuário.
Listener de atualização de animação
Para ouvir a cada nova atualização de frame na animação podemos implementar a Interface ValueAnimator.AnimatorUpdateListener como a seguir:
class MainActivity : AppCompatActivity(),
ValueAnimator.AnimatorUpdateListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
lav_time_zip.addAnimatorUpdateListener( this )
}
override fun onAnimationUpdate( animator: ValueAnimator? ) {
/* TODO */
}
}
Assuma que aqui estamos utilizando o plugin kottlin-android-extensions e assim a propriedade lav_time_zip é equivalente ao ID de uma LottieAnimationView em tela.
Enquanto a animação estiver ocorrendo o método onAnimationUpdate() é invocado.
O parâmetro animator, do tipo ValueAnimator, nos permite acesso (e modificação) a algumas configurações de animação. Mas segundo meus testes, realizar atualizações em animação, quando necessário, ainda tem como melhor caminho o uso da instância de LottieAnimationView.
Listener de início, de fim, de repetição e de cancelamento de animação
Para ouvir os triggers de início, de fim, de repetição e de cancelamento de animação devemos implementar a Interface Animator.AnimatorListener. Veja o código a seguir:
class MainActivity : AppCompatActivity(),
Animator.AnimatorListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
lav_time_zip.addAnimatorListener( this )
}
/*
* Depois do primeiro loop completo, então inicia a
* repetição e ai sim o método abaixo é invocado para
* cada nova repetição.
* */
override fun onAnimationRepeat( animator: Animator? ) {
/*
* Para qualquer um dos métodos de Animator.AnimatorListener
* podemos aplicar o casting "animator as ValueAnimator"
* pois animator é do tipo ValueAnimator.
* */
val valueAnimator = animator as ValueAnimator
/* TODO */
}
/*
* O método abaixo somente é invocado, ao final da animação, se
* a animação não estiver em loop.
* */
override fun onAnimationEnd( animator: Animator? ) {
/* TODO */
}
/*
* Assim que a animação é bloqueada (e não finalizada
* normalmente), mesmo com a abertura de uma outra
* atividade, o método abaixo é invocado.
* */
override fun onAnimationCancel( animator: Animator? ) {
/* TODO */
}
/*
* Método invocado assim que a animação inicia. Não funciona
* com animação que ocorre via propriedade progress em código
* dinâmico.
* */
override fun onAnimationStart( animator: Animator? ) {
/* TODO */
}
}
Como explicado em código acima, o parâmetro animator na verdade é também do tipo ValueAnimator como em onAnimationUpdate(), apresentado na seção anterior.
Importante: o conhecimento completo dos listerners Lottie é necessário, pois certamente você os utilizará para acionamento de funcionalidades de seu domínio de problema.
Progresso de animação via propriedade progress
É possível controlar o andamento da animação via código de programação, para isso temos de atualizar a propriedade progress como no código de exemplo a seguir.
Primeiro o trecho XML:
...
<!--
Animação controlada programaticamente pela propriedade
progress onde 0.0F é o valor mínimo e 1.0F é valor máximo.
-->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_sun_in_a_cloud"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="20dp"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:lottie_rawRes="@raw/sun_in_a_cloud" />
<SeekBar
android:id="@+id/sb_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
...
Então o código Kotlin (poderia ser Java, com quase as mesmas configurações):
class MainActivity : AppCompatActivity(),
SeekBar.OnSeekBarChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
...
sb_animation.setOnSeekBarChangeListener(this)
}
/*
* Listeners do SeekBar.
* */
override fun onProgressChanged( seekBar: SeekBar?, progress: Int, fromUser: Boolean ) {
lav_sun_in_a_cloud.progress = progress / 100F
}
override fun onStartTrackingTouch( seekBar: SeekBar? ) {}
override fun onStopTrackingTouch( seekBar: SeekBar? ) {}
}
Como informado em código XML: independente da duração da animação, o valor que deve ser abordado em progress deve estar entre, incluindo eles, 0.0F e 1.0F.
Executando o código anterior, temos:
O SeekBar foi utilizado para facilitar o exemplo, mas progress pode ser atualizado em qualquer contexto que esteja dentro da thread UI.
Trabalhando com frames
O trabalho com os frames da animação se faz necessário principalmente se você desenvolvedor Android for utilizar alguma animação baixada da Internet, ou seja, animação After Effects que não foi desenvolvida especificamente para o seu domínio de problema.
Com a configuração de frame é possível definir o ponto que a animação se inicia e o ponto que ela finaliza, mesmo se a configuração de loop estiver em true.
Antes de partirmos para o código de exemplo, veja quais são os métodos que permitem a configuração de frames:
- setMinFrame( Int ): permiti definir em qual parte, frame, a animação se iniciará. O argumento é um Int entre 0 e o último frame existente na animação;
- setMaxFrame( Int ): permiti definir em qual parte, frame, a animação finalizará. A configuração de argumento é como em setMinFrame();
- setMinAndMaxFrame( Int, Int ): permiti a definição de frame mínimo e máximo em animação;
- setMinProgress( Float ): permiti definir em qual ponto se iniciaria a animação. O argumento é do tipo Float e deve variar entre 0.0F e 1.0F;
- setMaxProgress( Float ): permiti definir em qual ponto se finalizará a animação. A configuração de argumento é a mesma em setMinProgress();
- setMinAndMaxProgress( Float, Float ): permiti, em uma única invocação, a definição de progresso mínimo e máximo;
- frame: propriedade que permite definir, ou obter, o frame atual da animação em tela.
Caso você não tenha em mãos a informação de total de frames presente em uma animação, é possível utilizar a propriedade maxFrame para saber o total de frames nela:
...
val totalFrames = lav_animacao.maxFrame
...
Note que você somente obterá o valor real em maxFrame se a animação já estiver renderizada em tela, ou seja, pode ser que um código similar ao seguinte seja necessário:
...
Thread{
kotlin.run {
SystemClock.sleep( 2000 ) /* Forçando um delay de 2 segundos. */
val totalFrames = lav_animacao.maxFrame
Log.i( "LOG", "Total frames: $totalFrames" )
}
}.start()
...
Com isso podemos ir ao código de exemplo. Primeiro a parte XML, sem ainda a definição de min e max frame:
...
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_video_cam"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/video_cam" />
...
Executando o código anterior, temos:
Agora a parte de programação, já com a definição de min e max frame:
...
lav_video_cam.setMinFrame( 50 )
lav_video_cam.setMaxFrame( 100 )
...
A animação em teste tem um total de 180 frames, mas no código acima cortamos isso. Executando o algoritmo com o trecho anterior incluído, temos:
Animação de duração bem inferior, de 180 frames (aproximadamente 6 segundos) para 50 frames (aproximadamente 1.8 segundos).
Trabalhando a velocidade (duração) da animação
Com a propriedade speed é possível definir em qual velocidade a animação vai trabalhar. speed aceita um valor Float, onde 1.0F é o valor da velocidade padrão. Valores menores que 1.0F diminuem a velocidade da animação e valores maiores aumentam.
A seguir o código XML de exemplo de uma LottieAnimationView sem alteração em speed:
...
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_isometric_gift_box"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/isometric_gift_box" />
...
Executando o código anterior, temos:
Agora o código que acelera a animação:
...
lav_isometric_gift_box.speed = 3.5F
...
Executando o código anterior, temos:
Uma animação de 4 segundos para agora aproximados 1.1 segundos.
Se um valor negativo for fornecido em speed, a animação trabalha de maneira invertida. Segue atualização do código anterior:
...
lav_isometric_gift_box.speed = -3.5F
...
Executando a última atualização, temos:
Número e modelo de repetições
É possível ser mais específico quanto a característica de loop e definir exatamente quantas vezes uma repetição deverá ocorrer. Também é possível definir o modo da repetição.
A seguir um LottieAnimationView de exemplo:
...
<com.airbnb.lottie.LottieAnimationView
android:visibility="visible"
android:id="@+id/lav_grab"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_rawRes="@raw/grab" />
...
Executando o trecho anterior, temos:
Agora definindo o repeatCount e o repeatMode:
...
lav_grab.repeatCount = 2
lav_grab.repeatMode = LottieDrawable.REVERSE
...
Executando novamente, agora com o trecho atualizado, temos:
A propriedade repeatCount define, ou retorna, quantas vezes a animação deve ser repetida. Se a contagem de repetições for 0, a animação nunca será repetida. Se a contagem de repetições for maior do que 0 ou então LottieDrawable.INFINITE, o modo de repetição será levado em consideração. O valor padrão é 0.
A propriedade repeatMode aceita os seguintes valores:
- LottieDrawable.RESTART: quando a animação chega ao fim e repeatCount é LottieDrawable.INFINITE ou um valor positivo, a animação é reiniciada;
- LottieDrawable.REVERSE: quando a animação chega ao final e repeatCount é LottieDrawable.INFINITE ou um valor positivo, a animação inverte a direção em cada nova iteração;
- LottieDrawable.INFINITE (esta constante tem real efeito quando em repeatCount): valor utilizado com a propriedade repeatCount para repetir a animação indefinidamente.
Modificando a cor com lottie_colorFilter
Para modificar a cor total de uma animação é tão simples quanto ativar / desativar o loop dela. A seguir o LottieAnimationView de uma animação com cores originais.
...
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_like"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/like" />
...
Executando o projeto com o código anterior, temos:
Agora o mesmo LottieAnimationView com o lottie_colorFilter configurado:
...
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_like"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_colorFilter="#3F51B5"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/like" />
...
Executando o projeto com a nova configuração de animação, temos:
Note que o efeito positivo de lottie_colorFilter ocorre quando a animação tem transparência também na parte interna dela, caso contrário ficará tudo de uma única cor, ou seja, não haverá animação.
Animação com LottieDrawable
Além da API LottieAnimationView temos a API LottieDrawable que resumidamente nos permite: colocar drawables animados em componentes que aceitam arquivos drawables.
A seguir um ImageView de exemplo, View que receberá uma animação presente no folder /assets:
...
<ImageView
android:visibility="visible"
android:scaleType="centerInside"
android:id="@+id/iv_animation"
android:layout_width="200dp"
android:layout_height="200dp" />
...
Agora o código dinâmico que ativa a animação no ImageView anterior:
...
val drawable = LottieDrawable()
LottieComposition.Factory.fromAssetFileName(
this, /* Contexto, atividade. */
"vigilance_camera.json" ) /* Path da animação no folder /assets. */
{ /* Lambda contendo um LottieComposition que permitirá a gerência via LottieDrawable. */
it -> drawable.composition = it
}
/* Iniciando a animação. */
drawable.playAnimation()
/* Colocando a animação, já ativada, no ImageView. */
iv_animation.setImageDrawable( drawable )
...
Executando o código anterior, temos:
É importante ressaltar que o recomendado em documentação é o uso da API LottieAnimationView. São raros os casos onde a LottieDrawable poderá ser utilizada e a LottieAnimationView não.
Eu particularmente me interessei pela LottieDrawable para utiliza-la junto a alguma Spanned String, pois sei que é possível colocar drawables nesse tipo de String.
Porém em meus testes foi constatado que um drawable animado não é carregado em uma Spanned String. Se você teve resultados diferentes, não deixe de comentar aqui no artigo.
A LottieDrawable é pouco documentada e a maioria dos códigos utilizando ela, e disponíveis na comunidade Android, são códigos depreciados.
Quando realizei os testes a invocação LottieComposition.Factory.fromAssetFileName() era apresentada como depreciada, mas foi o único exemplo válido encontrado.
Logo, prefira o uso da LottieAnimationView, pois, em minha opinião, há fortes indícios de que a LottieDrawable não continuará como uma entidade pública.
Uma informação importante para aqueles que tentarão a LottieDrawable é que: ao final do processamento, quando a animação não é mais útil, o método recycleBitmaps() deve ser invocado, pode ser até mesmo no onDestroy() da atividade host da animação.
KeyPath para a animação de partes separadas
Um arquivo de animação gerado via After Effects junto ao Bodymovin, esse arquivo tem uma estrutura JSON hierárquica onde o todo se chama Composition e os objetos que integram o todo são denominados Layers.
Cada layer, camada, tem um nome e pode ser atualizado de maneira independente.
A seguir um LottieAnimationView que ainda não passou por nenhuma atualização independente de layer:
...
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_postcard"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/postcard" />
...
Executando o projeto com o simples LottieAnimationView anterior, temos:
Agora o código onde modificaremos as cores das montanhas da animação de fotografia:
...
/*
* Segundo alguns testes, as cores de itens individuais
* de uma animação somente são passíveis de serem atualizadas
* se utilizando uma instância de SimpleColorFilter ou
* de PorterDuffColorFilter.
* */
val colorRed = SimpleColorFilter( Color.RED )
val colorBlue = PorterDuffColorFilter( Color.BLUE, PorterDuff.Mode.SRC_ATOP )
/*
* A sintaxe correta para que a cor funcione, sintaxe de
* construtor: KeyPath( "key_name", "**" ). Segundo alguns
* testes, se não fizer assim não funciona, isso com a
* API versão 2.6.0-beta19.
* */
lav_postcard.addValueCallback(
KeyPath( "Mountain front", "**" ),
LottieProperty.COLOR_FILTER,
LottieValueCallback<ColorFilter>( colorRed )
)
lav_postcard.addValueCallback(
KeyPath( "Mountain back", "**" ),
LottieProperty.COLOR_FILTER,
LottieValueCallback<ColorFilter>( colorBlue )
)
...
Para aplicação de animação em layer, o método addValueCallback() deve ser utilizado, com os parâmetros:
- KeyPath: que permite identificar, pelo nome, os layers que serão atualizados. A chave "**" indica a correspondência para zero ou mais layers. Para a definição de cor não foi possível colocar mais de um layer por método addValueCallback() e sempre, como último argumento em KeyPath, é necessária a definição de "**";
- Property: que identifica a propriedade que será atualizada nos KeyPaths indicados em primeiro argumento. Aqui definimos que a propriedade de cor é que será atualizada, LottieProperty.COLOR_FILTER. Em Animatable Properties há todas as propriedades passíveis de atualização independente;
- Callback: instância que contém a configuração de atualização da propriedade definida em segundo argumento. Tem que ser uma LottieValueCallback.
Os termos "Mountain front" e "Mountain back" foram encontrados no arquivo JSON de animação. Os nomes dos layers de sua animação estão sempre nas chaves nm.
Executando o projeto Android com o código anterior de atualização de layer, temos:
Agora a atualização de posicionamento da montanha de trás, a montanha azul:
...
val mountainBackPosition = LottieRelativePointValueCallback(
PointF( 100.9F, 81.99F )
)
lav_postcard.addValueCallback(
KeyPath( "Mountain back" ),
LottieProperty.TRANSFORM_POSITION,
mountainBackPosition
)
...
Note que diferente da animação da propriedade de cor, aqui não precisamos da chave "**" para que a atualização ocorra.
Eu sei, isso é um pouco estranho, mas acredite, descobri isso na "martelada", pois a documentação sobre KeyPath na doc oficial do Lottie é bem simples, não diz muito em termos de código.
Executando o projeto com novo algoritmo, temos:
Mais opções de configuração
A seguir mais algumas opções de configuração que podem ser acessadas, algumas também podem ser atualizadas, além das já apresentadas até aqui.
Note que todas as configurações a seguir somente retornarão valores válidos se forem invocadas depois de a animação já estar renderizada em tela.
Segue:
...
Thread{
kotlin.run {
/*
* Forçando um delay para que as solicitações somente ocorram
* depois da renderização de animação em tela.
*/
SystemClock.sleep( 2000 )
val duration = lavAnim.duration
val repeatMode = lavAnim.repeatMode
val isAnimating = lavAnim.isAnimating
val hasMasks = lavAnim.hasMasks()
val hasMatte = lavAnim.hasMatte()
val imageAssetsFolder = lavAnim.imageAssetsFolder
val isMergePathsEnabledForKitKatAndAbove = lavAnim.isMergePathsEnabledForKitKatAndAbove
val scale = lavAnim.scale
val repeatCount = lavAnim.repeatCount
val useHardwareAcceleration = lavAnim.useHardwareAcceleration
val composition = lavAnim.composition
val performanceTracker = lavAnim.performanceTracker
...
}
}.start()
...
Muitas das configurações apresentadas acima são bem especificas para aqueles profissionais que têm um conhecimento mais apurado sobre as necessidades das animações, por exemplo: a propriedade useHardwareAcceleration que permite um developer com expertise em animação tomar decisões de processamento com base no valor retornado desta entidade.
Tamanho (altura x largura) da animação
As animações criadas com o After Effects e exportadas com o plugin Bodymovin são animações vetoriais, ou seja, não há necessidade de criar animações com exacerbadas largura e altura.
A documentação oficial da Lottie API recomenda que as animações, quando o maior tamanho possível é necessário, sejam criadas nas dimensões 411px x 731px, pois a Lottie API transformará esses valores em DPs.
De qualquer forma, caso você tenha problemas com a cobertura da animação em tela, lembre que LottieAnimationView herda de ImageView e assim é possível utilizar com sucesso o atributo android:scaleType com os valore centerInside ou centerCrop.
Pontos negativos
- A documentação não está atualizada ao menos em relação a versão mais atual da API na época da construção deste conteúdo, a versão 2.6.0-beta19;
- Não há um mínimo exemplo funcional com a LottieDrawable. Se essa API não tem tanta importância assim ela poderia ser removida;
- Os exemplos com KeyPath, animação independente de layer, também são escassos.
Pontos positivos
- As animações podem ser invocadas com poucas linhas de código, exigindo um mínimo apenas em código XML, facilitando a construção do projeto;
- Todo o trabalho de carregamento e gerência de animação em cache já é realizado pela própria API;
- A herança do ImageView facilita em muito o enquadramento da animação em tela, quando necessário;
- A possibilidade de carregamento de animação em servidor remoto é outro ponto de destaque. Como código extra somente temos de definir a permissão de Internet.
Considerações finais
Animações, sem sombra de dúvidas, trarão maior profissionalismo ao seus projetos Android. A Lottie API permitirá que você consiga isso sem muito esforço.
Somente tome cuidado com a quantidade de animações carregadas em tela, pois em testes realizados foi constatado que quanto mais animação em tela, maior a necessidade de processamento e menos responsivo fica o aplicativo, afetando diretamente a experiência do usuário.
Lembre que caso você não tenha um artista de animações After Effects ao seu lado e também não tenha expertise em criação de animações com este software, você tem o site LottieFiles que certamente, junto a configurações LottieAnimationView, lhe permitirá obter o máximo de animação em seu domínio de problema.
Tutorias para criação de animações no After Effects
Caso você como desenvolvedor Android queira arriscar no aprendizado de animações no Adobe After Effects, a seguir deixo alguns tutoriais que poderão lhe ajudar com isso:
- Tutorial BÁSICO de ANIMAÇÃO | After Effects;
- COMO FAZER 5 ANIMAÇÕES INCRÍVEIS no AFTER EFFECTS com SHAPES!;
- After Effects-Animação simples;
- Tutorial Swift iOS: Exportando animações do After Effects com Bodymovin e Lottie;
- Instalação e configuração do Lottie no After Effects.
Projeto Android
Para projeto de exemplo vamos construir algo muito similar a ideia de aplicativo de fuso horário mundial, ideia desenvolvida no artigo sobre a True Time API.
Aqui teremos duas partes de desenvolvimento:
- A primeira parte onde já teremos o aplicativo com a ClockImageView e a TrueTime API funcionando, mas sem animações Lottie;
- A segunda parte onde iremos mudar o visual da aplicação colocando duas animações. Uma de "informativo de horário em uso" e outra para indicar que é de noite, ou de dia, no fuso horário atualmente escolhido.
Antes de prosseguir, saiba que os arquivos estáticos e todo o projeto finalizado está no GitHub dele em: https://github.com/viniciusthiengo/relgio-mundial-animado-lottie-kotlin-android.
De qualquer forma, não deixe de acompanhar o projeto de exemplo, pois nele discutiremos particularidades da Lottie API ainda não apresentadas até este ponto.
Protótipo estático
A seguir as imagens do protótipo estático desta primeira parte do aplicativo:
Tela de entrada | Tela com fuso horário definido |
Seletor de fuso horário aberto | Informe de horário local em uso |
Com isso podemos partir para a criação do projeto.
Iniciando o projeto
Em seu Android Studio inicie um novo projeto Kotlin (pode ser Java se você preferir assim):
- Nome da aplicação: Relógio Mundial Animado;
- API mínima: 16 (Android Jelly Bean). Mais de 99% dos aparelhos Android em mercado sendo atendidos;
- Atividade inicial: Empty Activity;
- Nome da atividade inicial: ClockActivity. O nome do layout da atividade inicial será atualizado automaticamente;
- Para todos os outros campos, deixe-os com os valores já definidos por padrão.
Ao final desta parte do app teremos a seguinte arquitetura de projeto:
Configurações Gradle
Abaixo as configurações do Gradle Project Level, ou build.gradle (Project: RelgioMundialAnimado):
buildscript {
ext.kotlin_version = '1.2.60'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
/*
* Para acesso a biblioteca Animated-Clock-Icon
* e também a TrueTime
* */
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Então as configurações iniciais do Gradle App Level, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "thiengo.com.br.relgiomundialanimado"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
/* Biblioteca Animated-Clock-Icon */
implementation 'com.github.alxrm:animated-clock-icon:1.0.2'
/* Biblioteca TrueTime */
implementation 'com.github.instacart.truetime-android:library-extension-rx:09087b6a6e'
}
Na segunda parte do projeto adicionaremos a referência a Lottie API. Por agora, se para as APIs acima tiver versões mais atuais, então escolha referenciar as versões mais novas.
No caso da True Time API certifique-se de que a versão mais atual não tem problemas de trava na UI.
Configurações AndroidManifest
A seguir as configurações do AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.relgiomundialanimado">
<!-- Para que a True Time API funcione. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".TrueTimeApplication"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ClockActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Configurações de estilo
Para as configurações de tema e estilo, vamos iniciar com o arquivo de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">@color/colorSunSky</color>
<color name="colorPrimaryDark">@color/colorSunSky</color>
<color name="colorAccent">@color/colorSunSky</color>
<color name="colorInfo">@android:color/white</color>
<!-- Azul céu. -->
<color name="colorSunSky">#84D4F8</color>
</resources>
Então o arquivo de dimensões, /res/values/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="paddingBottom">20dp</dimen>
<dimen name="paddingSide">20dp</dimen>
<dimen name="paddingTop">60dp</dimen>
<dimen name="clock_width">200dp</dimen>
<dimen name="clock_height">200dp</dimen>
<dimen name="icon_width">22dp</dimen>
<dimen name="icon_height">22dp</dimen>
<dimen name="icon_margin">10dp</dimen>
</resources>
Agora o arquivo de dimensões para aparelhos com o Android 19, KitKat, ou superior, o arquivo /res/values-v19/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="paddingBottom">60dp</dimen>
<dimen name="paddingSide">60dp</dimen>
<dimen name="paddingTop">100dp</dimen>
</resources>
Assim o arquivo de Strings, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Relógio Mundial Animado</string>
<string name="info_icon_desc">
Ícone de informativo de horário em uso.
</string>
<string name="info_content">
Temporariamente o aplicativo está utilizando o horário do
aparelho. Assim que o retorno do servidor NTP acontecer
esse informe será removido de tela.
</string>
</resources>
Agora o arquivo que conterá os arrays estáticos do projeto. Segue /res/values/arrays.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="countries">
<item>Brasil (GMT-03:00)</item>
<item>Estados Unidos (GMT-05:00)</item>
<item>Japão (GMT+09:00)</item>
<item>Moçambique (GMT+02:00)</item>
<item>Zambia (GMT+02:00)</item>
</string-array>
<string-array name="countries_gmt">
<item>GMT-03:00</item>
<item>GMT-05:00</item>
<item>GMT+09:00</item>
<item>GMT+02:00</item>
<item>GMT+02:00</item>
</string-array>
</resources>
Por fim o arquivo de definição de tema de projeto, /res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Tema base do aplicativo herdando de Theme.AppCompat.Light.NoActionBar
para remover a barra de topo padrão, barra que não será necessária no
app de Relógio Mundial Animado.
-->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!--
Temas trabalhados no Spinner para que ele tenha toda a configuração
visual desenvolvida no protótipo estático.
-->
<style name="AppTheme.SpinnerTheme">
<item name="android:textViewStyle">
@style/AppTheme.TextViewStyle
</item>
</style>
<style
name="AppTheme.TextViewStyle"
parent="android:Widget.TextView">
<item name="android:textColor">@android:color/white</item>
<item name="android:fontFamily">@font/josefin_sans_regular</item>
</style>
</resources>
Os dois últimos estilos definidos no XML anterior seguem uma convenção no Android para quando criamos estilos extras que estendem o estilo padrão, que em nosso caso é o estilo AppTheme. O prefixo dos estilos extras é o nome do tema base do app.
Aqui também estamos trabalhando com uma fonte personalizada, mais precisamente a fonte josefin_sans_regular.ttf, presente em /res/font.
Classe de aplicação
Agora vamos ao código da TrueTimeApplication, classe responsável por conter o código de inicialização da True Time API:
class TrueTimeApplication: Application() {
override fun onCreate() {
super.onCreate()
/*
* Sempre iniciaremos a busca por alguma data TrueTime, pois
* como o tick do aparelho é que será utilizado, ainda temos
* a possibilidade de ter uma data não atualizada se não houver
* uma nova conexão com a Internet. Mas lembrando que uma nova
* invocação a servidor NTP não remove da cache a TrueTime já
* salva.
* */
TrueTimeRx
.build()
.withSharedPreferences( this )
.initializeRx( "time.apple.com" )
.subscribeOn( Schedulers.io() )
.subscribe(
{
/*
* Tudo certo com a obtenção de uma data
* TrueTime de servidor NTP, agora é preciso
* atualizar o horário em tela, logo uma
* "mensagem" broadcast é utilizada para
* comunicar a atividade ClockActivity sobre
* isso, a comunicação será por meio de
* BroadcastApplication e LocalBroadcastManager.
* */
val intent = Intent( BroadcastApplication.FILTER )
LocalBroadcastManager
.getInstance( this@TrueTimeApplication )
.sendBroadcast( intent )
},
{}
)
}
}
Lembrando que a TrueTimeApplication já está sendo referenciada no AndroidManifest:
...
<application
android:name=".TrueTimeApplication"
...>
...
Broadcast de comunicação: Application para Activity
A seguir o código da classe BroadcastApplication que é responsável por permitir a comunicação da TrueTimeApplication para com a ClockActivity. Segue código:
/*
* Classe responsável por permitir a comunicação da
* TrueTimeApplication para com a ClockActivity, isso
* utilizando o canal LocalBroadcastManager.
* */
class BroadcastApplication( val activity: ClockActivity ):
BroadcastReceiver() {
companion object {
const val FILTER = "ba_filter"
}
override fun onReceive( context: Context, intent: Intent ) {
/*
* Assim que a "mensagem" é enviada de
* TrueTimeApplication, invoque o método
* fireSpinnerItemSelected().
* */
activity.fireSpinnerItemSelected()
}
}
Se quiser saber mais sobre LocalBroadcastManager, entre em: Como Utilizar o LocalBroadcastManager Para Comunicação no Android.
AsyncTask ouvidor de novos horários
Assim podemos ir a classe ouvidora de novos horários, mesmo aquele horário disponibilizado pelo aparelho. Abaixo o código de AsyncTrueTime:
class AsyncTrueTime(): AsyncTask<String, Unit, Calendar>() {
/*
* Trabalhando com WeakReference (referência fraca) para
* garantir que não haverá vazamento de memória por
* causa de uma instância de AsyncTrueTime().
* */
lateinit var weakActivity: WeakReference<ClockActivity>
constructor( activity: ClockActivity ): this(){
weakActivity = WeakReference( activity )
}
override fun doInBackground( vararg args: String? ): Calendar {
lateinit var date : Date
/*
* O bloco try{}catch{} a seguir indica que: caso não
* haja uma data TrueTime disponível então utilize a
* data local do aparelho.
* */
try{
date = TrueTimeRx.now() /* Horário servidor NTP. */
weakActivity.get()?.infoDateShow( false ) /* Esconde info. */
}
catch (e : Exception){
date = Date() /* Horário do aparelho. */
weakActivity.get()?.infoDateShow( true ) /* Apresenta info. */
}
/*
* Colocando o Date em um Calendar, juntamente ao GMT,
* fuso horário escolhido. Isso, pois o trabalho com
* Calendar é mais simples e eficiente do que com Date.
* */
val calendar = Calendar.getInstance(
TimeZone.getTimeZone( args[0] )
)
calendar.timeInMillis = date.time
return calendar
}
override fun onPostExecute( calendar: Calendar ) {
super.onPostExecute( calendar )
weakActivity.get()?.updateClock( calendar )
}
}
Com isso podemos prosseguir ao trecho final dessa primeira parte de projeto.
ClockActivity, atividade principal
Para a atividade principal vamos primeiro ao layout dela, /res/layout/activity_clock.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rv_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:paddingBottom="@dimen/paddingBottom"
android:paddingLeft="@dimen/paddingSide"
android:paddingRight="@dimen/paddingSide"
android:paddingTop="@dimen/paddingTop"
tools:context=".ClockActivity">
<Spinner
android:id="@+id/sp_countries"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/spinner_border_and_background"
android:entries="@array/countries"
android:padding="12dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />
<rm.com.clocks.ClockImageView
android:id="@+id/civ_clock"
android:layout_width="@dimen/clock_width"
android:layout_height="@dimen/clock_height"
android:layout_centerInParent="true"
app:clockColor="@android:color/white"
app:frameWidth="light"
app:indeterminateSpeed="2"
app:pointerWidth="light"
app:timeSetDuration="800" />
<TextView
android:id="@+id/tv_clock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/civ_clock"
android:layout_marginTop="2dp"
android:fontFamily="@font/josefin_sans_regular"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="41sp" />
<LinearLayout
android:id="@+id/ll_info_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:layout_width="@dimen/icon_width"
android:layout_height="@dimen/icon_height"
android:layout_marginEnd="@dimen/icon_margin"
android:layout_marginRight="@dimen/icon_margin"
android:contentDescription="@string/info_icon_desc"
android:src="@drawable/ic_warning"
android:tint="@color/colorInfo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/info_content"
android:textColor="@color/colorInfo" />
</LinearLayout>
</RelativeLayout>
Para o Spinner estamos utilizando um background personalizado. Em /res/drawable crie o arquivo spinner_border_and_background.xml como a seguir:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--
No <item> a seguir temos a definição de: cor de background;
curvatura de borda; e cor e largura de borda do componente
retangular que está sendo trabalhado.
-->
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="2dp" />
<stroke
android:width="2dp"
android:color="@android:color/white" />
</shape>
</item>
<!--
No <item> a seguir temos a definição de imagem de background,
no caso a imagem ic_keyboard_arrow_down_white_18dp. A imagem
estará centralizada na vertical e a 0.5dp da borda direita
(center_vertical|right) do componente container (em nosso
domínio do problema Spinner é o container). A cor da imagem
também está sendo definida via tint.
-->
<item android:right="8dp">
<bitmap
android:tint="@android:color/white"
android:gravity="center_vertical|right"
android:src="@drawable/ic_keyboard_arrow_down_white_18dp" />
</item>
</layer-list>
Com o XML anterior e o estilo para Spinners, este último definido no arquivo de tema do aplicativo, conseguiremos a seguinte formatação:
Abaixo o diagrama do layout activity_clock.xml:
Com isso podemos ir ao código Kotlin da ClockActivity:
class ClockActivity :
AppCompatActivity(),
AdapterView.OnItemSelectedListener {
lateinit var countriesGmt : Array<String>
lateinit var broadcast: BroadcastApplication
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_clock )
/*
* Colocando a barra de status (statusBar) e barra de
* navegação (bottomBar) em transparente.
* */
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ){
window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)
}
initBroadcastReceiver()
/* Iniciando o array de GMTs. */
countriesGmt = resources.getStringArray( R.array.countries_gmt )
/*
* Vinculando o "listener de item selecionado" ao Spinner
* de fusos horários.
* */
sp_countries.onItemSelectedListener = this
}
/*
* Método responsável por registrar um BroadcastReceiver
* (BroadcastApplication) para poder receber uma comunicação
* de TrueTimeApplication, comunicação sobre o retorno de
* uma data / horário corretos de algum servidor NTP.
* */
private fun initBroadcastReceiver(){
broadcast = BroadcastApplication( this )
val filter = IntentFilter( BroadcastApplication.FILTER )
LocalBroadcastManager
.getInstance(this)
.registerReceiver( broadcast, filter )
}
override fun onDestroy() {
super.onDestroy()
/* Liberação do BroadcastReceiver. */
LocalBroadcastManager
.getInstance(this)
.unregisterReceiver( broadcast )
}
/*
* Listener de novo item selecionado em Spinner. Note que
* este método é sempre invocado quando a atividade é
* construída, pois o item inicial em Spinner é considerado
* um "novo item selecionado", dessa forma desde o início
* a TrueTime API será solicitada sem que nós
* desenvolvedores tenhamos de criar algum código somente
* para essa invocação inicial.
* */
override fun onItemSelected(
adapter: AdapterView<*>?,
itemView: View?,
position: Int,
id: Long ) {
/*
* O array countriesGmt facilita o acesso ao formato GMT
* String esperado em TimeZone.getTimeZone(), assim não
* há necessidade de blocos condicionais ou expressões
* regulares para ter acesso ao GMT correto de acordo
* com o item escolhido.
* */
AsyncTrueTime(this)
.execute( countriesGmt[ position ] )
}
override fun onNothingSelected( adapter: AdapterView<*>? ) {}
/*
* Método responsável por apresentar / esconder a View de informação
* sobre a origem do horário (GMT) sendo utilizado: servidor NTP
* (certeza que o horário estará correto); ou aparelho. Este método
* será invocado sempre no doInBackground() de uma instância de
* AsyncTrueTime, por isso a necessidade do runOnUiThread() para que
* a atualização de View não seja fora da Thread UI.
* */
fun infoDateShow( status: Boolean ){
runOnUiThread {
ll_info_date.visibility =
if(status) /* Origem: aparelho. */
View.VISIBLE
else /* Origem: servidor NTP. */
View.INVISIBLE
}
}
/*
* Método responsável por atualizar tanto o ClockImageView
* quanto o TextView de horário de acordo com o parâmetro
* Calendar fornecido. Este método será invocado sempre no
* onPostExecute() de uma instância de AsyncTrueTime.
* */
fun updateClock( trueTime: Calendar){
val hour = trueTime.get( Calendar.HOUR_OF_DAY )
val minute = trueTime.get( Calendar.MINUTE )
/*
* Atualizando o ClockImageView com aplicação de animação.
* */
civ_clock.animateToTime( hour, minute )
/*
* O formato "%02d:%02d" garante que em hora e em minuto não
* haverá números menores do que 10 não acompanhados de um 0
* a esquerda.
* */
tv_clock.text = String.format( "%02d:%02d", hour, minute )
}
/*
* Método que invocará onItemSelected() para atualizar o
* horário, isso, pois fireSpinnerItemSelected() somente
* será acionado assim que a API TrueTime tiver retorno de
* algum servidor NTP. fireSpinnerItemSelected() garante
* que o horário em apresentação é o correto.
* */
fun fireSpinnerItemSelected(){
sp_countries
.onItemSelectedListener
.onItemSelected( null, null, sp_countries.selectedItemPosition, 0 )
}
}
Executando o projeto atual, temos:
Colocando animação no projeto
Apesar das animações já presentes, mais precisamente a animação da ClockImageView, é possível melhorar ainda mais a experiência do usuário.
A seguir os trechos do projeto que receberão animação:
- O ícone estático de informe de "horário do aparelho em uso" dará lugar a um ícone animado;
- Assim que houver a mudança de fuso horário, se for uma hora entre 6 da manhã e 17:59 da tarde, então uma animação de Sol deverá ocorrer. Caso contrário uma animação de Lua.
A seguir o fluxograma de acionamento da animação de "sol / lua" no app:
Novo protótipo estático
A seguir as imagens do novo protótipo estático do aplicativo:
Dia. Tela de relógio | Dia. Seletor de fuso horário aberto |
Dia. Informe de horário de aparelho | Noite. Tela de relógio |
Noite. Seletor de fuso horário aberto | Noite. Informe de horário de aparelho |
Com isso podemos partir para a atualização do projeto.
Atualizando o Gradle App Level
O primeiro passo é adicionar a API Lottie no Gradle App Level, ou build.gradle (Module: app):
...
dependencies {
...
/* Biblioteca Lottie */
implementation "com.airbnb.android:lottie:2.6.0-beta19"
}
Sincronize o projeto.
Baixando as animações que serão utilizadas
Aqui não precisaremos de expertise no After Effects, vamos baixar as duas animações necessárias diretamente do LottieFiles, são elas:
Quando nas páginas das animações acima, clique no ícone de download para baixar os arquivos JSON.
Ao final, coloque:
- O arquivo warning.json em /res/raw;
- O arquivo sun_moon.json (renomeado) no folder /assets.
Atualizando o layout XML de ClockActivity
Agora a nova versão do layout activity_clock.xml, versão já contendo os dois LottieAnimationView:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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="@color/colorPrimary"
android:fitsSystemWindows="true"
tools:context=".ClockActivity">
<!--
View Lottie de animação, cobre toda a extensão da tela
disponível ao aplicativo. Devido a isso, e também para não
reconfigurarmos os paddings já definidos no RelativeLayout
do design, foi escolhido utilizar um FrameLayout como
layout container.
-->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lav_sun_moon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@string/anim_sun_and_moon"
app:lottie_autoPlay="false"
app:lottie_fileName="sun_moon.json"
app:lottie_loop="false" />
<RelativeLayout
android:id="@+id/rv_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/paddingBottom"
android:paddingLeft="@dimen/paddingSide"
android:paddingRight="@dimen/paddingSide"
android:paddingTop="@dimen/paddingTop">
<Spinner
android:id="@+id/sp_countries"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/spinner_border_and_background"
android:entries="@array/countries"
android:padding="12dp"
android:popupBackground="@color/colorPrimary"
android:textColor="@android:color/white"
android:theme="@style/AppTheme.SpinnerTheme" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/ll_info_date"
android:layout_centerHorizontal="true"
android:layout_marginBottom="14dp"
android:gravity="center"
android:orientation="horizontal">
<rm.com.clocks.ClockImageView
android:id="@+id/civ_clock"
android:layout_width="@dimen/clock_width"
android:layout_height="@dimen/clock_height"
android:layout_marginEnd="14dp"
android:layout_marginRight="14dp"
app:clockColor="@android:color/white"
app:frameWidth="light"
app:indeterminateSpeed="2"
app:pointerWidth="light"
app:timeSetDuration="800" />
<TextView
android:id="@+id/tv_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/josefin_sans_regular"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="41sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_info_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal"
android:visibility="invisible">
<com.airbnb.lottie.LottieAnimationView
android:layout_width="@dimen/icon_width"
android:layout_height="@dimen/icon_height"
android:layout_marginEnd="@dimen/icon_margin"
android:layout_marginRight="@dimen/icon_margin"
android:contentDescription="@string/info_icon_desc"
app:lottie_autoPlay="true"
app:lottie_colorFilter="@color/colorInfo"
app:lottie_loop="false"
app:lottie_rawRes="@raw/warning" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/info_content"
android:textColor="@color/colorInfo" />
</LinearLayout>
</RelativeLayout>
</FrameLayout>
Colocamos um novo layout container, FrameLayout, para não ser necessária toda a reconfiguração de padding já existente no RelativeLayout.
Colocamos também um novo LinearLayout para conter o ClockImageView e o TextView de rótulo de horário.
Note que no LottieAnimationView de ícone animado de informação estamos utilizando o atributo lottie_colorFilter para deixar a animação com a exata mesma cor do ícone estático anterior:
...
<com.airbnb.lottie.LottieAnimationView
android:layout_width="@dimen/icon_width"
android:layout_height="@dimen/icon_height"
android:layout_marginEnd="@dimen/icon_margin"
android:layout_marginRight="@dimen/icon_margin"
android:contentDescription="@string/info_icon_desc"
app:lottie_autoPlay="true"
app:lottie_colorFilter="@color/colorInfo"
app:lottie_loop="false"
app:lottie_rawRes="@raw/warning" />
...
Para seguir as configurações de UI do novo protótipo estático, ainda é preciso atualizar as dimensões icon_width, icon_width, clock_width e clock_height no arquivo /res/values/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<dimen name="clock_width">50dp</dimen>
<dimen name="clock_height">50dp</dimen>
<dimen name="icon_width">60dp</dimen>
<dimen name="icon_height">60dp</dimen>
<dimen name="icon_margin">10dp</dimen>
</resources>
Ainda temos uma atualização no arquivo de Strings. Em /res/values/strings.xml adicione o trecho em destaque:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="anim_sun_and_moon">
Animação de Sol e Lua
</string>
</resources>
A String anim_sun_and_moon está sendo referenciada como contentDescription do primeiro LottieAnimationView do layout.
A seguir o novo diagrama do layout activity_clock.xml:
Classe container de animação Lottie
Para o trabalho com gerência de animação, digo, gerência da animação de "Sol / Lua", vamos criar uma classe especifica para isso.
Assim não colocaremos mais uma responsabilidade na atividade principal e conseguiremos o mínimo possível de código limpo em projeto.
Vamos colocar essa nova classe em um novo pacote, logo, na raiz do projeto, crie o pacote /domain e em seguida, dentro do novo pacote, crie a classe LottieContainer como a seguir:
/*
* Classe criada para ser responsável por manter toda a
* gerência da animação principal de ClockActivity.
* */
class LottieContainer(
val context: Context,
val animation: LottieAnimationView ) : Animator.AnimatorListener {
/*
* Constantes que contém valores que possivelmente seriam
* alterados com frequência em projeto de produção. Essas
* constantes evitam que trabalhemos com valores mágicos,
* algo negativo na arquitetura limpa de projeto.
* */
companion object {
const val SPEED_SUN_TO_MOON = 1.5F
const val SPEED_MOON_TO_SUN = -1.5F
const val FRAME_FIRST = 46
const val FRAME_LAST = 82
}
var hour: Int = 0
/*
* A importância do bloco de inicialização aqui é que a
* animação sempre estará com os dados iniciais corretos e
* componentes que têm mudança brusca de cor já estarão
* com as cores corretas.
* */
init{
animation.setMinAndMaxFrame( FRAME_FIRST, FRAME_LAST )
animation.addAnimatorListener( this )
startHourAndColor()
}
/*
* Método que deve ser invocado no bloco inicial da classe
* (init{}) afim de obter a atual hora presente no device.
* */
private fun startHourAndColor(){
val calendar = Calendar.getInstance()
hour = calendar.get( Calendar.HOUR_OF_DAY )
}
/*
* Não há problemas em manter aqui os valores mágicos 6 e 18,
* pois essa é a definição de "dia (sol)" para o nosso domínio
* de problema.
* */
private fun isMorning( hour: Int ): Boolean =
hour >= 6 && hour < 18
/*
* Método responsável por verificar se a hora atualmente
* definida, de acordo com a seleção em Spinner, exige que a
* animação aconteça, ou seja, se for uma hora que indica "dia"
* e a animação atualmente esteja em "noite", então a animação
* de "noite" para "dia" deve ocorrer e vice-versa.
* */
private fun canAnimate( hour: Int ): Boolean =
( isMorning( hour ) && animation.frame == FRAME_LAST )
|| ( !isMorning( hour ) && animation.frame == FRAME_FIRST )
/*
* Método responsável pela verificação e então a ativação de
* animação, se necessária. Deve ser invocado sempre que uma
* nova hora for fornecida pelo sistema de horário construído
* para o aplicativo.
* */
fun updateByHour( hour : Int ){
/*
* Padrão Cláusula de Guarda para evitar o processamento
* do método caso a condição mínima não seja satisfeita.
*
* Mais sobre este padrão, no link a seguir:
*
* https://www.thiengo.com.br/padrao-de-projeto-clausula-de-guarda
* */
if( !canAnimate( hour ) ){
return
}
/*
* Quando a propriedade speed de LottieAnimationView tem
* um valor negativo a animação é realizada de maneira
* invertida. Ressaltando que speed é a velocidade da
* animação e não a definição da duração dela em
* segundos.
* */
var speedValue = SPEED_SUN_TO_MOON /* Speed positivo. */
if( isMorning( hour ) ){
speedValue = SPEED_MOON_TO_SUN /* Speed negativo. */
}
this.hour = hour
animation.speed = speedValue
animation.playAnimation()
}
override fun onAnimationStart( animator: Animator? ) {}
override fun onAnimationEnd( animator: Animator? ) {
/*
* A cada final de animação o frame atual é atualizado.
* Caso essa lógica de negócio não fosse empregada, mesmo
* o modo visual da animação apontando para o primeiro
* frame a propriedade "frame", ao final da animação,
* passa a apontar sempre para o último frame da animação.
* */
animation.frame =
if( isMorning( hour ) )
FRAME_FIRST
else
FRAME_LAST
}
override fun onAnimationRepeat( animator: Animator? ) {}
override fun onAnimationCancel( animator: Animator? ) {}
}
Leia todos os comentários do código anterior, pois assim será fácil o entendimento dessa nova classe container.
Já lhe adianto que o entendimento completo do método startHourAndColor() virá em seções posteriores, logo, inicialmente, apenas assuma que a importância deste método é: inicializar a propriedade hour.
Vamos dar um destaque para o método onAnimationEnd():
...
override fun onAnimationEnd( animator: Animator? ) {
/*
* A cada final da animação o frame atual é atualizado.
* Caso essa lógica de negócio não fosse empregada, mesmo
* o modo visual da animação apontando para o primeiro
* frame a propriedade "frame", ao final da animação,
* passa a apontar sempre para o último frame da animação.
* */
animation.frame =
if( isMorning( hour ) )
FRAME_FIRST
else
FRAME_LAST
}
...
Logo no bloco de inicialização da classe, init{}, nós definimos os frames inicial e final para a animação de "Sol / Lua". Isso utilizando o método setMinAndMaxFrame(). Essa configuração foi necessária, pois caso contrário a animação demoraria muito para uma simples mudança de status, como é na versão sem cortes de frames.
Quando o algoritmo estiver em teste em seu IDE, sem o uso do código em onAnimationEnd(), você notará que mesmo quando a animação inversa, speed com valor negativo, é invocada o valor de frame continua sendo o de "último frame de animação" (82), mesmo o visual da animação apontando para o minFrame (46) definido em setMinAndMaxFrame().
Isso provavelmente é um bug da API.
Resumo que temos com o bug encontrado: mesmo que você tenha APIs para quase todas as funcionalidades do aplicativo, em algum momento você terá de criar na unha a lógica de negócio que ajusta aquele 1% não encontrado em APIs e fóruns.
Iniciando a LottieContainer
Agora na ClockActivity coloque os códigos de inicialização e gerência da LottieContainer. Segue em destaque:
class ClockActivity :
AppCompatActivity(),
AdapterView.OnItemSelectedListener {
...
lateinit var lottieContainer: LottieContainer
override fun onCreate( savedInstanceState: Bundle? ) {
...
/*
* Iniciando o objeto container das configurações e métodos
* de ação sobre a LottieAnimationView principal de projeto.
* */
lottieContainer = LottieContainer( this, lav_sun_moon )
}
...
fun updateClock( trueTime: Calendar ){
...
lottieContainer.updateByHour( hour )
}
...
}
Ainda falta algo!
Atualizando o background das opções de Spinner
A cor de background da lista de opções de fusos horários não está acompanhando a cor da tela:
No arquivo de cores do projeto, /res/values/colors.xml, adicione a seguinte nova cor:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="colorMoonSky">#617F8C</color>
</resources>
Na ClockActivity adicione o método setViewsColorByTime():
...
/*
* Permite a mudança de cor dos componentes visuais da
* ClockActivity que precisam acompanhar o design de acordo
* com a animação em tela.
* */
fun setViewsColorByTime( isMorning: Boolean ){
/*
* Definição de cor de background da lista de opções do
* Spinner de GMTs.
* */
if( isMorning ){
sp_countries.setPopupBackgroundResource( R.color.colorSunSky )
}
else{
sp_countries.setPopupBackgroundResource( R.color.colorMoonSky )
}
}
...
Agora na classe LottieContainer adicione os códigos em destaque:
class LottieContainer(
val context: Context,
val animation: LottieAnimationView ) : Animator.AnimatorListener {
...
private fun startHourAndColor(){
...
updateActivityViewsColor()
}
private fun updateActivityViewsColor(){
(context as ClockActivity)
.setViewsColorByTime( isMorning( hour ) )
}
...
fun updateByHour( hour : Int ){
...
updateActivityViewsColor()
}
...
}
Com isso podemos partir para os testes e resultados.
Testes e resultados
Acesse o menu de topo do Android Studio. Acione Build e em seguida Rebuild project. Assim execute o aplicativo em seu emulador ou aparelho de testes.
Iniciando o app Android e realizando algumas mudanças em Spinner, temos:
Lembrando que a animação de informe de "horário de aparelho" somente fica em tela até o momento que nenhum data NTP é retornada.
Com isso terminamos o estudo da Lottie API. Essa é uma das APIs que eu lhe aconselho a já buscar algum lugar em seu aplicativo Android para se beneficiar dela.
Com isso, não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.
Se inscreva também no canal do Blog em: YouTube Thiengo.
Slides
Abaixo os slides com a apresentação completa da Lottie API:
Vídeos
A seguir os vídeos com a construção passo a passo do algoritmo de animação do projeto de fuso horário mundial animado:
Para acessar o projeto de exemplo, entre no GitHub a seguir: https://github.com/viniciusthiengo/relgio-mundial-animado-lottie-kotlin-android.
Conclusão
Imagens são importantes para ajudar no conteúdo, mas animações conseguem ir além, ainda mais quando são interativas com as ações do usuário.
A Lottie API, mesmo com os problemas encontrados, é sem sombra de dúvidas a melhor opção para você aumentar o glamour de seu aplicativo Android colocando animações Adobe After Effects exportadas via Bodymovin plugin.
Não deixe de acompanhar a API, pois uma das promessas do time de engenheiros do AirBnb é o maior suporte as características do After Effects.
Assim finalizamos o artigo. Caso você tenha alguma dica ou dúvida sobre animações no Android, deixe logo abaixo nos comentários.
E se curtiu o conteúdo, não esqueça de compartilha-lo. E por fim, não deixe de se inscrever na 📩 lista de emails.
Abraço.
Fontes
A nova era de animações mobile com Lottie!
Comentários Facebook