DEV Community

Toshi Ossada for flutterbrasil

Posted on

Desenhos complexos com CustomPaint

No ultimo artigo vimos os conceitos básicos do CustomPaint e como ele pode nos habilitar a desenhar qualquer forma geométrica no Flutter, agora vamos juntar essas formas geométricas para que possamos fazer desenhos mais complexos.

Por exemplo, se juntarmos arco com linhas podemos desenhar um relógio

Primeiramente vamos desenhar o arco do relógio


import 'dart:math' as math;
import 'package:flutter/material.dart';
class Clock extends StatefulWidget {
const Clock({super.key});
@override
State<Clock> createState() => _ClockState();
}
class _ClockState extends State<Clock> {
final txtHour = TextEditingController(text: DateTime.now().hour.toString());
final txtMinutes =
TextEditingController(text: DateTime.now().minute.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clock'),
),
backgroundColor: Colors.white,
body: Column(
children: [
Center(
child: CustomPaint(
painter: _MyPainter(
hour: int.parse(txtHour.text),
minute: int.parse(txtMinutes.text)),
size: const Size(300, 300),
),
),
],
),
);
}
}
class _MyPainter extends CustomPainter {
_MyPainter();
@override
void paint(Canvas canvas, Size size) {
final arcPaint = Paint()
..color = Colors.black45.withOpacity(.4)
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawArc(
Offset.zero & size,
0,
math.pi * 2,
false,
arcPaint,
);
}
@override
bool shouldRepaint(covariant _MyPainter oldDelegate) => true;
}
view raw arco_relogio hosted with ❤ by GitHub

O arco vai de 0 à 2pi para formar uma circunferência completa

Agora precisamos gerar 12 linhas que irá representar as 12 horas do nosso relógio e precisamos posiciona-los no ângulo exato de cada hora, para isso pegamos o valor de 2pi e dividimos por 12 e depois multiplicamos pela hora.


import 'dart:math' as math;
import 'package:flutter/material.dart';
class Clock extends StatefulWidget {
const Clock({super.key});
@override
State<Clock> createState() => _ClockState();
}
class _ClockState extends State<Clock> {
final txtHour = TextEditingController(text: DateTime.now().hour.toString());
final txtMinutes =
TextEditingController(text: DateTime.now().minute.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clock'),
),
backgroundColor: Colors.white,
body: Column(
children: [
Center(
child: CustomPaint(
painter: _MyPainter(
hour: int.parse(txtHour.text),
minute: int.parse(txtMinutes.text)),
size: const Size(300, 300),
),
),
],
),
);
}
}
class _MyPainter extends CustomPainter {
_MyPainter();
@override
void paint(Canvas canvas, Size size) {
final arcPaint = Paint()
..color = Colors.black45.withOpacity(.4)
..strokeWidth = 5
..style = PaintingStyle.stroke;
final tickPaint = Paint()
..color = Colors.black45
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawArc(
Offset.zero & size,
0,
math.pi * 2,
false,
arcPaint,
);
final midleSize = size.width / 2;
for (var i = 0; i < 12; i++) {
final angle = i * (math.pi * 2 / 12);
final start = Offset(
midleSize + math.cos(angle) * (size.width * 0.48),
midleSize + math.sin(angle) * (size.width * 0.48),
);
final end = Offset(
midleSize + math.cos(angle) * (size.width * 0.45),
midleSize + math.sin(angle) * (size.width * 0.45),
);
canvas.drawLine(start, end, tickPaint);
}
}
@override
bool shouldRepaint(covariant _MyPainter oldDelegate) => true;
}
view raw Untitled-1 hosted with ❤ by GitHub

Agora iremos desenhar os ponteiros do nosso relógio, para isso precisamos saber como converter horas e minutos em ângulos para isso utilizarei o https://gemini.google.com/ para me ajudar a desenvolver meu método.

E cheguei nessa função

({double angleHour, double angleMinutes}) timeToAngles({
required int hours,
required int minutes,
}) {
const baseAngle = math.pi * 2;
final minute = ((minutes / 60));
final hour = (((hours % 12) / 12) + ((1 / 12) * minute));
final angleMinutes = baseAngle * minute - math.pi / 2;
final angleHour = baseAngle * hour - math.pi / 2;
return (angleHour: angleHour, angleMinutes: angleMinutes);
}
view raw Untitled-1 hosted with ❤ by GitHub

Com a função de converssao de horas em angulo basta desenharmos duas linhas passando o sem e cos do ângulo

