1. Introdução
As animações são uma ótima maneira de melhorar a experiência do usuário no app, comunicar informações importantes e deixar o app mais sofisticado e agradável de usar.
Visão geral do framework de animação do Flutter
O Flutter mostra efeitos de animação reconstruindo uma parte da árvore de widgets em cada frame. Ela oferece efeitos de animação pré-criados e outras APIs para facilitar a criação e a composição de animações.
- As animações implícitas são efeitos de animação pré-criados que executam toda a animação automaticamente. Quando o valor alvo da animação muda, ela é executada do valor atual para o valor de destino e exibe cada valor no meio para que o widget seja animado sem problemas. Exemplos de animações implícitas incluem
AnimatedSize
,AnimatedScale
eAnimatedPositioned
. - As animações explícitas também são efeitos de animação pré-criados, mas exigem um objeto
Animation
para funcionar. Por exemplo,SizeTransition
,ScaleTransition
ouPositionedTransition
. - Animation é uma classe que representa uma animação em execução ou interrompida e é composta por um valor que representa o valor de destino para o qual a animação está sendo executada e o status, que representa o valor atual que a animação está exibindo na tela a qualquer momento. Ela é uma subclasse de
Listenable
e notifica os listeners quando o status muda enquanto a animação está em execução. - AnimationController é uma maneira de criar uma animação e controlar o estado dela. Os métodos dele, como
forward()
,reset()
,stop()
erepeat()
, podem ser usados para controlar a animação sem precisar definir o efeito que está sendo mostrado, como a escala, o tamanho ou a posição. - Os tweens são usados para interpolar valores entre um valor inicial e final e podem representar qualquer tipo, como double,
Offset
ouColor
. - As curvas são usadas para ajustar a taxa de mudança de um parâmetro ao longo do tempo. Quando uma animação é executada, é comum aplicar uma curva de abrandamento para acelerar ou desacelerar a taxa de mudança no início ou no fim da animação. As curvas recebem um valor de entrada entre 0,0 e 1,0 e retornam um valor de saída entre 0,0 e 1,0.
O que você vai criar
Neste codelab, você vai criar um jogo de perguntas com várias opções de resposta que tem vários efeitos e técnicas de animação.
Você vai aprender a...
- Criar um widget que anima o tamanho e a cor
- Criar um efeito de virada de cartão 3D
- Usar efeitos de animação pré-criados do pacote de animações
- Adicionar suporte ao gesto de volta preditivo disponível na versão mais recente do Android
O que você vai aprender
Neste codelab, você vai aprender:
- Como usar efeitos de animação implícita para criar animações incríveis sem precisar de muito código.
- Como usar efeitos animados explícitos para configurar seus próprios efeitos usando widgets animados predefinidos, como
AnimatedSwitcher
ouAnimationController
. - Como usar
AnimationController
para definir seu próprio widget que mostra um efeito 3D. - Como usar o pacote
animations
para mostrar efeitos de animação sofisticados com uma configuração mínima.
O que é necessário
- O SDK do Flutter
- Um ambiente de desenvolvimento integrado, como o VSCode ou o Android Studio / IntelliJ
2. Configurar o ambiente de desenvolvimento do Flutter
Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor.
É possível executar o codelab usando qualquer um destes dispositivos:
- Um dispositivo físico Android (recomendado para implementar a volta preditiva na etapa 7) ou iOS conectado ao computador e configurado para o modo de desenvolvedor.
- O simulador do iOS, que exige a instalação de ferramentas do Xcode.
- O Android Emulator, que requer configuração no Android Studio.
- Um navegador (o Chrome é necessário para depuração).
- Um computador desktop Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de builds adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop.
Verificar a instalação
Para verificar se o SDK do Flutter está configurado corretamente e se você tem pelo menos uma das plataformas de destino acima instaladas, use a ferramenta Flutter Doctor:
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio [✓] IntelliJ IDEA Ultimate Edition [✓] VS Code [✓] Connected device (4 available) [✓] Network resources • No issues found!
3. Executar o app inicial
Fazer o download do app inicial
Use git
para clonar o app de início do repositório flutter/samples no GitHub.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
Se preferir, faça o download do código-fonte como um arquivo .zip.
Executar o app
Para executar o app, use o comando flutter run
e especifique um dispositivo de destino, como android
, ios
ou chrome
. Para conferir a lista completa de plataformas compatíveis, consulte a página Plataformas compatíveis.
$ flutter run -d android
Você também pode executar e depurar o app usando o ambiente de desenvolvimento integrado de sua preferência. Consulte a documentação oficial do Flutter para mais informações.
Tour pelo código
O app inicial é um jogo de perguntas com múltipla escolha que consiste em duas telas seguindo o padrão de design modelo-visualização-modelo ou MVVM. A QuestionScreen
(visualização) usa a classe QuizViewModel
(modelo de visualização) para fazer ao usuário perguntas de múltipla escolha da classe QuestionBank
(modelo).
- home_screen.dart: mostra uma tela com um botão New Game.
- main.dart: configura o
MaterialApp
para usar o Material 3 e mostrar a tela inicial. - model.dart: define as classes principais usadas em todo o app.
- question_screen.dart: mostra a interface do jogo de perguntas.
- view_model.dart: armazena o estado e a lógica do jogo de perguntas, exibido pelo
QuestionScreen
.
O app ainda não oferece suporte a efeitos animados, exceto a transição de visualização padrão exibida pela classe Navigator
do Flutter quando o usuário pressiona o botão New Game.
4. Usar efeitos de animação implícita
As animações implícitas são uma ótima escolha em muitas situações, porque não exigem nenhuma configuração especial. Nesta seção, você vai atualizar o widget StatusBar
para que ele mostre uma placar animada. Para encontrar efeitos de animação implícita comuns, navegue pela documentação da API ImplicitlyAnimatedWidget.
Criar o widget de placar não animado
Crie um novo arquivo, lib/scoreboard.dart
, com o seguinte código:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color:
score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
)
],
),
);
}
}
Em seguida, adicione o widget Scoreboard
aos filhos do widget StatusBar
, substituindo os widgets Text
que mostravam a pontuação e a contagem total de perguntas. O editor vai adicionar automaticamente o import "scoreboard.dart"
necessário na parte de cima do arquivo.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
Esse widget mostra um ícone de estrela para cada pergunta. Quando uma pergunta é respondida corretamente, outra estrela acende instantaneamente sem nenhuma animação. Nas próximas etapas, você vai ajudar a informar ao usuário que a pontuação dele mudou, animando o tamanho e a cor.
Usar um efeito de animação implícita
Crie um novo widget chamado AnimatedStar
que use um widget AnimatedScale
para mudar o valor de scale
de 0.5
para 1.0
quando a estrela ficar ativa:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Agora, quando o usuário responde a uma pergunta corretamente, o widget AnimatedStar
atualiza o tamanho usando uma animação implícita. O color
do Icon
não é animado aqui, apenas o scale
, que é feito pelo widget AnimatedScale
.
Usar um Tween para interpolar entre dois valores
Observe que a cor do widget AnimatedStar
muda imediatamente depois que o campo isActive
muda para "true".
Para conseguir um efeito de cor animado, tente usar um widget AnimatedContainer
(que é outra subclasse de ImplicitlyAnimatedWidget
), porque ele pode animar automaticamente todos os atributos, incluindo a cor. Infelizmente, nosso widget precisa mostrar um ícone, não um contêiner.
Você também pode tentar usar AnimatedIcon
, que implementa efeitos de transição entre as formas dos ícones. Mas não há uma implementação padrão de um ícone de estrela na classe AnimatedIcons
.
Em vez disso, vamos usar outra subclasse de ImplicitlyAnimatedWidget
chamada TweenAnimationBuilder
, que usa um Tween
como parâmetro. Um interpolador é uma classe que recebe dois valores (begin
e end
) e calcula os valores intermediários para que uma animação possa mostrá-los. Neste exemplo, vamos usar um ColorTween
, que atende à interface Tween<Color>
necessária para criar o efeito de animação.
Selecione o widget Icon
e use a ação rápida "Wrap with Builder" no seu ambiente de desenvolvimento integrado. Mude o nome para TweenAnimationBuilder
. Em seguida, informe a duração e o ColorTween
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(
Icons.star,
size: 50,
color: value, // Modify from here...
);
}, // To here.
),
);
}
}
Agora, faça uma recarga automática do app para conferir a nova animação.
O valor end
da ColorTween
muda com base no valor do parâmetro isActive
. Isso ocorre porque o TweenAnimationBuilder
executa a animação novamente sempre que o valor Tween.end
muda. Quando isso acontece, a nova animação é executada do valor atual para o novo valor final, o que permite mudar a cor a qualquer momento (mesmo durante a execução da animação) e exibir um efeito de animação suave com os valores intermediários corretos.
Aplicar uma curva
Esses dois efeitos de animação são executados a uma taxa constante, mas as animações geralmente são mais interessantes e informativas quando aceleram ou diminuem a velocidade.
Um Curve
aplica uma função de aceleração, que define a taxa de mudança de um parâmetro ao longo do tempo. O Flutter vem com uma coleção de curvas de transição pré-criadas na classe Curves
, como easeIn
ou easeOut
.
Esses diagramas (disponíveis na página de documentação da API Curves
) dão uma ideia de como as curvas funcionam. As curvas convertem um valor de entrada entre 0,0 e 1,0 (exibido no eixo x) em um valor de saída entre 0,0 e 1,0 (exibido no eixo y). Esses diagramas também mostram uma prévia de como vários efeitos de animação ficam quando usam uma curva de transição.
Crie um novo campo em AnimatedStar chamado _curve
e transmita-o como um parâmetro para os widgets AnimatedScale
e TweenAnimationBuilder
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(
Icons.star,
size: 50,
color: value,
);
},
),
);
}
}
Neste exemplo, a curva elasticOut
fornece um efeito de mola exagerado que começa com um movimento de mola e se equilibra até o final.
Faça uma recarga automática do app para conferir essa curva aplicada a AnimatedSize
e TweenAnimationBuilder
.
Usar o DevTools para ativar animações lentas
Para depurar qualquer efeito de animação, o Flutter DevTools oferece uma maneira de desacelerar todas as animações no app para que você possa conferir a animação com mais clareza.
Para abrir o DevTools, verifique se o app está em execução no modo de depuração e abra o Widget Inspector selecionando-o na Debug toolbar no VSCode ou selecionando o botão Open Flutter DevTools na Debug tool window no IntelliJ / Android Studio.
Quando o inspetor de widgets estiver aberto, clique no botão Animações lentas na barra de ferramentas.
5. Usar efeitos de animação explícitos
Assim como as animações implícitas, as animações explícitas são efeitos de animação predefinidos, mas, em vez de usar um valor de destino, elas usam um objeto Animation
como parâmetro. Isso as torna úteis em situações em que a animação já é definida por uma transição de navegação, AnimatedSwitcher
ou AnimationController
, por exemplo.
Usar um efeito de animação explícito
Para começar com um efeito de animação explícito, envolva o widget Card
com um AnimatedSwitcher
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
Por padrão, AnimatedSwitcher
usa um efeito de transição, mas você pode substituir isso usando o parâmetro transitionBuilder
. O builder de transição fornece o widget filho que foi transmitido para o AnimatedSwitcher
e um objeto Animation
. Essa é uma ótima oportunidade para usar uma animação explícita.
Para este codelab, a primeira animação explícita que vamos usar é SlideTransition
, que usa um Animation<Offset>
que define o deslocamento inicial e final entre os widgets de entrada e saída.
Os tweens têm uma função auxiliar, animate()
, que converte qualquer Animation
em outro Animation
com o tween aplicado. Isso significa que um Tween<Offset>
pode ser usado para converter o Animation<double>
fornecido pelo AnimatedSwitcher
em um Animation<Offset>
, que será fornecido ao widget SlideTransition
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Observe que isso usa Tween.animate
para aplicar um Curve
ao Animation
e, em seguida, convertê-lo de um Tween<double>
que varia de 0,0 a 1,0 para um Tween<Offset>
que faz a transição de -0,1 a 0,0 no eixo x.
Como alternativa, a classe Animation tem uma função drive()
que recebe qualquer Tween
(ou Animatable
) e o converte em um novo Animation
. Isso permite que as transições sejam "encadeadas", tornando o código resultante mais conciso:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
Outra vantagem de usar animações explícitas é que elas podem ser facilmente compostas. Adicione outra animação explícita, FadeTransition, que usa a mesma animação curvada, envolvendo o widget SlideTransition.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
Personalizar o layoutBuilder
Talvez você note um pequeno problema com o AnimationSwitcher. Quando um card de pergunta muda para uma nova pergunta, ele é exibido no centro do espaço disponível enquanto a animação está em execução. Quando a animação é interrompida, o widget é fixado na parte de cima da tela. Isso causa uma animação instável porque a posição final do card de pergunta não corresponde à posição enquanto a animação está em execução.
Para corrigir isso, o AnimatedSwitcher também tem um parâmetro layoutBuilder, que pode ser usado para definir o layout. Use essa função para configurar o Layout Builder e alinhar o card à parte de cima da tela:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
Esse código é uma versão modificada do defaultLayoutBuilder da classe AnimatedSwitcher, mas usa Alignment.topCenter
em vez de Alignment.center
.
Resumo
- As animações explícitas são efeitos de animação que usam um objeto Animation (ao contrário dos ImplicitlyAnimatedWidgets, que usam um valor e duração de destino)
- A classe Animation representa uma animação em execução, mas não define um efeito específico.
- Use Tween().animate ou Animation.drive() para aplicar Tweens e curvas (usando CurveTween) a uma animação.
- Use o parâmetro layoutBuilder do AnimatedSwitcher para ajustar a disposição das filhas.
6. Controlar o estado de uma animação
Até agora, todas as animações foram executadas automaticamente pelo framework. As animações implícitas são executadas automaticamente, e os efeitos de animação explícitos exigem uma animação para funcionar corretamente. Nesta seção, você vai aprender a criar seus próprios objetos de animação usando um AnimationController e a usar uma TweenSequence para combinar Tweens.
Executar uma animação usando um AnimationController
Para criar uma animação usando um AnimationController, siga estas etapas:
- Criar um StatefulWidget
- Use o mixin SingleTickerProviderStateMixin na classe de estado para fornecer um Ticker ao AnimationController.
- Inicializar o AnimationController no método de ciclo de vida initState, fornecendo o objeto de estado atual ao parâmetro
vsync
(TickerProvider). - Verifique se o widget é recriado sempre que o AnimationController notifica os listeners, usando o AnimatedBuilder ou chamando listen() e setState manualmente.
Crie um novo arquivo, flip_effect.dart, e copie e cole o seguinte código:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
Essa classe configura um AnimationController e executa a animação novamente sempre que o framework chama didUpdateWidget para notificar que a configuração do widget mudou e que pode haver um novo widget filho.
O AnimatedBuilder garante que a árvore de widgets seja recriada sempre que o AnimationController notificar os listeners. O widget Transform é usado para aplicar um efeito de rotação 3D para simular a virada de um cartão.
Para usar esse widget, envolva cada card de resposta com um CardFlipEffect. Forneça um key
ao widget de card:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
Agora faça uma recarga dinâmica do app para ver os cards de resposta sendo virados usando o widget CardFlipEffect.
Essa classe se parece muito com um efeito de animação explícito. Na verdade, é uma boa ideia estender a classe AnimatedWidget diretamente para implementar sua própria versão. Como essa classe precisa armazenar o widget anterior no estado, ela precisa usar um StatefulWidget. Para saber mais sobre como criar seus próprios efeitos de animação explícita, consulte a documentação da API para AnimatedWidget.
Adicionar um atraso usando TweenSequence
Nesta seção, você vai adicionar um atraso ao widget CardFlipEffect para que cada card vire um de cada vez. Para começar, adicione um novo campo chamado delayAmount
.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
Em seguida, adicione o delayAmount
ao método de build AnswerCards
.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Em seguida, em _CardFlipEffectState
, crie uma nova animação que aplique o atraso usando um TweenSequence
. Não use utilitários da biblioteca dart:async
, como Future.delayed
. Isso ocorre porque o atraso faz parte da animação e não é algo que o widget controla explicitamente quando usa o AnimationController. Isso facilita a depuração do efeito de animação ao ativar animações lentas no DevTools, já que ele usa o mesmo TickerProvider.
Para usar um TweenSequence
, crie dois TweenSequenceItem
s, um contendo um ConstantTween
que mantém a animação em 0 por uma duração relativa e um Tween
normal que vai de 0.0
a 1.0
.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: widget.duration * (widget.delayAmount + 1));
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Por fim, substitua a animação do AnimationController pela nova animação atrasada no método de build.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
Agora faça uma recarga automática do app e observe os cards sendo virados um por um. Para um desafio, tente mudar a perspectiva do efeito 3D fornecido pelo widget Transform
.
7. Usar transições de navegação personalizadas
Até agora, aprendemos a personalizar efeitos em uma única tela, mas outra maneira de usar animações é para fazer a transição entre telas. Nesta seção, você vai aprender a aplicar efeitos de animação às transições de tela usando efeitos de animação integrados e efeitos de animação pré-criados fornecidos pelo pacote oficial animations (link em inglês) no pub.dev.
Animar uma transição de navegação
A classe PageRouteBuilder
é um Route
que permite personalizar a animação de transição. Ele permite que você substitua o callback transitionBuilder
, que fornece dois objetos Animation, representando a animação de entrada e saída executada pelo Navigation.
Para personalizar a animação de transição, substitua o MaterialPageRoute
por um PageRouteBuilder
e personalize a animação de transição quando o usuário navegar da HomeScreen
para a QuestionScreen
. Use uma FadeTransition (um widget explicitamente animado) para fazer com que a nova tela apareça em cima da anterior.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
O pacote de animações oferece efeitos de animação predefinidos, como FadeThroughTransition. Importe o pacote de animações e substitua a FadeTransition pelo widget FadeThroughTransition:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Personalizar a animação de volta preditiva
A volta preditiva é um novo recurso do Android que permite ao usuário dar uma olhada atrás da rota ou do app atual para saber o que está por trás antes de navegar. A animação de peek é determinada pelo local do dedo do usuário enquanto ele é arrastado de volta pela tela.
O Flutter oferece suporte à volta preditiva do sistema ao ativar o recurso no nível do sistema quando o Flutter não tem rotas para destacar na pilha de navegação, ou seja, quando uma volta sai do app. Essa animação é processada pelo sistema, e não pelo próprio Flutter.
O Flutter também oferece suporte à volta preditiva ao navegar entre rotas em um app Flutter. Um PageTransitionsBuilder especial chamado PredictiveBackPageTransitionsBuilder
detecta gestos de volta preditiva do sistema e direciona a transição de página com o progresso do gesto.
A volta preditiva só tem suporte no Android U e versões mais recentes, mas o Flutter voltará ao comportamento original do gesto de volta e ao ZoomPageTransitionBuilder. Confira nossa postagem do blog para saber mais, incluindo uma seção sobre como configurar no seu app.
Na configuração do ThemeData do app, configure o PageTransitionsTheme para usar o PredictiveBack no Android e o efeito de transição de desbotamento do pacote de animações em outras plataformas:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Agora você pode mudar a chamada Navigator.push() para um MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Usar a transição de desbotamento para mudar a pergunta atual
O widget AnimatedSwitcher fornece apenas uma animação no callback do builder. Para resolver esse problema, o pacote animations
fornece um PageTransitionSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Usar o OpenContainer
O widget OpenContainer do pacote animations
oferece um efeito de animação de transformação de contêiner que se expande para criar uma conexão visual entre dois widgets.
O widget retornado por closedBuilder
é mostrado inicialmente e se expande para o widget retornado por openBuilder
quando o contêiner é tocado ou quando o callback openContainer
é chamado.
Para conectar o callback openContainer
ao modelo de visualização, adicione um novo elemento de visualização ao widget QuestionCard e armazene um callback que será usado para mostrar a tela "Game Over":
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel =
QuizViewModel(onGameOver: _handleGameOver);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
)
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
Adicione um novo widget, GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text(
'You Win!',
style: Theme.of(context).textTheme.displayLarge,
),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
No widget QuestionCard, substitua o card por um widget OpenContainer do pacote de animações, adicionando dois novos campos para o callback do viewModel e do contêiner aberto:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}
8. Parabéns
Parabéns! Você adicionou efeitos de animação a um app Flutter e aprendeu sobre os componentes principais do sistema de animação do Flutter. Mais especificamente, você aprendeu:
- Como usar um ImplicitlyAnimatedWidget
- Como usar um ExplicitlyAnimatedWidget
- Como aplicar curvas e interpolações a uma animação
- Como usar widgets de transição pré-criados, como AnimatedSwitcher ou PageRouteBuilder
- Como usar efeitos de animação pré-criados do pacote
animations
, como FadeThroughTransition e OpenContainer - Como personalizar a animação de transição padrão, incluindo a adição de suporte à volta preditiva no Android.
Qual é a próxima etapa?
Confira alguns desses codelabs:
- Como criar o layout de um app responsivo animado com o Material 3
- Como criar transições incríveis com o Material Design para o Flutter
- Deixe seu app do Flutter lindo, não chato
Ou faça o download do app de exemplo de animações, que mostra várias técnicas de animação.
Leia mais
Encontre mais recursos de animação em flutter.dev:
- Introdução às animações
- Tutorial de animações (tutorial)
- Animações implícitas (tutorial)
- Animar as propriedades de um contêiner (cookbook)
- Esvaecer um widget (livro de receitas)
- Animações principais
- Animar uma transição de rota de página (cookbook)
- Animar um widget usando uma simulação de física (livro de receitas)
- Animações intercaladas
- Widgets de animação e movimento (catálogo de widgets)
Ou confira estes artigos no Medium:
- Análise detalhada da animação
- Animações implícitas personalizadas no Flutter
- Gerenciamento de animação com Flutter e Flux / Redux
- Como escolher o widget de animação do Flutter certo para você?
- Animações direcionais com animações explícitas integradas
- Noções básicas de animação do Flutter com animações implícitas
- Quando devo usar AnimatedBuilder ou AnimatedWidget?