COAUTOR DAWID SILVA

Criando animação de CircularProgressIndicator com gradiente no Flutter

Gabriel Abreu
Kobe
Published in
7 min readDec 14, 2022

--

Atualmente está cada vez mais comum o uso de animações nos aplicativos móveis, por isso, temos várias opções diferentes de construir uma animação em Flutter.

Este artigo aborda uma solução para indicador de carregamento (progress/loading indicator) com gradiente de cores animado diretamente no Flutter, bem como sua implementação e algumas soluções alternativas.

O problema

Na nossa aplicação precisamos de um indicador de carregamento circular que tenha mais de uma cor, ou seja, no mínimo duas cores diferentes presentes no aro de carregamento. Entretanto, nossa aplicação é whitelabel e, portanto, essas cores são dinâmicas e vem de um arquivo de configuração .properties.

PRIMARY_COLOR=0xFF006191
PRIMARY_LIGHT=0xFF008EC5
PRIMARY_DARK=0xFF0C3F6C
SECONDARY=0xFF77B931
SECONDARY_DARK=0xFF529736
SECONDARY_LIGHT=0xFF93D34E

Os valores desse arquivo vão ser usados como variáveis de ambiente usando o dart-define e recuperados no Flutter usando o método fromEnvironment.

Soluções alternativas

Desse modo, a primeira solução tentada foi construir uma animação em um software separado, utilizamos o Adobe After Effects e importá-la no Flutter utilizando a biblioteca Lottie.

Indicador de carregamento usando uma animação do Adobe After Effects

Com essa solução temos muita liberdade de customização, portanto conseguimos criar o círculo de carregamento com gradiente de cores conforme o esperado.

Ao importar uma animação para o projeto, precisamos gerar um arquivo json para a animação. Como precisamos de cores definidas dinamicamente, a única maneira de utilizar animações prontas seria criar uma animação diferente para cada possibilidade de cores que possam aparecer no aplicativo, após isso devemos adicionar cada um desses arquivos nas configurações do projeto.

Portanto, essa solução se mostrou bastante custosa e adicionaria uma complexidade grande nos scripts de build do projeto, dificultando também a manutenção do código no futuro.

Outra solução testada foi o uso do widget CircularProgressIndicator. Esse widget é muito usado e atende a necessidade de cores dinâmicas. O grande problema ao usar CircularProgressIndicator é a apresentação do componente, por não trabalhar com gradiente de cores, não fica com o efeito visual esperado.

Indicador de carregamento usando CircularProgressIndicator

Solução proposta

Como já devem saber, utilizamos Flutter neste projeto, a praticidade trazida para o desenvolvimento torna a curva de aprendizado bastante satisfatória, tanto pelas opções de build (Android, iOS, entre outras plataformas) quanto pelo código. Por conta dessa facilidade, optamos por construir o componente utilizando somente o Flutter. Existem bibliotecas que oferecem o recurso desejado para o que construímos, contudo não ficaria tão dinâmico como gostaríamos.

Em resumo, a solução que buscamos deve:

  • Nos permitir ter um maior controle da animação de carregamento, uso de gradiente e múltiplas cores, tipo e velocidade da animação
  • Suporte ao uso de cores dinamicamente
  • Ter uma boa performance

Dito isto, não teria biblioteca que atendesse todos os requisitos do nosso componente de carregamento.

CustomPaint & CustomPainter

Nativamente no Flutter, existe um Listenable chamado CustomPaint. Ele é o nosso provedor de canvas, então precisamos entender o que seria este canvas: de maneira geral e objetiva, se trata de um componente visual preparado para receber possíveis formas e desenhos customizados. No CustomPaint existem várias propriedades, mas focaremos em um específico, o painter, que irá receber um CustomPainter.
Fazendo uma analogia com a realidade técnica, quando um designer inicia um novo projeto tudo que se tem a frente é uma tela limpa, um canvas. Para melhor entendimento chamaremos esse designer de pintor, então quando se tem a necessidade de criar algum produto, este pintor desenha algo nesse canvas (nessa tela limpa). E é exatamente assim que funciona o CustomPaint, é um canvas e precisa de um pintor (painter) que no caso é o CustomPainter.

Com isso, vamos ao código

Código do GradientCircularProgressPainter

Vamos iniciar construindo o desenho, para isto, precisamos seguir alguns passos:

  1. Atribuir a função de pintor, estendendo a classe GradientCircularProgressPainter de CustomPainter;
  2. Ao fazer isto, precisamos de duas funções: a paint e a shouldRepaint. Os nomes são bem intuitivos se traduzidos: pintar e deveria repintar.
    A função paint refere-se ao desenho que será mostrado em tela, um círculo, um quadrado, ou até mesmo um desenho livre.
    Já a função shouldRepaint tem relação com a necessidade de reconstruir o componente ao ser instanciado novamente. Importe as duas funções;
  3. Neste exemplo, vamos customizar apenas as cores (gradientColors), o tamanho do círculo (radius) e a largura da circunferência (strokeWidth), definimos as variáveis como atributos o desenho;
  4. Agora de fato iniciaremos o desenho.
    Criamos uma variável rect que irá receber um retângulo (Rect) que ocupará todo o espaço disponível, que pode ser o tamanho inteiro da tela ou não.
    A variável paint irá receber nosso desenho. Para isto, instanciamos um objeto Paint() e através do cascade — técnica que se resume a utilizar “..” para evitar a repetição do “paint = …” — atribuímos algumas propriedades:
    - Em style atribuímos o PaintingStyle.stroke para que o nosso círculo seja oco, nosso interesse aqui é apenas a circunferência, sem preenchimento;
    - Em strokeWidth passamos o valor da variável que criamos no começo com o mesmo nome para que seja customizável;
    - Em shader é onde recebemos as cores por variável (por estarmos construindo um componente com gradiente utilizamos o shader mesmo, porém se fosse só uma cor poderia ser utilizado o color), e montamos nosso gradiente através do objeto LinearGradient, podendo ser outros objetos que representam os gradientes (RadialGradient e SweepGradient), fica ao seu critério. No final temos que utilizar o createShader passando o rect pois isto criará as cores no componente.
  5. Por fim, vamos colocar o desenho no canvas. Precisamos passar três propriedades no drawCircle do canvas: a posição, que neste caso é o centro do desenho no espaço disponível (offset), o tamanho do círculo (radius) e o desenho (paint). Como queremos que o componente fique centralizado na tela, dividimos o valor de x e o valor de y por dois.

