Animando aplicativos com Flutter

Publicados: 2022-03-10
Resumo rápido ↬ O Flutter oferece excelente suporte de animação para aplicativos multiplataforma. Este artigo explora a nova maneira não convencional e mais fácil de adicionar animações a aplicativos. O que exatamente são esses novos widgets de “Animação e Movimento” e como podemos usá-los para adicionar animações simples e complexas?

Aplicativos para qualquer plataforma são elogiados quando são intuitivos, bonitos e fornecem feedback agradável às interações do usuário. A animação é uma das maneiras de fazer exatamente isso.

O Flutter, um framework multiplataforma, amadureceu nos últimos dois anos para incluir suporte para web e desktop. Ele ganhou uma reputação de que os aplicativos desenvolvidos com ele são suaves e bonitos. Com seu rico suporte de animação, maneira declarativa de escrever UI, “Hot Reload” e outros recursos, agora é uma estrutura completa de plataforma cruzada.

Se você está começando com o Flutter e quer aprender uma maneira não convencional de adicionar animação, então você está no lugar certo: vamos explorar o reino da animação e widgets de movimento, uma forma implícita de adicionar animações.

Flutter é baseado no conceito de widgets. Cada componente visual de um aplicativo é um widget — pense neles como visualizações no Android. Flutter fornece suporte de animação usando uma classe Animation, um objeto “AnimationController” para gerenciamento e “Tween” para interpolar o intervalo de dados. Esses três componentes trabalham juntos para fornecer uma animação suave. Como isso requer criação manual e gerenciamento de animação, é conhecido como uma forma explícita de animação.

Agora deixe-me apresentá-lo aos widgets de animação e movimento. O Flutter fornece vários widgets que suportam inerentemente a animação. Não há necessidade de criar um objeto de animação ou qualquer controlador, pois toda a animação é tratada por esta categoria de widgets. Basta escolher o widget apropriado para a animação necessária e passar os valores das propriedades do widget para animar. Esta técnica é uma forma implícita de animar.

Hierarquia de animação no Flutter. (Visualização grande)

O gráfico acima define aproximadamente a hierarquia de animação no Flutter, como a animação explícita e implícita é suportada.

Alguns dos widgets animados abordados neste artigo são:

  • AnimatedOpacity
  • AnimatedCrossFade
  • AnimatedAlign
  • AnimatedPadding
  • AnimatedSize
  • AnimatedPositioned .

O Flutter não apenas fornece widgets animados predefinidos, mas também um widget genérico chamado AnimatedWidget , que pode ser usado para criar widgets animados implicitamente personalizados. Como é evidente pelo nome, esses widgets pertencem à categoria de widgets animados e de movimento e, portanto, têm algumas propriedades comuns que nos permitem fazer animações muito mais suaves e com melhor aparência.

Deixe-me explicar essas propriedades comuns agora, pois elas serão usadas posteriormente em todos os exemplos.

  • duration
    A duração sobre a qual animar os parâmetros.
  • reverseDuration
    A duração da animação reversa.
  • curve
    A curva a ser aplicada ao animar os parâmetros. Os valores interpolados podem ser obtidos de uma distribuição linear ou, se e quando especificado, podem ser obtidos de uma curva.

Vamos começar a jornada criando um aplicativo simples que chamaremos de “Citado”. Ele exibirá uma cotação aleatória toda vez que o aplicativo for iniciado. Duas coisas a serem observadas: primeiro, todas essas citações serão codificadas no aplicativo; e segundo, nenhum dado do usuário será salvo.

Nota : Todos os arquivos para esses exemplos podem ser encontrados no GitHub.

Mais depois do salto! Continue lendo abaixo ↓

Começando

O Flutter deve estar instalado e você precisará de alguma familiaridade com o fluxo básico antes de prosseguir. Um bom lugar para começar é “Usando o Flutter do Google para um desenvolvimento móvel verdadeiramente multiplataforma”.