({double angleHour, double angleMinutes}) timeToAngles({
required int hours,
required int minutes,
}) {
const baseAngle = math.pi * 2;
final minute = ((minutes / 60));
final hour = (((hours % 12) / 12) + ((1 / 12) * minute));
final angleMinutes = baseAngle * minute - math.pi / 2;
final angleHour = baseAngle * hour - math.pi / 2;
return (angleHour: angleHour, angleMinutes: angleMinutes);
}aimport 'dart:math' as math;
import 'package:flutter/material.dart';
class Page6 extends StatefulWidget {
const Page6({super.key});
@override
State<Page6> createState() => _Page6State();
}
class _Page6State extends State<Page6> {
final txtHour = TextEditingController(text: DateTime.now().hour.toString());
final txtMinutes =
TextEditingController(text: DateTime.now().minute.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clock'),
),
backgroundColor: Colors.white,
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: txtHour,
keyboardType: TextInputType.number,
onSubmitted: (s) => setState(() {}),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Horas',
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(':'),
),
Expanded(
child: TextField(
controller: txtMinutes,
keyboardType: TextInputType.number,
onSubmitted: (s) => setState(() {}),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Minutos',
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
txtHour.text = DateTime.now().hour.toString();
txtMinutes.text = DateTime.now().minute.toString();
setState(() {});
},
child: const Text('Set Time to Now'),
),
)
],
),
),
Center(
child: CustomPaint(
painter: _MyPainter(
hour: int.parse(txtHour.text),
minute: int.parse(txtMinutes.text)),
size: const Size(300, 300),
),
),
],
),
);
}
}
class _MyPainter extends CustomPainter {
final int hour;
final int minute;
_MyPainter({
required this.hour,
required this.minute,
});
@override
void paint(Canvas canvas, Size size) {
final arcPaint = Paint()
..color = Colors.black45.withOpacity(.4)
..strokeWidth = 5
..style = PaintingStyle.stroke;
final tickPaint = Paint()
..color = Colors.black45
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawArc(
Offset.zero & size,
0,
math.pi * 2,
false,
arcPaint,
);
final midleSize = size.width / 2;
for (var i = 0; i < 12; i++) {
final angle = i * (math.pi * 2 / 12);
final start = Offset(
midleSize + math.cos(angle) * (size.width * 0.48),
midleSize + math.sin(angle) * (size.width * 0.48),
);
final end = Offset(
midleSize + math.cos(angle) * (size.width * 0.45),
midleSize + math.sin(angle) * (size.width * 0.45),
);
canvas.drawLine(start, end, tickPaint);
}
final (angles) = timeToAngles(hours: hour, minutes: minute);
final startHour = Offset(midleSize + math.cos(angles.angleHour),
midleSize + math.sin(angles.angleHour));
final endHour = Offset(
midleSize + math.cos(angles.angleHour) * (size.width * 0.2),
midleSize + math.sin(angles.angleHour) * (size.width * 0.2));
final startMinutes = Offset(midleSize + math.cos(angles.angleMinutes),
midleSize + math.sin(angles.angleMinutes));
final endMinutes = Offset(
midleSize + math.cos(angles.angleMinutes) * (size.width * .4),
midleSize + math.sin(angles.angleMinutes) * (size.width * .4));
final armHourPaint = Paint()
..color = Colors.purple
..strokeWidth = 5
..style = PaintingStyle.stroke;
final armMinPaint = Paint()
..color = Colors.red.withOpacity(0.6)
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawLine(startHour, endHour, armHourPaint);
canvas.drawLine(startMinutes, endMinutes, armMinPaint);
}
@override
bool shouldRepaint(covariant _MyPainter oldDelegate) => true;
({double angleHour, double angleMinutes}) timeToAngles({
required int hours,
required int minutes,
}) {
const baseAngle = math.pi * 2;
final minute = ((minutes / 60));
final hour = (((hours % 12) / 12) + ((1 / 12) * minute));
final angleMinutes = baseAngle * minute - math.pi / 2;
final angleHour = baseAngle * hour - math.pi / 2;
return (angleHour: angleHour, angleMinutes: angleMinutes);
}
}
view raw Untitled-1 hosted with ❤ by GitHub

E para finalizar vamos colocar um circulo no centro para representar a junção dos ponteiros

