Criar um widget de aplicativo Custom Paint Flutter
Quer criar uma interface de usuário altamente personalizada com uma animação incrível? Não tenha medo, o Flutter CustomPaint Widget tem tudo o que você precisa
Neste artigo, abordaremos tanto a animação explícita quanto a forma de trabalhar com o widget CustomPaint
em um flutter.
Custom Paint: Um widget que fornece uma tela na qual desenhar durante a fase de pintura.
Basta adicionar o widget CustomPaint
à sua árvore de widgets e fornecer o painter a ele, que é uma subclasse da classe abstrata CustomPainter
.
Como o CustomPainter
é uma classe abstrata, ele nos obriga a implementar dois métodos importantes, ou seja, paint()
e shouldRepaint()
. O tamanho especifica o size
do widget desenhado. Vamos colocar a mão na massa:
class RingPainter extends StatelessWidget { const RingPainter({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: CustomPaint( size: Size(200.0, 200.0), painter: MyPainter(), ), ); } } class MyPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // TODO: implement paint } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { // TODO: implement shouldRepaint throw UnimplementedError(); } }
O método paint()
nos fornece dois parâmetros importantes:
Canvas
: Canvas é a área na tela onde desenhamos nosso widget. Ele tem vários métodos para desenhar widgets personalizados. Alguns deles são:canvas.drawLine()
,canvas.drawCircle()
,canvas.drawArc()
,canvas.drawOval()
etc.Size
: Qual deve ser o tamanho do desenho. Por padrão, ele assume o tamanho do widget wrapper, ou seja, o widget que envolve nosso widgetCustomPaint
. Além disso, podemos fornecer parâmetros de tamanho dentro do nosso widgetCustomPaint
. Como queremos desenhar um círculo, passamos o mesmo valor para a largura e a altura.
O método shouldRepaint()
é chamado sempre que o CustomPainter
precisa ser reconstruído. Isso ficará claro quando implementarmos nosso projeto.
Agora que entendemos a classe CustomPainter
, vamos desenhar algo na tela. Nosso objetivo é desenhar um círculo com alguma largura de traço.
Vamos ver como fazer isso:
@override void paint(Canvas canvas, Size size) { final strokeWidth = size.width / 15.0; final circleCenter = Offset(size.width / 2, size.height / 2); final circleRadius = (size.width - strokeWidth) / 2; final paint = Paint() ..color = Colors.grey ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawCircle(circleCenter, circleRadius, paint); }
Como queremos desenhar um círculo, usamos o método pré-construído canvas.drawCircle()
, que espera três parâmetros, ou seja, o centro do círculo, o raio do círculo e um objeto de pintura. Calculamos o centro e o raio do círculo a partir do parâmetro size
. A classe Paint()
nos fornece várias propriedades, como a cor da paint
, storkeWidth
, style
da tinta, ou seja, se ela deve ser traçada ou preenchida. A implementação atual resultará no seguinte:
Nosso próximo objetivo é desenhar um arco sobre o mesmo círculo que seria animado em torno do traço. Para conseguir isso:
@override void paint(Canvas canvas, Size size) { final strokeWidth = size.width / 15.0; final circleCenter = Offset(size.width / 2, size.height / 2); final circleRadius = (size.width - strokeWidth) / 2; final paint = Paint() ..color = Colors.grey ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawCircle(circleCenter, circleRadius, paint); final arcPaint = Paint() ..color = Colors.green ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawArc(Rect.fromCircle(center: circleCenter, radius: circleRadius), -pi / 2, 2 * pi * 0.4, false, arcPaint); }
Como queremos desenhar um arco, usamos o método canvas.drawArc()
que espera cinco parâmetros, ou seja, o objeto Rect
, startAngle
em radianos, sweepAngle em radian
, useCenter
boolean e o objeto paint.
Além disso, queremos desenhar um arco ao redor do círculo, por isso usamos o método Rect.fromCircle() como primeiro parâmetro. O segundo parâmetro é o ângulo inicial que aplicamos a -pi/2, pois queremos iniciar o ângulo a partir do topo do círculo.
O ângulo de varredura especifica onde o arco deve terminar e aplicamos 40% do ângulo total, ou seja, nosso arco será desenhado de -pi/2 a 40% do total de 360 radianos.
Se o booleano useCenter
for verdadeiro, o arco será fechado de volta ao centro, formando um setor de círculo. Caso contrário, o arco não será fechado, formando um segmento de círculo. E o argumento final é o nosso objeto de pintura, com o qual já estamos familiarizados.
A implementação atual com useCenter
definido como true resultará no seguinte:
Mas não queremos esse setor circular, portanto, defini-la como falsa resultaria no seguinte:
Você pode ajustar o startAngle
e o endAngle
para ver os vários efeitos.
Agora estamos quase perto do que queremos alcançar. A única parte que falta é animar o progresso. Para isso, temos de usar a animação explícita, ou seja, temos de criar o AnimationController
e passar seu valor para a classe RingPainter
:
import 'dart:math'; import 'package:flutter/material.dart'; class RingPainter extends StatefulWidget { const RingPainter({Key? key}) : super(key: key); @override State<RingPainter> createState() => _RingPainterState(); } class _RingPainterState extends State<RingPainter> with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); //create animation controller _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); //start the animation _controller.forward(); //add a callback to obeserve various state during animation _controller.addListener(() { setState(() {}); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Center( child: CustomPaint( size: const Size(200.0, 200.0), painter: MyPainter( progress: _controller.value, defaultColor: Colors.grey, fillColor: Colors.green, ), ), ); } } class MyPainter extends CustomPainter { final double progress; final Color defaultColor; final Color fillColor; MyPainter({ required this.progress, required this.defaultColor, required this.fillColor, }); @override void paint(Canvas canvas, Size size) { final strokeWidth = size.width / 15.0; final circleCenter = Offset(size.width / 2, size.height / 2); final circleRadius = (size.width - strokeWidth) / 2; final paint = Paint() ..color = defaultColor ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawCircle(circleCenter, circleRadius, paint); final arcPaint = Paint() ..color = fillColor ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawArc(Rect.fromCircle(center: circleCenter, radius: circleRadius), -pi / 2, 2 * pi * progress, false, arcPaint); } @override bool shouldRepaint(covariant MyPainter oldDelegate) => oldDelegate.progress != progress; }
As várias coisas que fizemos agora são:
- Convertemos nosso
RingPainter
StatelessWidget
em Stateful widget para reconstruir a interface do usuário sempre que o valor da animação for alterado. - Criamos o
AnimationController
_controller misturando nossa classe _RingPainterState com SingleTickerProviderStateMixin e o inicializamos no métodoinitState()
. - Iniciamos a animação com
_controller.forward()
- Adicionamos um listener ao controlador e um
setState((){})
é chamado para que toda a nossa interface do usuário seja reconstruída sempre que o valor do nosso_controller
for alterado. - Agora nossa classe
MyPainter
espera três argumentos obrigatórios:progress
,defaultColor
do traço do círculo e cor de preenchimento. Isso é feito para passar os vários valores de fora dessa classe e para tornar esse componente reutilizável. Observe que o métodoshouldRepaint()
foi atualizado de forma que nossocircle
só deve ser desenhado quando o valor do progresso for diferente do valor anterior. - Finalmente, passamos esses três argumentos para a classe
MyPainter
, sendo o valor de progresso o valor atual da animação.
Com a implementação atual, estamos conseguindo atingir esse objetivo com sucesso:
Uau, estamos quase no fim! A única coisa que falta é adicionar um ícone no meio do círculo e mudar sua cor, dependendo se a animação foi concluída ou não.
Para conseguir isso, atualize seu método de compilação da seguinte forma:
@override Widget build(BuildContext context) { return Center( child: Stack( children: [ CustomPaint( size: const Size(200.0, 200.0), painter: MyPainter( progress: _controller.value, defaultColor: Colors.grey, fillColor: Colors.green, ), ), Positioned.fill( child: Icon( Icons.check, color: _controller.isCompleted ? Colors.green : Colors.grey, size: 68.0, ), ) ], ), ); }
A implementação a seguir resultaria nisso:
Portanto, agora alcançamos com sucesso o que queríamos.
Espero que você tenha aprendido algo sobre o widget CustomPaint. Agora você pode explorar por conta própria para obter vários outros efeitos.
Obrigado pela leitura.
Todo o projeto se encontra no meu GitHub (Git) (Pasta Custom Circular)