Crie um novo projeto Flutter no Android Studio.

Novo menu de projeto de vibração no Android Studio. (Visualização grande)

Isso abrirá um novo assistente de projeto, onde você pode configurar os conceitos básicos do projeto.

Tela de seleção de tipo de projeto Flutter. (Visualização grande)

Na tela de seleção de tipo de projeto, existem vários tipos de projetos Flutter, cada um atendendo a um cenário específico. Para este tutorial, escolha Flutter Application e pressione Next .

Agora você precisa inserir algumas informações específicas do projeto: o nome e o caminho do projeto, o domínio da empresa e assim por diante. Dê uma olhada na imagem abaixo.

Tela de configuração do aplicativo Flutter. (Visualização grande)

Adicione o nome do projeto, o caminho do SDK Flutter, o local do projeto e uma descrição opcional do projeto. Pressione Avançar .

Tela de nome do pacote do aplicativo Flutter. (Visualização grande)

Cada aplicativo (seja Android ou iOS) requer um nome de pacote exclusivo. Normalmente, você usa o reverso do domínio do seu site; por exemplo, com.google ou com.yahoo. Pressione Concluir para gerar um aplicativo Flutter funcional.

O projeto de amostra gerado. (Visualização grande)

Uma vez que o projeto é gerado, você deve ver a tela mostrada acima. Abra o arquivo main.dart (destacado na captura de tela). Este é o arquivo principal do aplicativo. O projeto de amostra é completo em si mesmo e pode ser executado diretamente em um emulador ou dispositivo físico sem qualquer modificação.

Substitua o conteúdo do arquivo main.dart pelo seguinte snippet de código:

 import 'package:animated_widgets/FirstPage.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Animated Widgets', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, accentColor: Colors.redAccent, ), home: FirstPage(), ); } }

Esse código limpa o arquivo main.dart apenas adicionando informações simples relevantes para a criação de um novo aplicativo. A classe MyApp retorna um objeto: um widget MaterialApp , que fornece a estrutura básica para criar aplicativos em conformidade com o Material Design. Para tornar o código mais estruturado, crie dois novos arquivos dart dentro da pasta lib : FirstPage.dart e Quotes.dart .

O arquivo FirstPage.dart. (Visualização grande)

FirstPage.dart conterá todo o código responsável por todos os elementos visuais (widgets) necessários para nosso aplicativo Quoted. Toda a animação é tratada neste arquivo.

Nota : Mais adiante no artigo, todos os trechos de código para cada widget animado são adicionados a este arquivo como filhos do widget Scaffold. Para obter mais informações, este exemplo no GitHub pode ser útil.

Comece adicionando o seguinte código a FirstPage.dart . Este é o código parcial onde outras coisas serão adicionadas posteriormente.

 import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
O arquivo Quotes.dart. (Visualização grande)

O arquivo Quotes.dart contém uma lista de todas as cotações codificadas. Um ponto a ser observado aqui é que a lista é um objeto estático. Isso significa que ele pode ser usado em outros lugares sem criar um novo objeto da classe Quotes. Isso é escolhido por design, pois a lista acima atua simplesmente como um utilitário.

Adicione o seguinte código a este arquivo:

 class Quotes { static const quotesArray = [ "Good, better, best. Never let it rest. 'Til your good is better and your better is best", "It does not matter how slowly you go as long as you do not stop.", "Only I can change my life. No one can do it for me." ]; }

O esqueleto do projeto agora está pronto, então vamos detalhar o Quoted um pouco mais.

AnimatedOpacity

Para dar um toque pessoal ao app, seria bom saber o nome do usuário, então vamos pedir e mostrar um próximo botão. Até que o usuário insira seu nome, esse botão fica oculto e aparecerá normalmente quando um nome for fornecido. Precisamos de algum tipo de animação de visibilidade para o botão, mas existe um widget para isso? Sim existe.

