Pular para o conteúdo
charmotion

Como adicionar animação a um app Flutter

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia: Como adicionar animação a um app Flutter, com personagens de demonstração do Charmotion

Para adicionar animação a um app Flutter, use widgets implícitos como AnimatedContainer para mudanças simples de UI e um AnimationController quando precisar controlar o tempo. Use Hero para transições de elemento compartilhado, o pacote lottie para loops vetoriais feitos por designers, o pacote rive para personagens interativos e video_player para clipes.

Principais pontos

  • Animações implícitas fazem a transição do valor antigo para o novo por você; as explícitas rodam em um AnimationController.
  • Hero implementa transições de elemento compartilhado entre rotas.
  • O pacote lottie (3.6.x) é Dart puro e reproduz arquivos com Lottie.asset.
  • O pacote rive (0.14.x) exige RiveNative.init(), depois File.asset e um RiveWidgetController.
  • Verifique MediaQuery.disableAnimationsOf(context) para respeitar a configuração de movimento do usuário.

Já tem um personagem ou logo? Veja seu personagem em telas reais de app: telas de splash, onboarding, carregamento e estado vazio. É grátis, roda no navegador e não envia nada.

Qual abordagem de animação usar no Flutter?

Escolha a ferramenta pelo que se move e pela origem da arte:

Você quer animarUso
Tamanho, cor, padding ou opacidade após uma mudança de estadoWidgets implícitos: AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder
Loops, movimentos escalonados ou guiados por gestosUm AnimationController com widgets de transição
Um elemento que se move entre duas telasHero
Um loop vetorial feito por designerO pacote lottie
Um personagem que reage a eventos do appO pacote rive com uma state machine
Um clipe fixo e pré-renderizadoO plugin video_player

A animação nativa não precisa de assets nem de pacotes extras, então comece por ela para o feedback da UI. Adicione um pacote só quando um designer entregar um arquivo. Lottie e Rive rodam em iOS, Android, desktop e web, então um único asset atende todas as plataformas do seu app Flutter.

Uma regra simples para pacotes: se a animação toca do início ao fim, Lottie ou vídeo bastam. Se o seu código precisa dizer o que ela faz em seguida, como "continue em loop enquanto o upload roda e depois comemore", use Rive.

Qual a diferença entre animações implícitas e explícitas?

A documentação do Flutter divide a animação nativa em dois tipos:

  • Animações implícitas animam uma mudança por você. Passe um novo valor e uma duração para AnimatedContainer, AnimatedOpacity ou TweenAnimationBuilder, e o Flutter interpola a partir do valor antigo.
  • Animações explícitas são controladas pelo seu próprio AnimationController. Você decide quando iniciar, parar, repetir ou inverter.
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeOut,
  width: selected ? 200 : 120,
  height: 120,
  decoration: BoxDecoration(
    color: selected ? Colors.indigo : Colors.grey,
    borderRadius: BorderRadius.circular(16),
  ),
)

Chame setState para alternar selected, e o container anima a largura e a cor. Comece pelos widgets implícitos; eles dão conta da maioria dos botões, cards e toggles.

Se nenhum widget implícito pronto servir, o TweenAnimationBuilder anima qualquer valor que você consiga descrever com um Tween, como um ângulo de rotação, e reconstrói o builder com o valor atual a cada frame.

Seis reações únicas: success, error, yes, no, enter e exit
As reações únicas são disparadas com triggers e voltam ao humor atual quando terminam.

Como usar AnimationController e Hero?

Um AnimationController gera valores de 0,0 a 1,0 ao longo de uma duração e precisa de um ticker vsync, normalmente vindo de SingleTickerProviderStateMixin:

class Pulse extends StatefulWidget {
  const Pulse({super.key, required this.child});
  final Widget child;
  @override
  State<Pulse> createState() => _PulseState();
}

class _PulseState extends State<Pulse> with SingleTickerProviderStateMixin {
  late final AnimationController controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 900),
  )..repeat(reverse: true);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) =>
      FadeTransition(opacity: controller, child: widget.child);
}

Sempre faça o dispose do controller. Widgets de transição como FadeTransition, ScaleTransition e SlideTransition reconstroem apenas o que animam.

