Animações no Flutter: compreendendo o que são e como implementá-las
Introdução
Usar animações em sua aplicação pode ser um diferencial na experiência do usuário, tornando as transições entre telas e elementos mais atraentes e intuitivas, além de geralmente tornar sua aplicação mais bonita. No Flutter existem diversas formas de criar essas animações. Nesse artigo vamos entender o que são animações no Flutter, os tipos de animações e como podemos escolher a melhor opção para cada situação.
Animações no Flutter
No Flutter existe suporte para diversos tipos de animações, facilitando a implementação delas em seu projeto, em geral existem algumas categorias de animações, como as:
- Animações implícitas: São a primeira coisa que procuramos quando precisamos de animações, elas são animações prontas, o Flutter possui uma lista de widgets que pode ser animado dessa forma;
- Animações implícitas personalizadas: Quando o que precisamos não está na lista de widgets de animação implícita de Flutter, é hora de criar as animações implícitas personalizadas;
- Animações explícitas: O Flutter também disponibiliza uma série de widgets desse tipo, elas são uma opção quando precisamos obter mais controle sobre as animações ou controlar mais de uma animação ao mesmo tempo, algo que não é possível com animações implícitas;
- Animações explícitas personalizadas: Quando não existem widgets de animação explícita para o que precisamos, é novamente o momento de criar os seus próprios widgets, e para isso personalizamos as animações com duas classes (AnimatedBuilder e AnimatedWidget);
- Animações com pacotes externos: Então, se nenhuma das opções acima por possível, se sua animação se parece mais com um desenho, se for muito complexa para transformar em código, existem pacotes para te auxiliar a inserir elas na sua aplicação.
Vamos conhecer cada um desses tipos e entender quando devemos utilizá-los!
Animações implícitas
Os widgets de animação implícita do Flutter geralmente são a primeira opção procurada por nós pessoas programadoras quando queremos adicionar animações nos aplicativos, pois facilitam a criação de uma animação de forma menos complexa. Eles são widgets prontos, com animações que possuem início e fim, e que não se repetem.
No Flutter existem animações de widgets já existentes, elas são chamadas AnimatedFoo, em que o Foo é o nome do widget que ao ter suas propriedades alteradas é animado automaticamente, veja abaixo alguns dos widgets de animações implícitas:
- AnimatedAlign: Versão animada do widget Align, em que quando o alinhamento é alterado, é feita uma animação para a nova posição;
- AnimatedContainer: Versão animada do widget Container, em que as suas propriedades ao serem alteradas, é feita uma animação da mudança;
- AnimatedCrossFade: Esse widget realiza a mudança entre dois widgets filhos de tamanhos iguais (caso sejam de tamanhos diferentes, o maior é “recortado”), animando a transição entre eles;
- AnimatedDefaultTextStyle: Versão animada do DefaultTextStyle que faz a animação da mudança de estilo do texto sempre que algo é alterado;
- AnimatedList: Esse widget possui uma lista de itens e realiza uma animação sempre que um item é adicionado ou removido;
- AnimatedOpacity: Versão animada do widget Opacity, ele realiza a animação da mudança do valor de opacidade do widget filho;
- AnimatedPositioned: Versão animada do widget Positioned, em que ao ser alterada a posição é feita uma animação da mudança; e
- AnimatedSize: Esse widget anima a mudança das propriedades de borderRadius e elevation do filho.
Ufa! São muitas animações possíveis. Que tal vermos um exemplo de uso do AnimatedContainer para entender como uma animação implícita funciona?
AnimatedContainer
O AnimatedContainer é uma versão animada do widget Container que já conhecemos porém quando uma dessas propriedades muda é automaticamente reproduzida uma animação de mudança. Esse widget possui todas as propriedades que um Container poderia ter e também algumas outras relacionadas a animações. Vamos conhecer ele?
Veja um exemplo de AnimatedContainer cujas propriedades animadas são altura, largura e cor ao apertar um botão:
Como fizemos isso?
- Criamos uma variável que é alterada sempre que o botão é apertado;
- Usamos ela como flag para alterar as propriedades do container;
- Além disso, especificamos uma nova propriedade (que um Container comum não possuiria), a
duration
, que especifica quanto tempo a interpolação (processo de animação entre o valor antigo e o valor novo) deve durar.
AnimatedContainer(
width: _crescer ? 300 : 100,
height: _crescer ? 300 : 100,
duration: const Duration(seconds: 1),
color: _crescer ? Colors.blue : Colors.pink,
)
Você pode mudar os valores, criar funções que geram valores aleatórios de altura, largura, cores, o próprio decoration
, ou seja qualquer propriedade do AnimatedContainer, poderia adicionar um filho ao widget, além de mudar também a duração da animação caso queira uma animação mais lenta ou mais rápida.
Um exemplo de uso do AnimatedContainer pode ser um campo de texto que se expande com informações ao ser clicado, ou seja, quando a pessoa usuária clica no ícone de seta para baixo, os detalhes são exibidos ou ocultados com uma transição suave.
A sua criatividade é o limite!
Curve
Vimos uma nova propriedade, a duration
, que pode ser usada para controlar quanto tempo a interpolação da animação dura. Existe mais uma propriedade importante para conhecer: a curve
.
Curve é uma propriedade que determina como a interpolação deve ser feita. Por padrão, essa propriedade tem valor linear, mas podemos escolher ainda diversas outras opções de como interpolar a animação. Veja um exemplo:
Agora a animação possui uma interpolação diferente, começando mais lenta e terminando mais rapidamente, para isso apenas adicionamos uma curve
do tipo easeInOutQuint
. Olha só:
AnimatedContainer(
width: _crescer ? 300 : 100,
height: _crescer ? 300 : 100,
duration: const Duration(seconds: 1),
color: _crescer ? Colors.blue : Colors.pink,
curve: Curves.easeInOutQuint,
),
Existem diversas curvas que você pode utilizar, caso tenha interesse a documentação de Flutter sobre Curves detalha todas elas.
Animações implícitas personalizadas
Existem muitas animações implícitas integradas no Flutter, contudo quando você pode precisar de alguma animação implícita que não exista na lista de animações prontas do Flutter, você pode criar uma nova, utilizando o TweenAnimationBuilder
.
O TweenAnimationBuilder
anima a interpolação entre (tween em inglês) valores e para isso devemos definir duas propriedades principais:
- Duração da interpolação;
- Intervalo dos valores que desejo animar (pode ser qualquer tipo de valor);
- Construtor que retorna o widget que queremos animar.
No exemplo abaixo estamos animando a mudança de cores (do branco para o azul), com a duração de 2 segundos de interpolação, e o construtor está retornando uma imagem com um filtro de cores.
O código para este exemplo fica dessa forma:
TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: Colors.white,
end: Colors.blue,
),
duration: const Duration(seconds: 2),
builder: (_, Color? color, __) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
color ?? Colors.transparent,
BlendMode.modulate,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.network(_imagePath),
),
);
},
),
Muito legal! Nesse exemplo não precisamos utilizar o setState para alterar o valor do Tween, mas poderíamos utilizar para modificar esses valores dinamicamente, que tal vermos isso?
Usando setState para modificar o valor do Tween
Utilizando agora um widget com estado, podemos alterar o valor do Tween dinamicamente, fazendo com que a mudança de cores e animação, do branco até o azul, agora seja gradual. Para isso, nós vamos:
- Utilizar um novo widget chamado Slider (um controle deslizante);
- Para usar o Slider, vamos ter mais duas variáveis que serão atualizadas utilizando o setState:
- 2.1.
_newValue
: Que deve receber um novo valor (do tipodouble
) sempre que usamos o Slider; - 2.2.
_newColor
: Que deve receber um novo valor (do tipoColor
), variando entre branco e azul e utilizando a variável_newValue
para a interpolação.
- 2.1.
- O valor de
_newColor
deve ser o valor deend
dentro deColorTween
agora.
Dessa maneira, a partir do valor do Slider, atualizamos o valor da nova cor que queremos utilizar, criando um filtro dinâmico para a imagem. Veja o código abaixo:
double _newValue = 0.0;
Color? _newColor = Colors.white;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: Colors.white,
end: _newColor,
),
duration: const Duration(seconds: 2),
builder: (_, Color? color, __) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
color ?? Colors.transparent,
BlendMode.modulate,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.network(_path),
),
);
},
),
),
Slider.adaptive(
value: _newValue,
onChanged: (double value) {
setState(() {
_newValue = value;
_newColor = Color.lerp(
Colors.white,
Colors.blue,
value,
);
});
},
),
],
),
);
}
Muito bem! Aprendemos como fazer uma animação implícita personalizada de maneira dinâmica, e ainda temos algumas propriedades do TweenAnimationBuilder para aprender, são elas: onEnd e Child.
Propriedade Child
Vamos começar pelo Child, ele é o filho do TweenAnimationBuilder. Se reparar bem, não precisamos utilizar ele para fazer a animação, contudo poderíamos e seria uma boa escolha pois:
- Evitaria reconstruir a imagem a cada vez que o construtor fosse chamado;
- A única coisa que precisa ser reconstruída é o filtro de cores;
- Otimizamos o desempenho com isso.
Para usar a propriedade Child:
- Passamos um objeto do tipo Widget chamado
child
(poderia ser qualquer outro nome) no construtor; - Esse objeto deve ser usado no ColorFiltered;
- Por fim, passamos a imagem no Child do próprio TweenAnimationBuilder.
Veja o código abaixo:
TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: Colors.white,
end: Colors.blue,
),
duration: const Duration(seconds: 2),
builder: (_, Color? color, Widget? child) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
color ?? Colors.transparent,
BlendMode.modulate,
),
child: child,
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.network(_imagePath),
),
),
Essa alteração não deve mudar nada visualmente, mas se o componente animado fosse mais complexo, essa otimização seria importante para a sua aplicação.
Propriedade onEnd
Como o nome diz, o parâmetro onEnd é executado no fim da animação. Ele pode ser usado para muitas coisas, podemos fazer um print, por exemplo, mostrar uma mensagem… No nosso exemplo vamos usar para que sempre que chegue ao final da animação, ele volte ao início, dessa maneira:
Agora sempre que chegamos no fim da a animação, verificamos:
- O valor da variável
_newValue
é azul?- 1.1. Se sim, atribuímos a ela a cor branca;
- 1.2 Se não, atribuímos a cor azul.
E o código final fica da seguinte forma:
Color _newColor = Colors.blue;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: Colors.white,
end: _newColor,
),
duration: const Duration(seconds: 2),
builder: (_, Color? color, __) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
color ?? Colors.transparent,
BlendMode.modulate,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.network(_imagePath),
),
);
},
onEnd: () {
setState(() {
if (_newColor == Colors.blue) {
_newColor = Colors.white;
} else {
_newColor = Colors.blue;
}
});
},
),
),
],
),
);
}
Agora conseguimos “rebobinar” a animação, que legal!
Curve no TweenAnimationBuilder
A propriedade Curve, que já conhecemos, determina como a interpolação deve ser feita, e ela funciona da mesma maneira com o TweenAnimationBuilder. Portanto, por padrão a animação tem uma interpolação linear, e podemos escolher outros tipos atribuindo uma Curve diferente.
Para conhecer todas as opções de Curves, veja documentação de Flutter sobre Curves.
Animações explícitas
As animações implícitas são mais facilmente utilizadas pois interpolam-se do início ao fim e acabam automaticamente, com o próprio Flutter controlando por nós. Mas e se quisermos ter mais controle sobre as animações? Então podemos utilizar as animações explícitas!
As animações explícitas possuem algo que as implícitas não poderiam ter (ao menos não sem dar um certo trabalho), elas podem: ser paradas, pausadas e retomadas; podem ser controladas de fato. Os widgets de animação explícita são chamados FooTransition, em que o Foo é o que queremos animar, veja alguns exemplos:
- DecoratedBoxTransition: Versão animada do widget DecoratedBox, ele anima as várias propriedades de um DecoratedBox;
- FadeTransition: Anima a opacidade do widget filho;
- PositionedTransition: Versão animada do widget Positioned, realiza a transição da posição inicial até a final do widget filho;
- RotationTransition: Esse widget anima a rotação do widget filho;
- ScaleTransition: Anima a escala do widget filho;
- SizeTransition: Anima o tamanho, corta e alinha o widget filho; e
- SlideTransition: Anima a posição de um widget filho em relação à posição normal.
Que tal vermos um exemplo de RotationTransition para entendermos melhor como uma animação explícita funciona?
RotationTransition
O RotationTransition realiza a rotação de um widget filho, veja abaixo um exemplo utilizando um Container como filho:
Ele precisa de três propriedades:
child
: Aquilo que ele vai rotacionar;alignment
: O alinhamento do widget, o eixo sobre o qual deve ser realizada a rotação;turn
: Um controlador de animação.
Vamos entender cada uma dessas propriedades!
A começar pelo child
, para reproduzir o exemplo mostrado, o filho do RotationTransition vai ser um Container, portanto:
RotationTransition(
alignment: //...,
turns: //...,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
O eixo sobre o qual queremos que o widget filho rotacione será o centro, então em alignment usaremos Alignment.center
:
RotationTransition(
alignment: Alignment.center,
turns: _animationController,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
Por fim, a propriedade turns
recebe um AnimationController, que é o que vai nos dar controle sobre a animação, diferentemente das animações implícitas que são automáticas. Essa uma parte muito importante dos widgets de animação explícita, então daremos uma atenção especial para ela no próximo tópico. Vamos lá?
AnimationController
Para começar, vamos criar uma instância de AnimationController:
late AnimationController _animationController;
Definimos ela como late
pois queremos defini-la depois, no initState
. E dentro do initState
, ao atribuirmos o AnimationController a um objeto, vamos precisar de alguns detalhes para a animação funcionar, são eles:
duration
: Especifica a duração da animação.vsync
: Mantem o controle da animação na tela. Ele deve receber o valorthis
, e para isso precisa utilizar oSingleTickerProviderStateMixin
na classe State:
class _Exemplo1State extends State<Exemplo1>
with SingleTickerProviderStateMixin {
//...
}
repeat
: Faz com que a animação se repita continuamente.
O InitState
deve ficar da seguinte maneira:
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 15),
vsync: this,
)..repeat();
}
Precisamos também fazer o dispose da animação, para liberar o recurso alocado para a animação quando não estivermos mais utilizando ela. Veja:
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Por fim, basta utilizar no RotationTransition o controlador criado e, assim, a animação já deve estar funcionando!
RotationTransition(
alignment: Alignment.center,
turns: _animationController,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
Parando e retomando a animação
Com o AnimationController, podemos verificar o status da animação, obter informação sobre, além de realizar ações como parar, retroceder e reiniciar a animação. Um exemplo de caso real disso, poderia ser a animação de relógio, que seria pausada ou retomada ao clicar em um botão.
Abaixo um exemplo de parar e reiniciar a animação criada:
if (_animationController.isAnimating) {
_animationController.stop();
} else {
_animationController.repeat();
}
Criei um ElevatedButton para isso e o resultado foi o seguinte:
Legal! Além de ter uma animação que se repete, também podemos controlar ela realizando ações como parar e retomar.
Animações explícitas personalizadas
Já conhecemos os widgets de animação explícita, chamados de FooTransition, mas ainda temos a possibilidade de não encontrar o que queremos dentro daquela lista de widgets prontos, e nesse caso, o que fazemos? Personalizamos as animações explícitas!
As animações explícitas personalizadas são a solução quando precisamos criar nossas próprias animações, e isso é possível utilizando duas classes: AnimatedBuilder e AnimatedWidget.
Vamos conhecê-las!
Para utilizar o AnimatedBuilder vamos precisar de:
animation
: Um controlador de animação;child
: É o widget que vamos animar; ebuilder
: O construtor da animação, ele vai determinar o que deve ser feito.
A começar pelo animation
vamos utilizar o mesmo controlador de animação do exemplo anterior:
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
O child
deve ser um container e estar centralizado, portanto vamos utilizar:
child: Center(
child: Container(
width: 200,
height: 200,
color: Colors.purple,
),
),
E, por fim, o builder
deve ser uma função que:
- Recebe um contexto e um filho que deve animar;
- Retorna uma transformação, que no caso será uma rotação de Child em determinado ângulo, utilizando
Transform.rotate
.
Veja:
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _animationController.value * 2.0 * math.pi,
child: child,
);
},
Ao final, o AnimatedBuilder ficará da seguinte forma:
AnimatedBuilder(
animation: _animationController,
child: Center(
child: Container(
width: 200,
height: 200,
color: Colors.purple,
),
),
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _animationController.value * 2.0 * math.pi,
child: child,
);
},
),
Com isso, realizamos a animação de rotação do Container.
AnimatedWidget
O AnimatedWidget pode ser visto como uma refatoração do AnimatedBuilder, pois extrai o construtor para um código separado. AnimatedWidget é uma classe abstrata, então podemos criar novos componentes animados, que vão estender dessa classe.
Portanto, vamos:
- Criar uma nova classe, que deve estender da classe abstrata AnimatedWidget;
- Ela recebe por parâmetro o controlador de animação;
- Ela deve retornar o
Transform.rotate
, reutilizando o construtor do exemplo anterior de AnimatedBuilder.
A nova classe (que chamei de ContainerAnimatedExample
) ficará da seguinte forma:
import 'dart:math' as math;
import 'package:flutter/material.dart';
class ContainerAnimatedExample extends AnimatedWidget {
const ContainerAnimatedExample({
super.key,
required AnimationController controller,
}) : super(listenable: controller);
Animation<double> get _animationController => listenable as Animation<double>;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _animationController.value * 2.0 * math.pi,
child: Center(
child: Container(
width: 200,
height: 200,
color: Colors.purple,
),
),
);
}
}
E agora o que precisamos é usar a classe de animação separada que criamos na tela da aplicação, passando o controlador de animação do exemplo anterior, o código fica assim:
class AnimatedWidgetExample extends StatefulWidget {
const AnimatedWidgetExample({super.key});
@override
State<AnimatedWidgetExample> createState() => _AnimatedWidgetExampleState();
}
class _AnimatedWidgetExampleState extends State<AnimatedWidgetExample>
with TickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ContainerAnimatedExample(
controller: _animationController,
),
);
}
}
Assim, devemos conseguir exatamente o mesmo resultado do exemplo anterior. Então você pode se perguntar qual devo usar, já que ambos chegam ao mesmo resultado? E na verdade não existe uma resposta correta, segundo a própria documentação de Flutter, é uma questão de gosto.
Uma vantagem do AnimatedWidget é a possibilidade de separar seus widgets de animação individualmente, porém, se seu código de animação não for muito grande, o AnimatedBuilder pode fazer mais sentido. Caso tenha interesse em ler o artigo que fala sobre isso, deixo ele aqui: “Quando devo usar AnimatedBuilder ou AnimatedWidget?”.
Pacotes de animação externos
Até o momento, conhecemos as ferramentas de animações que o próprio Flutter nos proporciona, mas se mesmo com todas elas ainda precisarmos de algo mais, algo que elas não abrangem, recomenda-se o uso de pacotes de animações externos, como Rive e Lottie.
Rive
O pacote Rive para Flutter, anteriormente conhecido como Flare, é outra biblioteca que possibilita a incorporação de animações vetoriais diretamente em aplicativos Flutter. Ela oferece uma plataforma de criação e edição de animações vetoriais, permitindo que os desenvolvedores exportem essas animações e as integrem em seus aplicativos Flutter. Essa integração facilita a criação de interfaces de usuário mais dinâmicas e envolventes, enquanto a interface gráfica do Rive oferece uma maneira intuitiva de criar e ajustar animações, tornando o processo de desenvolvimento visualmente orientado e eficiente.
Lottie
O pacote Lottie para Flutter é uma biblioteca que permite a integração de animações vetoriais complexas diretamente em aplicativos Flutter. Ele simplifica a incorporação de animações interativas e atraentes. O formato JSON usado pelo Lottie é eficiente e otimizado para desempenho, permitindo uma experiência de usuário suave e responsiva, enquanto o Flutter Lottie oferece uma integração fácil por meio de widgets especializados para reproduzir essas animações vetoriais em aplicativos Flutter.
Como escolher uma animação?
Vamos recapitular! Com essa leitura aprendemos principalmente que:
- Animação implícita é a porta de entrada para animações, com ela temos diversas formas de animações, chamadas AnimatedFoo;
- Animação implícita personalizada é uma forma de criar animações implícitas com o TweenBuilderAnimation, quando as animações implícitas não suprem a sua necessidade;
- Animação explícita é uma animação sobre a qual possuímos controle, além de poder ser reproduzida continuamente, diferente das animações implícitas. Com ela temos diversas formas de animações, chamadas FooTransition;
- Animação explícita personalizada é a forma de criar suas próprias animações explícitas com AnimatedBuilder ou AnimatedWidget, caso os widgets FooTransition não tenham o que precisa; e
- Por fim, caso nenhuma das opções acima seja a melhor para você, o Flutter recomenda uso de pacotes externos como Rive e Lottie.
Na documentação do Flutter sobre animações é disponibilizado um fluxograma com as perguntas que você deve fazer ao escolher uma animação e as respostas recomendadas, caso queira conferir, deixo aqui o tópico da documentação com o fluxograma.
Conclusão
No Flutter existem diversas formas de utilizar ou criar suas animações, sendo elas implícitas, explícitas ou com pacotes externos, e é importante entender sobre elas para aplicar em seu projeto a melhor forma.
O Flutter possui uma documentação muito boa sobre animações, recomendo a leitura da Introdução a Animações, a qual foi a base deste artigo. Além disso, recomendo também que verifique o Catálogo de Animações do Flutter, com todos os exemplos de animações que ele disponibiliza.
Espero que tenha gostado da leitura. Bons estudos!