Insira AnimatedOpacity . Este widget se baseia no widget Opacity adicionando suporte de animação implícita. Como o usamos? Lembre-se do nosso cenário: precisamos mostrar um próximo botão com visibilidade animada. Envolvemos o widget de botão dentro do widget AnimatedOpacity , alimentamos alguns valores apropriados e adicionamos uma condição para acionar a animação - e o Flutter pode lidar com o resto.

 _getAnimatedOpacityButton() { return AnimatedOpacity( duration: Duration(seconds: 1), reverseDuration: Duration(seconds: 1), curve: Curves.easeInOut, opacity: showNextButton ? 1 : 0, child: _getButton(), ); }
Animação de opacidade do próximo botão. (Visualização grande)

O widget AnimatedOpacity tem duas propriedades obrigatórias:

  • opacity
    Um valor de 1 significa completamente visível; 0 (zero) significa oculto. Durante a animação, o Flutter interpola valores entre esses dois extremos. Você pode ver como uma condição é colocada para alterar a visibilidade, acionando assim a animação.
  • child
    O widget filho que terá sua visibilidade animada.

Agora você deve entender como é realmente simples adicionar animação de visibilidade com o widget implícito. E todos esses widgets seguem as mesmas diretrizes e são fáceis de usar. Vamos para o próximo.

AnimatedCrossFade

Temos o nome do usuário, mas o widget ainda está aguardando entrada. Na etapa anterior, conforme o usuário digita seu nome, exibimos o próximo botão. Agora, quando o usuário pressiona o botão, quero parar de aceitar a entrada e mostrar o nome digitado. Há muitas maneiras de fazer isso, é claro, mas talvez possamos ocultar o widget de entrada e mostrar um widget de texto não editável. Vamos experimentá-lo usando o widget AnimatedCrossFade .

Este widget requer dois filhos, pois o widget faz crossfade entre eles com base em alguma condição. Uma coisa importante a ter em mente ao usar este widget é que ambos os filhos devem ter a mesma largura. Se a altura for diferente, o widget mais alto será cortado na parte inferior. Neste cenário, dois widgets serão usados ​​como filhos: input e label.

 _getAnimatedCrossfade() { return AnimatedCrossFade( duration: Duration(seconds: 1), alignment: Alignment.center, reverseDuration: Duration(seconds: 1), firstChild: _getNameInputWidget(), firstCurve: Curves.easeInOut, secondChild: _getNameLabelWidget(), secondCurve: Curves.easeInOut, crossFadeState: showNameLabel ? CrossFadeState.showSecond : CrossFadeState.showFirst, ); }
Fading cruzado entre o widget de entrada e o widget de nome. (Visualização grande)

Este widget requer um conjunto diferente de parâmetros obrigatórios:

  • crossFadeState
    Este estado determina qual criança mostrar.
  • firstChild
    Especifica o primeiro filho para este widget.
  • secondChild
    Especifica o segundo filho.

AnimatedAlign

Neste ponto, o rótulo do nome está posicionado no centro da tela. Ele ficará muito melhor no topo, pois precisamos do centro da tela para mostrar as cotações. Simplificando, o alinhamento do widget de rótulo de nome deve ser alterado do centro para o topo. E não seria legal animar essa mudança de alinhamento junto com a animação cross-fade anterior? Vamos fazer isso.

Como sempre, várias técnicas podem ser usadas para conseguir isso. Como o widget de rótulo de nome já está alinhado ao centro, animar seu alinhamento seria muito mais simples do que manipular os valores superior e esquerdo do widget. O widget AnimatedAlign é perfeito para este trabalho.

Para iniciar esta animação, é necessário um gatilho. O único propósito deste widget é animar a mudança de alinhamento, então ele tem apenas algumas propriedades: adicionar um filho, definir seu alinhamento, acionar a mudança de alinhamento e pronto.

 _getAnimatedAlignWidget() { return AnimatedAlign( duration: Duration(seconds: 1), curve: Curves.easeInOut, alignment: alignTop ? Alignment.topLeft : Alignment.center, child: _getAnimatedCrossfade(), ); }