Para uma transição de elemento compartilhado entre rotas, envolva o mesmo widget nas duas telas em um Hero com a mesma tag:

Hero(tag: 'avatar', child: CircleAvatar(backgroundImage: image))

Quando você abre a segunda rota, o Flutter faz o widget voar de uma posição para a outra.

Como adicionar um arquivo Lottie no Flutter?

Adicione o pacote lottie (3.6.1 no momento em que este texto foi escrito). Ele é escrito em Dart puro, então roda em qualquer lugar onde o Flutter roda. Declare o arquivo como asset:

dependencies:
  lottie: ^3.6.1

flutter:
  assets:
    - assets/loading.json

Depois, reproduza com Lottie.asset, ou Lottie.network para um arquivo hospedado:

import 'package:lottie/lottie.dart';

Lottie.asset('assets/loading.json', width: 160, height: 160)

Por padrão, a animação toca e se repete. Passe seu próprio AnimationController pelo parâmetro controller se precisar iniciá-la a partir de um evento ou sincronizá-la com outra coisa. O pacote também suporta arquivos dotLottie (.lottie) por meio de um loader personalizado.

O Lottie funciona bem para spinners, checks de sucesso e ilustrações de onboarding que tocam do início ao fim. É menos adequado para um personagem cujo comportamento precisa mudar no meio do loop de acordo com o estado do app.

Como adicionar um arquivo Rive e trocar de estado no Flutter?

Adicione o pacote rive. A documentação do Rive mostra rive: ^0.14.11, que depende de rive_native e permite escolher o renderizador do Rive (Factory.rive) ou o do Flutter (Factory.flutter). Declare assets/character.riv no pubspec.yaml e inicialize o runtime uma única vez:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await RiveNative.init();
  runApp(const MyApp());
}

Carregue o arquivo, crie um controller para a state machine e renderize com RiveWidget:

final f = await File.asset('assets/character.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(
  f!,
  stateMachineSelector: StateMachineSelector.byName('CharacterStateMachine'),
);

// in build():
RiveWidget(controller: controller, fit: Fit.contain)

Para trocar de estado em um arquivo baseado em inputs, busque os inputs na state machine e defina seus valores:

final mood = controller.stateMachine.number('mood');
final success = controller.stateMachine.trigger('success');
mood?.value = 5;   // loading loop
success?.fire();   // one-shot reaction

Faça o carregamento em initState, não em build, e faça o dispose do controller, dos inputs e do arquivo quando o widget sair da tela. Os inputs estão obsoletos em favor do Data Binding, que o runtime do Rive para Flutter também suporta em arquivos com view models. Veja o guia do Rive no Flutter para um widget completo.

Como fazer um vídeo tocar em loop com video_player?

Para um clipe fixo, o plugin video_player é a escolha padrão. Crie um controller, inicialize, depois ative o loop e dê play:

late final VideoPlayerController video;

@override
void initState() {
  super.initState();
  video = VideoPlayerController.asset('assets/logo_reveal.mp4')
    ..setLooping(true)
    ..setVolume(0)
    ..initialize().then((_) {
      setState(() {});
      video.play();
    });
}

@override
void dispose() {
  video.dispose();
  super.dispose();
}

// in build():
video.value.isInitialized
    ? AspectRatio(aspectRatio: video.value.aspectRatio, child: VideoPlayer(video))
    : const SizedBox.shrink()

A cascata (..) configura o controller antes de initialize() terminar; o callback then reconstrói o widget assim que o primeiro frame está disponível. Silenciar com setVolume(0) mantém um loop decorativo sem som.

Vídeo lida bem com detalhes fotográficos e 3D, mas cada variação é mais um arquivo e nada reage ao estado. Use para uma revelação de abertura ou um loop de marketing, e um arquivo Rive para um personagem que precisa responder ao usuário.

Qual a melhor opção para um mascote animado no Flutter?

Para um mascote que reage ao app, o Rive é a escolha prática: um único arquivo guarda todos os loops e reações, e uma state machine cuida das transições. Seu código associa eventos do app a valores:

  • Tela ociosa: mood 0. Chamada de rede em andamento: mood 5.
  • Chamada concluída: dispare success ou error.
  • Usuário ausente por um tempo: um loop de sono.