({double angleHour, double angleMinutes}) timeToAngles({
required int hours,
required int minutes,
}) {
const baseAngle = math.pi * 2;
final minute = ((minutes / 60));
final hour = (((hours % 12) / 12) + ((1 / 12) * minute));
final angleMinutes = baseAngle * minute - math.pi / 2;
final angleHour = baseAngle * hour - math.pi / 2;
return (angleHour: angleHour, angleMinutes: angleMinutes);
}aimport 'dart:math' as math;
import 'package:flutter/material.dart';
class Page6 extends StatefulWidget {
const Page6({super.key});
@override
State<Page6> createState() => _Page6State();
}
class _Page6State extends State<Page6> {
final txtHour = TextEditingController(text: DateTime.now().hour.toString());
final txtMinutes =
TextEditingController(text: DateTime.now().minute.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Clock'),
),
backgroundColor: Colors.white,
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: txtHour,
keyboardType: TextInputType.number,
onSubmitted: (s) => setState(() {}),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Horas',
),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(':'),
),
Expanded(
child: TextField(
controller: txtMinutes,
keyboardType: TextInputType.number,
onSubmitted: (s) => setState(() {}),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Minutos',
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
txtHour.text = DateTime.now().hour.toString();
txtMinutes.text = DateTime.now().minute.toString();
setState(() {});
},
child: const Text('Set Time to Now'),
),
)
],
),
),
Center(
child: CustomPaint(
painter: _MyPainter(
hour: int.parse(txtHour.text),
minute: int.parse(txtMinutes.text)),
size: const Size(300, 300),
),
),
],
),
);
}
}
class _MyPainter extends CustomPainter {
final int hour;
final int minute;
_MyPainter({
required this.hour,
required this.minute,
});
@override
void paint(Canvas canvas, Size size) {
final arcPaint = Paint()
..color = Colors.black45.withOpacity(.4)
..strokeWidth = 5
..style = PaintingStyle.stroke;
final tickPaint = Paint()
..color = Colors.black45
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawArc(
Offset.zero & size,
0,
math.pi * 2,
false,
arcPaint,
);
final midleSize = size.width / 2;
for (var i = 0; i < 12; i++) {
final angle = i * (math.pi * 2 / 12);
final start = Offset(
midleSize + math.cos(angle) * (size.width * 0.48),
midleSize + math.sin(angle) * (size.width * 0.48),
);
final end = Offset(
midleSize + math.cos(angle) * (size.width * 0.45),
midleSize + math.sin(angle) * (size.width * 0.45),
);
canvas.drawLine(start, end, tickPaint);
}
final (angles) = timeToAngles(hours: hour, minutes: minute);
final startHour = Offset(midleSize + math.cos(angles.angleHour),
midleSize + math.sin(angles.angleHour));
final endHour = Offset(
midleSize + math.cos(angles.angleHour) * (size.width * 0.2),
midleSize + math.sin(angles.angleHour) * (size.width * 0.2));
final startMinutes = Offset(midleSize + math.cos(angles.angleMinutes),
midleSize + math.sin(angles.angleMinutes));
final endMinutes = Offset(
midleSize + math.cos(angles.angleMinutes) * (size.width * .4),
midleSize + math.sin(angles.angleMinutes) * (size.width * .4));
final armHourPaint = Paint()
..color = Colors.purple
..strokeWidth = 5
..style = PaintingStyle.stroke;
final armMinPaint = Paint()
..color = Colors.red.withOpacity(0.6)
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawLine(startHour, endHour, armHourPaint);
canvas.drawLine(startMinutes, endMinutes, armMinPaint);
}
@override
bool shouldRepaint(covariant _MyPainter oldDelegate) => true;
({double angleHour, double angleMinutes}) timeToAngles({
required int hours,
required int minutes,
}) {
const baseAngle = math.pi * 2;
final minute = ((minutes / 60));
final hour = (((hours % 12) / 12) + ((1 / 12) * minute));
final angleMinutes = baseAngle * minute - math.pi / 2;
final angleHour = baseAngle * hour - math.pi / 2;
return (angleHour: angleHour, angleMinutes: angleMinutes);
}
}
view raw Untitled-1 hosted with ❤ by GitHub

E o resultado será este:

E de bônus vamos aprender a fazer aquelas mascara na câmara quando queremos enquadrada algo (como o rosto)

Primeiramente vamos instalar algum package de câmera, estou utilizando o câmera

E vamos inicializa-la em nosso widget

A construção do nosso widget será a seguinte, uma Stack onde o primeiro item de nossa Stack será a câmera e o segundo item será nosso custom paint que ficará acima da câmera.

E com isso iremos criar um Retangulo que irá cobrir nossa camera.

Agora vamos desenhar um circulo oval que será o local que iremos centralizar o rosto na câmera

O resultado será um circulo preto no meio da tela

Com o path é possível fazer algumas operações como retirar a diferença entre dois paths, com isso podemos fazer um recorte em nosso CustomPaint criando um buraco na tela.

Com o path é possível fazer algumas operações como retirar a diferença entre dois paths, com isso podemos fazer um recorte em nosso CustomPaint criando um buraco na tela.

E voilá temos, temos nossa mascara para o rosto.

Você pode acessar esses dois exemplo em meu github

https://github.com/toshiossada/clippath

Legal né? Lembrando que poderíamos ter usado este recurso com um Socket, como o Firebase para que a ativação e desativação seja de forma instantânea, caso seu negócio necessite

Confira o exemplo completo em

https://github.com/toshiossada/togglefeature

Entre em nosso discord para interagir com a comunidade: https://discord.com/invite/flutterbrasil

https://linktr.ee/flutterbrasil

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️