Animação de alinhamento do widget de nome. (Visualização grande)

Possui apenas duas propriedades obrigatórias:

  • criança:
    A criança cujo alinhamento será modificado.
  • alinhamento:
    Valor de alinhamento necessário.

Este widget é realmente simples, mas os resultados são elegantes. Além disso, vimos com que facilidade podemos usar dois widgets animados diferentes para criar uma animação mais complexa. Esta é a beleza dos widgets animados.

AnimatedPadding

Agora temos o nome do usuário no topo, suavemente animado sem muito esforço, usando diferentes tipos de widgets animados. Vamos adicionar uma saudação, "Oi", antes do nome. Adicionar um widget de texto com o valor “Oi”, na parte superior fará com que ele se sobreponha ao widget de texto de saudação, semelhante à imagem abaixo.

Os widgets de saudação e nome se sobrepõem. (Visualização grande)

E se o widget de texto de nome tivesse algum preenchimento à esquerda? Aumentar o preenchimento esquerdo definitivamente funcionará, mas espere: podemos aumentar o preenchimento com alguma animação? Sim, e é isso que AnimatedPadding faz. Para tornar tudo isso muito mais bonito, vamos fazer com que o widget de texto de saudação apareça e o preenchimento do widget de texto de nome aumente ao mesmo tempo.

 _getAnimatedPaddingWidget() { return AnimatedPadding( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, padding: increaseLeftPadding ? EdgeInsets.only(left: 28.0) : EdgeInsets.only(left: 0), child: _getAnimatedCrossfade(), ); }

