Animando as transições de Views em um aplicativo Android

Luciano Medeiros
Android Dev BR
Published in
8 min readJul 30, 2018

Ultimamente passei por um problema aparentemente simples, mas que para uma pessoa sem experiência no desenvolvimento para Android, ou até mesmo alguém com mais bagagem, é uma questão não tão rápida de se resolver. Pensando nisso resolvi compartilhar aqui o problema que eu tive, quais foram as soluções que tentei e como resolvi o desejado.

Em toda aplicação Android, uma UI (User Interface) e uma UX (User Experience) bem construída é algo fundamental para que o aplicativo seja aceito pelos usuários e talvez tenha sucesso. Um detalhe que ajuda a se chegar nesse objetivo é a utilização de animações, que mesmo sendo simples passam a impressão de algo bem construído, organizado e amigável. Esse pensamento, inclusive, é bem difundido nas diretrizes elicitadas pelo Material Design:

Why does motion matter?
Motion shows how an app is organized and what it can do.
Motion provides:
Guided focus between views
Hints at what will happen if a user completes a gesture
Hierarchical and spatial relationships between elements
Distraction from what’s happening behind the scenes (like fetching content or loading the next view)
— — Tradução — —
Por que o movimento é importante?
Movimento mostra como um aplicativo é organizado e o que ele pode fazer.
Movimento fornece:
Foco orientado entre views
Dicas sobre o que acontecerá se um usuário concluir um gesto
Relações hierárquicas e espaciais entre elementos
Distração do que está acontecendo nos bastidores (como buscar conteúdo ou carregar a próxima view)

Um exemplo de animação que segue essas ideias do Material Design é quando o aplicativo exibe poucas informações em uma determinada área (um Card, por exemplo) e quando o usuário toca nessa área, a view tocada cresce para exibir mais informações. Imagine uma tela que possua dois Cards, um logo abaixo do outro, onde dentro de cada Card possui um título e outros campos de entrada (um form). Seguindo o conceito de adicionar movimento, animação, aos elementos do aplicativo, o ideal seria que quando o usuário tocasse em um desses Cards (neste exemplo implementados com o widget CardView), os campos do formulário fossem exibidos ou ocultados com a animação correta, animando tanto a aparição deles quanto dos Cards em que eles estão. Um jeito de projetar esse layout da tela é conforme o exemplo abaixo (algumas tags omitidas para ficar melhor de ler):

<LinearLayout ...>
<CardView ...>
<ConstraintLayout ...>
<TextView android:id="@+id/titulo" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
<CardView ...>
<ConstraintLayout ...>
<TextView android:id="@+id/titulo2" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
</LinearLayout>

Para alcançar o objetivo citado anteriormente, a ideia é que os TextViews de título respondam ao toque do usuário, fazendo com que a visibilidade das demais views do ConstraintLayout fique alternando entre View.VISIBLE e View.GONE. Dessa forma, a altura do ConstraintLayout é alterada para exibir ou ocultar as demais views, assim como a altura do CardView e do LinearLayout. Se a implementação do layout for realizada conforme descrita até aqui, com os eventos de clique, porém sem se preocupar com nenhuma animação, o resultado será o visto abaixo.

Layout sem animações

Como é possível visualizar, não é tão agradável a transição das views do estado de invisível para visível. Nem existe uma transição, um movimento que demonstra para o usuário o que aconteceu com a tela. Do ponto de vista do Material Design qual seria a transição ideal? O ideal seria que o conteúdo dos Cards fossem exibidos de maneira suave (por exemplo, interpolando o alpha dos seus componentes, onde o alpha é uma propriedade que cada view possui e define a transparência do seu desenho), ao mesmo tempo que a altura do Card aumentasse também suavemente, e, caso seja necessário, que o outro Card mude a sua posição Y (altura relativa ao topo da tela, ou seja, alterando o valor de Y ele sobe ou desce na tela) também com um movimento suave.

Fazendo uma pesquisa na documentação é possível encontrar uma seção falando apenas de animações (Developer Android). Desde animações simples, como deslocar um objeto, alterar o alpha de uma view, até animações mais complexas, como animar bitmaps e mudanças de activities. Se você se aprofundar nos tipos de animações vai perceber que a animação ideal, para o exemplo deste post, é possível implementando um LayoutTransition. A maneira mais simples de implementar isso, segundo o link, é adicionando a tag animateLayoutChanges com o valor true no ViewGroup que se deseja animar. Dessa forma, as mudanças que ocorrerem nas views filhas do ViewGroup serão animadas. Como o objetivo é que o CardView anime, basta adicionar a tag no seu ViewGroup, que no caso é o LinearLayout:

<LinearLayout android:animateLayoutChanges="true"...>
<CardView ...>
<ConstraintLayout ...>
<TextView android:id="@+id/titulo" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
<CardView ...>
<ConstraintLayout ...>
<TextView android:id="@+id/titulo2" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
</LinearLayout>

Eis o resultado dessa mudança:

Hum… nada aconteceu :(

Se você reler a página citada acima, do LayoutTransition, ela fala que deveria ser animada a adição/remoção de uma view, mas não aconteceu. Isso ocorre porque no LinearLayout (ViewGroup que encapsula os dois cards retângulos brancos) em que a tag foi adicionada não houve adição/remoção de view, o Card (retângulo braco) apenas teve sua altura modificada. As views que tiveram sua visibilidade alterada fazem parte do ConstraintLayout, que não possuía a tag animateLayoutChanges. Ou seja, essa animação padrão do LayoutTransition não é repassada para as views aninhadas, ela é aplicada diretamente as views filhas do ViewGroup escolhido. Se uma das views filhas for um ViewGroup, a adição de uma nova view nesse ViewGroup não será animada.

Parando para se pensar no passo a passo da animação desejada, pode-se listar os seguintes objetivos:

  1. Animar o aumento da altura do Card que foi tocado pelo usuário
  2. Caso seja necessário, animar o deslocamento do outro Card em decorrência do aumento do primeiro
  3. Animar a exibição das Views que estão ocultas dentro do ConstraintLayout do Card que foi tocado

Para chegar a esse resultado é preciso ter um LayoutTransition no ConstraintLayout (com intuito de conseguir o objetivo 3) e um no LinearLayout (para conseguir os objetivos 1 e 2). Ficando desta forma:

<LinearLayout android:animateLayoutChanges="true"...>
<CardView ...>
<ConstraintLayout android:animateLayoutChanges="true"...>
<TextView android:id="@+id/titulo" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
<CardView ...>
<ConstraintLayout android:animateLayoutChanges="true"...>
<TextView android:id="@+id/titulo2" .../>
<!-- Outros textviews, edittexts e button -->
</ConstraintLayout>
</CardView>
</LinearLayout>

Dando o build e executando, eis o resultado:

Objetivos 1 e 3 ok, mas o objetivo 2 não foi atingido

O objetivo 3 foi conseguido, com a exibição das Views dentro do Card sendo animada. O objetivo 1 também foi atingido, com a mudança de altura dos Cards. Porém, o objetivo 2 não foi totalmente completado. Ao visualizar o gif com atenção, é possível notar que o Card de Situação Eleitoral expande e contrai realizando a animação correta, mas quando o Card de Local de Votação é alterado, o outro Card não segue o seu movimento. Quando ele é expandido, o segundo Card some da sua posição e aparece embaixo, na sua nova posição. E quando ele é contraído, o segundo Card é exibido na nova posição antes da animação de contração terminar.

Investigando o código-fonte do ViewGroup, para entender o que está acontecendo, tem-se o seguinte trecho de código:

case R.styleable.ViewGroup_animateLayoutChanges:
boolean animateLayoutChanges = a.getBoolean(attr, false);
if (animateLayoutChanges) {
setLayoutTransition(new LayoutTransition());
}
break;

Ou seja, uma versão padrão do LayoutTransition é o responsável por fazer as animações de transição. Se você analisar o código da classe LayoutTransition, para entender o que ela faz quando seu construtor padrão é usado, é possível ler o seguinte trecho:

/**
* These are the types of transition animations that the LayoutTransition is reacting
* to. By default, appearing/disappearing and the change animations related to them are
* enabled (not CHANGING).
*/
private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | FLAG_APPEARING | FLAG_DISAPPEARING;

Então, quando apenas a tag animateLayoutChanges é utilizada, o ViewGroup adiciona um LayoutTransition que anima apenas os eventos de aparecer e desaparecer uma view, ignorando os eventos quando uma view sofre outra mudança (deslocamento, por exemplo). Para que o objetivo 2 seja realizado, o segundo Card deve se deslocar quando o primeiro expandir, portanto é preciso adicionar, manualmente, um LayoutTransition que responda também aos eventos de mudança. Aproveitando as extensions functions de Kotlin, pode-se criar uma função genérica da seguinte forma:

fun ViewGroup.setupAllAnimations() {
val transition = LayoutTransition()

// enableTransitionType está disponível a partir do Jelly Bean
if (Build.VERSION.SDK_INT >= 16) {
transition.enableTransitionType(LayoutTransition.CHANGING)
}

this.layoutTransition = transition
}

E utilizá-la no LinearLayout para que ele anime os deslocamentos dos Cards: linearLayout.setupAllAnimations(). Uma vez que será adicionado o LayoutTransition manualmente, a tag animateLayoutChanges pode ser removida do LinearLayout. Resultado:

Objetivos alcançados, mas uma das animações ficou com problema

A mudança realizada surtiu efeito, fazendo com que o deslocamento dos Cards fossem animados, mas um problema surgiu: quando um deles é contraído, a animação de diminuição da altura é realizada e logo após o Card sofre uma alteração, como se sua altura aumentasse e diminuísse rapidamente. No momento da expansão esse problema não ocorre.

Esse imprevisto é mais complicado. Mas se você adicionar um listener ao LayoutTransition e analisar o que ele anima e em qual ordem, dá para notar que quando se define um LayoutTransition para um ViewGroup, por padrão, ele anima algumas mudanças também do seu ViewGroup pai. Exemplo: quando o conteúdo do ConstraintLayout é exibido, a animação de exibição é executada, a animação do ConstraintLayout crescendo é executada, mas uma animação também ocorre para o CardView (ViewGroup pai do ConstraintLayout). Da mesma forma, ao configurar o LinearLayout, quando o CardView é deslocado, a animação do CardView é executada, a do próprio LinearLayout e a do seu pai, o NestedScrollView (ocultado dos trechos de código do layout para simplificar). Se você notar, ocorrem duas animações envolvendo o CardView, uma causada pelo LayoutTransition do ConstraintLayout e uma iniciada pelo LayoutTransition do LinearLayout. Isso é o que causa o comportamento indesejado, é preciso arrumar uma maneira de apenas uma animação ser executada no CardView.

Analisando mais uma vez o código da classe LayoutTransition, tem-se o seguinte método:

/**
* This flag controls whether CHANGE_APPEARING or CHANGE_DISAPPEARING animations will
* cause the default changing animation to be run on the parent hierarchy as well...
*
(Resumo: Essa flag controla se as animações de aparecer e
* ou desaparecer também serão executadas na view pai)
*
(Comentário resumido para exibir apenas a frase inicial)
*/
public void setAnimateParentHierarchy(boolean animateParentHierarchy) {
mAnimateParentHierarchy = animateParentHierarchy;
}

Portanto, é possível configurar o LayoutTransition para não animar a view pai quando os eventos de CHANGE_APPEARING e CHANGE_DISAPPEARING acontecerem. Esse é exatamente o caso do ConstraintLayout do layout de exemplo, ele está sofrendo uma animação por conta de views que estão aparecendo/desaparecendo e está repassando essa animação para o Card (view pai), causando a animação duplicada. Se você alterar o LayoutTransition, associado ao ConstraintLayout, para que ele não repasse essa animação para o seu pai, apenas uma animação será executada em cima do Card, decorrente do LayoutTransition do LinearLayout. Customizando a extension function anterior, tem-se:

fun ViewGroup.setupAllAnimations(animateParent: Boolean) {
val transition = LayoutTransition()

// Disponível a partir do Ice Cream Sandwich
transition.setAnimateParentHierarchy(animateParent)
// enableTransitionType está disponível a partir do Jelly Bean
if (Build.VERSION.SDK_INT >= 16) {
transition.enableTransitionType(LayoutTransition.CHANGING)
}

this.layoutTransition = transition
}

E agora é possível utilizar um LayoutTransition animando tudo no LinearLayout: linearLayout.setupAllAnimations(true) e um que não anima o pai para os dois ConstraintLayout: constraintLayout.setupAllAnimations(false) . Como esses códigos já adicionam os LayoutTransitions necessários, as tags animateLayoutChanges não são mais necessárias em nenhum XML de layout, deixando o layout assim como foi definido inicialmente, no primeiro trecho.

Resultado Final

Resultado final das animações
  • O Card aumentando e diminuindo está sendo animado
  • O deslocamento dos Cards está sendo animado
  • As views que estão sendo adicionadas e removidas estão sofrendo animações

\o/

No fim, conseguiu-se as animações desejadas, com uma interface mais suave e responsiva as ações do usuário, além de entender um pouco o funcionamento do LayoutTransition.

Espero que isso tenha sido útil para você, encurtando o caminho para se chegar a solução desse tipo de animação. Qualquer dúvida estou aberto aos comentários.

--

--

Luciano Medeiros
Android Dev BR

Analista de Sistemas do TJPB | Desenvolvedor Android