Com isto, temos o desenho, mas as telas do Flutter não entendem o desenho como um componente, por isso, utilizamos o CustomPaint que converte nosso desenho num widget e assim se torna possível renderizar na tela. Tendo como resultado:

Beleza, qual nosso próximo passo?! Adicionar a animação de rotação no nosso desenho! Vamos lá!

RotationTransition

Para construir uma animação do ZERO, o que podemos considerar como animação explícita, no Flutter, na maior parte das vezes, precisamos de quatro coisas, são elas:

  • Criar uma classe StatefulWidget, onde nela utilizaremos o CustomPaint;
    Obs.: Neste caso, não podemos utilizar o StatelessWidget. Quando precisamos trabalhar com animações, sendo necessário utilizar o SingleTickerProviderStateMixin, estamos informando para o Flutter que precisamos atualizar a tela para que mostre os quadros da animação, isto só é possível num componente que aceite trabalhar com estado, portanto, o StatefulWidget.
  • Importar o Mixin SingleTickerProviderStateMixin no StatefulWidget, para sinalizar para o Flutter que estaremos trabalhando com animação naquele componente;
  • Criar um AnimationController, que irá controlar a animação;
  • Criar um Animation<T> como representação matemática da animação;

Salvos os casos onde o Flutter já traz a animação pronta, as animações implícitas, como o Hero.

Animando o GradientCircularProgressPainter

Entendendo o código

O RotationTransition basicamente é um widget que nos fornece um cenário preparado para desenvolver animações de rotação baseada em turns (Nesse caso, podemos chamar de voltas).

Como estamos utilizando uma StatefulWidget, podemos utilizar as funções initState() e dispose(). Elas nos auxiliam no ciclo de vida do componente em relação ao aplicativo, evitando uso desnecessário de memória.

De antemão, declaramos as variáveis de modo que não sejam em hipótese alguma nulas, visto que o loading deve estar pronto para uso em todo o ciclo de vida da aplicação. Com isto em mente, como nosso objetivo é inicializar as variáveis dentro do initState, ao invés de declará-las desta forma:

AnimationController? _animationController;
Animation<T>? _animation;

Dando a possibilidade de serem nulas, fazemos a utilização da palavra chave late que irá disparar uma exception caso ao utilizar o componente, pelo menos uma das variáveis seja nula. Resultando neste formato:

late AnimationController _animationController;
late Animation<T> _animation;

Após isto, devemos inicializá-las dentro do initState pois assim que o componente for chamado, esta função será a primeira a ser executada. Nela, atribuiremos à _animationController uma instância de AnimationController; passando this no vsync (isto só é possível, pois estamos utilizando o mixin SingleTickerProviderStateMixin) e passando uma duration para estimar o tempo de duração da animação. Em seguida adicionamos um ouvinte executando o setState() para que a animação seja reativa e limpa a olho nu, e solicitamos que a animação atue em repetição.

Na variável da animação, atribuímos um valor oriundo de um objeto Tween que nos proporciona um valor de início e um valor de fim, como estamos pensando nas voltas que a animação irá mostrar, consideramos esses valores a porcentagem de volta já executada, ou seja, no começo ele está em 0% e assim que ele chegar em 100% é dada como uma volta completa, e assim a animação irá repetir. Obviamente Tween não é do tipo Animation, porém para nossa alegria, ele nos entrega uma função animate que irá receber o controller e converter esse Tween em animação.

No dispose iremos só destruir todo aquele componente para que não fique executando sem necessidade.

Agora que temos tudo configurado, basta passarmos a animação para a propriedade turns do widget RotationTransition e teremos nosso loading customizado!

Resultado Final

Indicador de carregamento usando Custom Painter

Esse foi o resultado final da implementação com Custom Paint, exatamente como esperado, seguindo a nossa proposta de design e com total controle das cores e da animação como um todo.

Exemplo utilizando outro conjunto de cores

A utilização do Custom Paint para implementar animações em Flutter se mostrou uma excelente solução. Além de ter uma boa performance, permite total controle das animações, servindo as mais diversas necessidades. Entretanto, é uma solução um pouco mais complexa para implementar, exigindo a criação de duas novas classes e o domínio de alguns componentes como CustomPainter, RotationTransition e AnimationController. O uso da classe CustomPainter também exige o uso de conhecimentos básicos em matemática e geometria caso a animação esperada seja mais complexa.

--

--