Como a animação acima deve ocorrer somente após a conclusão do alinhamento animado anterior, precisamos atrasar o acionamento dessa animação. Fugindo do tópico brevemente, este é um bom momento para falar sobre um mecanismo popular para adicionar atraso. O Flutter fornece várias dessas técnicas, mas o construtor Future.delayed é uma das abordagens mais simples, limpas e legíveis. Por exemplo, para executar um trecho de código após 1 segundo:

 Future.delayed(Duration(seconds: 1), (){ sum = a + b; // This sum will be calculated after 1 second. print(sum); });

Como a duração do atraso já é conhecida (calculada a partir das durações de animação anteriores), a animação pode ser acionada após esse intervalo.

 // Showing “Hi” after 1 second - greetings visibility trigger. _showGreetings() { Future.delayed(Duration(seconds: 1), () { setState(() { showGreetings = true; }); }); } // Increasing the padding for name label widget after 1 second - increase padding trigger. _increaseLeftPadding() { Future.delayed(Duration(seconds: 1), () { setState(() { increaseLeftPadding = true; }); }); }
Animação de preenchimento do widget de nome. (Visualização grande)

Este widget possui apenas duas propriedades obrigatórias:

  • child
    O filho dentro deste widget, ao qual o preenchimento será aplicado.
  • padding
    A quantidade de espaço a ser adicionada.

AnimatedSize

Hoje, qualquer aplicativo que tenha algum tipo de animação incluirá o zoom nos componentes visuais para chamar a atenção do usuário (comumente chamado de animação de escala). Por que não usar a mesma técnica aqui? Podemos mostrar ao usuário uma citação motivacional que se aproxima do centro da tela. Deixe-me apresentá-lo ao widget AnimatedSize , que habilita os efeitos de aumentar e diminuir o zoom, controlados alterando o tamanho de seu filho.

Este widget é um pouco diferente dos outros quando se trata dos parâmetros necessários. Precisamos do que Flutter chama de “ticker”. O Flutter tem um método para informar os objetos sempre que um novo evento de quadro é acionado. Pode ser pensado como algo que envia um sinal dizendo: “Faça agora! … Faça isso agora! … Faça isso agora! …”

O widget AnimatedSize requer uma propriedade — vsync — que aceita um provedor de ticker. A maneira mais fácil de obter um provedor de ticker é adicionar um Mixin à classe. Existem duas implementações básicas de provedor de ticker: SingleTickerProviderStateMixin , que fornece um único ticker; e TickerProviderStateMixin , que fornece vários.

A implementação padrão de um Ticker é usada para marcar os quadros de uma animação. Neste caso, o último é empregado. Mais sobre mixins.

 // Helper method to create quotes card widget. _getQuoteCardWidget() { return Card( color: Colors.green, elevation: 8.0, child: _getAnimatedSizeWidget(), ); } // Helper method to create animated size widget and set its properties. _getAnimatedSizeWidget() { return AnimatedSize( duration: Duration(seconds: 1), curve: Curves.easeInOut, vsync: this, child: _getQuoteContainer(), ); } // Helper method to create the quotes container widget with different sizes. _getQuoteContainer() { return Container( height: showQuoteCard ? 100 : 0, width: showQuoteCard ? screenWidth - 32 : 0, child: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text(quote, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w400, fontSize: 14),), ), ), ); } // Trigger used to show the quote card widget. _showQuote() { Future.delayed(Duration(seconds: 2), () { setState(() { showQuoteCard = true; }); }); }
Animação de dimensionamento do widget de cotações. (Visualização grande)

Propriedades obrigatórias para este widget:

  • vsync
    O provedor de ticker necessário para coordenar as alterações de animação e quadro.<
  • child
    A criança cujas mudanças de tamanho serão animadas.

A animação de zoom in e zoom out agora é facilmente domada.

AnimatedPositioned

Excelente! As aspas se aproximam do centro para chamar a atenção do usuário. E se ele deslizasse de baixo para cima enquanto aumentava o zoom? Vamos tentar. Esse movimento envolve brincar com a posição do widget de cotação e animar as alterações nas propriedades de posição. AnimatedPositioned é o candidato perfeito.

Este widget faz a transição automática da posição da criança durante um determinado período sempre que a posição especificada for alterada. Um ponto a ser observado: ele funciona apenas se seu widget pai for uma “Pilha”. Este widget é bastante simples e direto de usar. Vamos ver.

 // Helper method to create the animated positioned widget. // With position changes based on “showQuoteCard” flag. _getAnimatedPositionWidget() { return AnimatedPositioned( duration: Duration(seconds: 1), curve: Curves.easeInOut, child: _getQuoteCardWidget(), top: showQuoteCard ? screenHeight/2 - 100 : screenHeight, left: !showQuoteCard ? screenWidth/2 : 12, ); }
Posição com animação de dimensionamento de cotações. (Visualização grande)

Este widget possui apenas uma propriedade obrigatória:

  • child
    O widget cuja posição será alterada.

Se não se espera que o tamanho do filho mude junto com sua posição, uma alternativa mais performática para esse widget seria SlideTransition .

Aqui está nossa animação completa:

Todos os widgets animados juntos. (Visualização grande)

Conclusão

As animações são parte integrante da experiência do usuário. Aplicativos estáticos ou com animação irregular não apenas reduzem a retenção de usuários, mas também a reputação de um desenvolvedor para fornecer resultados.

Hoje, os aplicativos mais populares têm algum tipo de animação sutil para encantar os usuários. O feedback animado para solicitações de usuários também pode envolvê-los para explorar mais. O Flutter oferece muitos recursos para desenvolvimento multiplataforma, incluindo suporte avançado para animações suaves e responsivas.

O Flutter tem um ótimo suporte a plug-ins que nos permite usar animações de outros desenvolvedores. Agora que amadureceu para a versão 1.9, com tanto amor da comunidade, o Flutter deve melhorar no futuro. Eu diria que agora é um ótimo momento para aprender Flutter!

Recursos adicionais

  • “Widgets de animação e movimento”, Flutter Docs
  • “Introdução às animações”, Flutter Docs
  • Codelabs Flutter

Nota do Editor : Um enorme obrigado a Ahmad Awais por sua ajuda na revisão deste artigo.