Mantenha esse mapeamento em um só lugar, como um listener no seu gerenciamento de estado (um ChangeNotifier, Bloc ou provider do Riverpod) que atualiza os inputs. Assim, os widgets nunca precisam saber qual animação está tocando.

Diagrama de uma state machine do Rive: um estado de entrada, dez estados de loop escolhidos por um número mood e estados de reação única por trigger que voltam ao loop atual
Como um .riv do Charmotion é conectado: o número mood escolhe um loop, e os triggers tocam reações únicas a partir de qualquer estado.

O Lottie funciona para um mascote que toca só um loop por tela; trocar de arquivo para cada estado é possível, mas as transições vão ter cortes. Vídeo é a opção mais pesada e a menos flexível.

Se você tem um personagem mas não tem um animador, o Charmotion gera um conjunto de 17 animações a partir de um upload, incluindo um .riv cuja CharacterStateMachine recebe um número mood de 0 a 9 e triggers como success e error, então as chamadas do Flutter acima funcionam sem mudanças. Veja estados de animação de mascote para saber quais estados planejar.

Como manter as animações no Flutter fluidas e acessíveis?

A maior parte dos travamentos vem de reconstruir demais a cada frame. Alguns hábitos ajudam:

  • Use widgets de transição (FadeTransition, ScaleTransition) ou AnimatedBuilder com um child, para que só a parte animada seja reconstruída.
  • Faça o dispose de todo AnimationController, controller do Rive e controller de vídeo.
  • Carregue arquivos Lottie e Rive uma vez por tela, nunca dentro de build.
  • Meça a performance no modo profile em um dispositivo real; o modo debug é bem mais lento.

Respeite a configuração de movimento reduzido da plataforma. MediaQuery.disableAnimationsOf(context) retorna o valor de MediaQueryData.disableAnimations mais próximo e só reconstrói quando essa propriedade muda:

final reduce = MediaQuery.disableAnimationsOf(context);
return reduce
    ? Image.asset('assets/mascot_still.png', width: 220)
    : RiveWidget(controller: controller, fit: Fit.contain);

Mantenha a informação que a animação transmite, como "carregando", disponível em texto. O guia de movimento reduzido explica as regras da WCAG.

Adicione animação a um app Flutter

  1. Comece com animações implícitas. Use AnimatedContainer, AnimatedOpacity ou TweenAnimationBuilder para mudanças simples de UI.
  2. Parta para explícitas quando precisar. Crie um AnimationController com vsync para loops e sequências temporizadas, e faça o dispose dele.
  3. Adicione pacotes para animações feitas por designers. Adicione lottie ou rive ao pubspec.yaml e declare os arquivos em flutter: assets:.
  4. Renderize o arquivo. Use Lottie.asset(), ou chame RiveNative.init() e renderize RiveWidget com um RiveWidgetController.
  5. Conecte o estado do app. Defina stateMachine.number('mood').value e dispare triggers quando o estado mudar.
  6. Respeite as configurações de movimento. Verifique MediaQuery.disableAnimationsOf(context) e mostre uma imagem estática quando for true.

Perguntas frequentes

Devo usar Lottie ou Rive no Flutter?

Use Lottie para animações vetoriais lineares feitas no After Effects, como spinners e checks de sucesso. Use Rive quando a animação precisar reagir ao estado do app, como um mascote que alterna entre idle, loading e success, porque a state machine dele cuida das transições dentro de um único arquivo. Os dois pacotes suportam mobile, desktop e web.

Por que meu AnimationController precisa de vsync?

O ticker vsync avisa o controller quando um novo frame deve ser desenhado e o pausa quando o widget não está visível, o que economiza bateria. Adicione SingleTickerProviderStateMixin à sua classe State e passe this como vsync. Use TickerProviderStateMixin se um mesmo State tiver vários controllers.

O RiveNative.init() precisa rodar antes de tudo?

Ele precisa rodar antes de você carregar qualquer arquivo Rive. Os exemplos do Rive chamam WidgetsFlutterBinding.ensureInitialized() e depois await RiveNative.init() em main(), antes de runApp. Mantenha a chamada na inicialização, e não dentro de um widget, para que ela rode uma única vez e todas as telas possam carregar arquivos imediatamente.

Respostas rápidas deste guia

Fontes

Guias relacionados