Sobre este codelab
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. Ele fornece 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. Os exemplos incluemSizeTransition
,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 apresenta 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
- Adição de 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 compilação adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop (link em inglês).
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
Também é possível 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 um placar animado. 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
nos 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(isActive: score > i), // Edit this line.
],
),
);
}
}
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 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 exibi-los. Neste exemplo, vamos usar um ColorTween
, que atende à interface Tween
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); // And modify this line.
},
),
);
}
}
Agora, faça uma recarga automática do app para conferir a nova animação.
O valor end
do 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 fim.
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 as ferramentas de desenvolvedor, 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 pré-criados, 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
pode ser usado para converter a Animation
fornecida pelo AnimatedSwitcher
em uma Animation
, que será fornecida 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
que varia de 0,0 a 1,0 para um Tween
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 do uso de animações explícitas é que elas podem ser compostas juntas. 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 QuestionCard
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 que usam um objeto
Animation
(ao contrário deImplicitlyAnimatedWidgets
, que usam umvalue
eduration
de destino) - A classe
Animation
representa uma animação em execução, mas não define um efeito específico. - Use
Tween().animate
ouAnimation.drive()
para aplicarTweens
eCurves
(usandoCurveTween
) a uma animação. - Use o parâmetro
layoutBuilder
doAnimatedSwitcher
para ajustar a disposição dos filhos.
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 um Animation
para funcionar corretamente. Nesta seção, você vai aprender a criar seus próprios objetos Animation
usando um AnimationController
e usar um TweenSequence
para combinar Tween
s.
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 classeState
para fornecer umTicker
aoAnimationController
- Inicialize o
AnimationController
no método de ciclo de vidainitState
, fornecendo o objetoState
atual ao parâmetrovsync
(TickerProvider
). - Verifique se o widget é recriado sempre que o
AnimationController
notifica os listeners, usandoAnimatedBuilder
ou chamandolisten()
esetState
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
notifica os listeners. O widget Transform
é usado para aplicar um efeito de rotação 3D e simular a virada de um card.
Para usar esse widget, envolva cada card de resposta com um widget CardFlipEffect
. Forneça um key
ao widget 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 State
, ela precisa usar um StatefulWidget
. Para saber mais sobre como criar seus próprios efeitos de animação explícitos, 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 um novo Animation
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>([ // Add from here...
if (widget.delayAmount > 0)
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: widget.delayAmount,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
]).animate(_animationController); // To here.
}
Por fim, substitua a animação AnimationController
pela nova animação atrasada no método 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 a transições de tela usando efeitos de animação integrados e efeitos de animação pré-criados fornecidos pelo pacote oficial animations 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 um 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( // Add from here...
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
), // To here.
);
},
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 o 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( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
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 arrasta para trás na 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 do Flutter. Um PageTransitionsBuilder
especial chamado PredictiveBackPageTransitionsBuilder
detecta gestos de volta preditiva do sistema e controla 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 vai retornar 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 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),
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 uma MaterialPageRoute
.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
Usar a transição de desbotamento para mudar a pergunta atual
O widget AnimatedSwitcher
fornece apenas um Animation
no callback do builder. Para resolver isso, 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( // Add from here...
layoutBuilder: (entries) {
return Stack(alignment: Alignment.topCenter, children: entries);
},
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
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,
),
),
),
);
}
}
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
é exibido 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 passe ao viewModel
no 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 animations
, adicionando dois novos campos para o callback de viewModel
e 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
Curves
eTweens
a uma animação - Como usar widgets de transição pré-criados, como
AnimatedSwitcher
ouPageRouteBuilder
- Como usar efeitos de animação pré-criados do pacote
animations
, comoFadeThroughTransition
eOpenContainer
- Como personalizar a animação de transição padrão, incluindo a adição de suporte à volta preditiva no Android.
A seguir
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.
Leitura adicional
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 (cookbook)
- 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 qual widget de animação do Flutter é ideal 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?