Hoje vamos aprender como utilizar o CustomPaint
para criar uma estrutura de assinatura de nome.
Isso pode ser muito útil em aplicativos que precisam solicitar assinaturas para usuários para se assegurar de algo, por exemplo uma aplicativo de folha de ponto.
Quem trabalha no modelo CLT sabe que todo final de mês precisamos assinar nossa folha de ponto para confirmar que estamos de acordo com os horários que trabalhamos, pois com base nesses horários é que vamos receber nosso salário.
Criando o nosso projeto.
Vou utilizar o VSCode, mas você pode utilizar a IDE/Editor de sua preferência. Abra o VSCode e criei um novo projeto.
Vamos selecionar o opção Empty Application.
Selecione a pasta onde irá salvar o projeto.
E agora precisamos dar um nome a esse projeto, que no meu caso eu coloquei signature
, mas você pode ficar a vontade pra colocar o nome que quiser.
Como o projeto criado vamos dar inicio ao desenvolvimento do nosso app.
Criando a nossa Pagina Inicial
Dentro da past lib
do nosso projeto vamos criar mais uma pasta de dar o nome de pages
, e dentro dessa pasta vamos criar um arquivo chamado home_page.dart
. Essa será a pagina inicial do nosso aplicativo.
No arquivo main.dart
vamos definir a rota inicial da nossa aplicação como '/'
e apontar ela para a nossa home_page.dart
.
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
},
);
}
Voltando ao arquivo home_page.dart
vamos adicionar um botão na tela de dar no nome dele de "Criar assinatura".
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: ElevatedButton(
onPressed: () {},
child: const Text('Criar assinatura'),
),
),
],
),
),
);
}
Criando Página de Assinatura
O próximo passo será criar o arquivo signature_page.dart
dentro da pasta pages
.
Antes de dar inicio a construção da tela onde iremos fazer a nossa assinatura, primeiro vamos criar um classe de modelo que vai representar a nossa linha.
Criando nossa classe de modelo DrawLine
Dentro da pasta lib
crie uma nova pasta e dê o nome de models
, depois crie um arquivo como o nome de draw_line.dart
.
No arquivo draw_line.dart
vamos criar uma classe como mesmo nome, e dentro dessa classe vamos receber o parâmetro List<Offset> points
que irá representar os pontos(x,y) que nossa linha vai percorrer.
class DrawLine {
DrawLine({
required this.points,
});
final List<Offset> points;
}
Agora vamos voltar no nosso arquivo signature_page.dart
de vamos criar um StatefulWidget
, que a principio ficará conforme exemplo abaixo.
class SignaturePage extends StatefulWidget {
const SignaturePage({super.key});
@override
State<SignaturePage> createState() => _SignaturePageState();
}
class _SignaturePageState extends State<SignaturePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(''),
),
body: Container(),
);
}
}
Abaixo da classe _SignaturePageState
vamos criar a nossa classe que irá ser responsável por desenhar nossas linhas.
Criando classe para desenhar as linhas
Vamos dar o nome de _Signature
e vamos estender a classe CustomPainter
. Nessa classe vamos implementar os métodos shouldRepaint
e paint
.
No método shouldRepaint vamos retornar true
. O método shouldRepaint
é chamado quando uma nova instância da classe é fornecida, para verificar se a nova instância realmente representa informações diferentes.
O método paint
é chamado sempre que o objeto personalizado precisa ser repintado e é nesse método que vamos fazer o nosso desenho.
class _Signature extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
No método paint
vamos criar uma instância da da classe Paint()
e definir as propriedades color
para Colors.black
e o strokeWidth
igual a 2 para definir a cor da linha e a largura do traçado.
class _Signature extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = 2;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Desenhando as linhas
Agora precisamos definir como nossas linhas serão desenhadas. A estratégia que iremos utilizar é a seguinte.
Vamos ter uma lista de linhas [linha0, linha1, linha2...] e cada linha será representada pela classe DrawLine
e cada linha terá o sua lista de pontos [ponto0, ponto1, ponto2...] que será representado pelo parâmetro Offset
da classe DrawLine
.
Para fazer a assinatura vamos utilizar o widget GestureDetector
, esse widget nos permite fazer captura gestos na na tela do celular, como pro exemplo o gesto de arrastar os dedos sobre a tela.
Vamos adicionar esse widget no nosso arquivo signature_page.dart
, envolver ele em outro widget chamado Stack
com um alinhamento central alignment: Alignment.center
, e implementar as funções dos parâmetros onPanStart
e onPanUpdate
.
onPanStart: Quando o dedo entrou em contato com a tela e começou a se mover.
onPanUpdate: Quando o dedo está em contato com a tela e em movimento.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Assinatura'),
),
body: Stack(
alignment: Alignment.center,
children: [
GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
),
],
),
);
}
void _onPanStart(DragStartDetails details) {}
void _onPanUpdate(DragUpdateDetails details) {}
No método _onPanStart
ou seja, quando o usuário tocar na tela e começar a movimentar o dedo, vamos criar uma nova linha.
Para isso, vamos criar uma variável que vai representar a nossa lista de linhas late List<DrawLine> _lines
e inicializar ela no método initState()
.
@override
void initState() {
super.initState();
_lines = [];
}
Pra deixar nosso código mais organizado, vamos criar um método pra isso, void _createLine(List<Offset> point)
e receber nossa lista de inicial de pontos.
Agora é só chamar essa função no método _onPanStart
e envolver ele em método chamado setState
que será o responsável em atualizar a nossa tela.
O parâmetro DragStartDetails details
nos dá a posição atual do eixo X e Y Offset(x,y)
onde o usuário tocou na tela e começou a movimentar os dedos através da variável localPosition
.
void _createLine(List<Offset> point) {
_lines.add(DrawLine(points: point));
}
void _onPanStart(DragStartDetails details) {
setState(() {
_createLine([details.localPosition]);
});
}
No método _onPanUpdate
vamos pegar a última linha adicionada na nossa lista de linhas e atualizar os pontos dela.
Vamos criar um método separado pra isso também void _updateLine(Offset point)
e chamar esse método no método _onPanUpdate
envolvendo ele em um setState
pra atualizar a nossa tela, igual fizemos no _onPanStart
.
void _updateLine(Offset point) {
_lines.last.points.add(point);
}
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
_updateLine(details.localPosition);
});
}
Agora vamos voltar para a nossa classe _Signature
onde vamos implementar a lógica para desenhar as nossas linhas.
No método paint
vamos percorrer as nossas linhas e pintar os nossos pontos com o método void drawLine(Offset p1, Offset p2, Paint paint,)
.
Esse método recebe 3 parâmetros, o p1
é o ponto inicial e o p2
o ponto final da linha, o parâmetro paint
é as características de como essa linha vai ser desenhada.
for (final line in lines) {
for (var i = 0; i < line.points.length - 1; i++) {
final currentPoint = line.points[i];
if (i > 0) {
final previousPoint = line.points[i - 1];
canvas.drawLine(previousPoint, currentPoint, paint);
}
}
}
Vamos entender o que foi feito aqui.
No primeiro for (final line in lines)
vamos percorrer as linhas que criamos e no segundo vamos percorrer os pontos nos eixos x e y Offset(x,y)
de cada linha for (var i = 0; i < line.points.length - 1; i++)
.
No método drawLine(previousPoint, currentPoint, paint);
vamos passar o ponto anterior e o ponto atual para que a linha seja desenhada. O parâmetro paint
vamos passar o parâmetro já foi definido conforme mostramos na sessão anterior Criando classe para desenhar as linhas
O verificação do if(i > 0)
é para garantir que vamos começar a desenhar a linha a partir do segundo ponto, pois precisamos saber qual é o ponto anterior para desenhar a linha.
Para que tudo funcione, agora precisamos adicionar o nosso widget CustomPaint
no método build
.
Para deixar nosso código mais organizado vamos extrair o GestureDetector
para um novo método.
GestureDetector _buildCurrentPath() {
return GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
child: Container(
color: Colors.transparent,
width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height,
child: CustomPaint(
painter: _Signature(_lines),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Assinatura'),
),
body: Stack(
alignment: Alignment.center,
children: [
_buildCurrentPath(),
],
),
);
}
Agora precisamos voltar ao arquivo main.dart
e implementar a rota para a tela de SignaturePage
e chamar essa rota no método onPressed
do ElevatedButton
no arquivo home_page.dart
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/signature': (context) => const SignaturePage(),
},
);
}
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/signature');
},
child: const Text('Criar assinatura'),
)
Se tudo estiver OK você já deve estar conseguindo fazer as linhas conforme exemplo abaixo.
Incrementando o nosso projeto
Agora vamos aprimorar nossa implementação salvar a assinatura como uma imagem PNG.
Primeiro, no arquivo signature_page.dart
vamos colocar a nossa tela no modo paisagem para facilitar a assinatura. No método initState()
vamos alterar a orientação do dispositivo.
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
]);
@override
void initState() {
super.initState();
_lines = [];
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
]);
}
Proximo passo é criar uma pasta com o nome de widgets
e dentro dela criar o arquivo horizontal_line.dart
e na sequência criar uma classe StatelessWidget
com o nome de HorizontalLine
e implementar o seguinte código abaixo.
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 100,
child: Container(
width: MediaQuery.sizeOf(context).width * 0.6,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(),
),
),
),
);
}
Agora vamos adicionar esse widget uma linha abaixo do nosso método _buildCurrentPath()
no arquivo signature_page.dart
.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Assinatura'),
),
body: Stack(
alignment: Alignment.center,
children: [
_buildCurrentPath(),
const HorizontalLine(),
],
),
);
}
Se tudo estiver OK, você estará vendo a seguinte tela.
Para conseguirmos localizar o widget que vamos transformar em uma image PNG, precisamos criar uma GlobalKey late GlobalKey _renderObjectKey
iniciar ela no método initState()
e adicionar um novo widget RepaintBoundary
sobre o nosso Container
.
GestureDetector _buildCurrentPath() {
return GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
child: RepaintBoundary(
key: _renderObjectKey,
child: Container(
color: Colors.transparent,
width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height,
child: CustomPaint(
painter: _Signature(_lines),
),
),
),
);
}
Vamos criar um método _saveSignature()
para salvar a nossa assinatura e adicionar esse método dentro do onPressed
de um FloatingActionButton
.
Assim que salvarmos a assinatura, vamos fechar a tela de retornar o resultado para a tela de HomePage
Future<void> _saveSignature() async {
final nav = Navigator.of(context);
final boundary = _renderObjectKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary != null) {
final image = await boundary.toImage(
pixelRatio: 3,
);
final byteData = await image.toByteData(format: ImageByteFormat.png);
if (byteData != null) {
final pngBytes = byteData.buffer.asUint8List();
final bs64 = base64Encode(pngBytes);
debugPrint(bs64.length.toString());
nav.pop(pngBytes);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Assinatura'),
),
body: Stack(
alignment: Alignment.center,
children: [
_buildCurrentPath(),
const HorizontalLine(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _saveSignature,
child: const Icon(
Icons.save,
),
),
);
}
Agora precisamos recuperar essa assinatura que estamos retornando da tela de SignaturePage
, para isso, vamos precisar fazer algumas mudanças nessa tela HomePage
.
Primeiro vamos criar 2 variáveis, Uint8List? _imageMemory
e bool _showSignature = false
_imageMemory: Onde vamos armazenar a imagem da assinatura.
_showSignature: Para determinar se mostramos a assinatura ou não.
Segundo passo é envolver o no botão "Criar assinatura" em outros 2 widgtes, uma Column
com alinhamento central mainAxisAlignment: MainAxisAlignment.center
, e um SingleChildScrollView
.
Terceiro passo é adicionar um widget Visibility
juntamente com um InteractiveViewer
para fazermos o controle se mostramos a assinatura ou não. O InteractiveViewer
permite você dar um zoom na assinatura para ampliar ela.
O quarto e último passo é receber o retorno da tela SignaturePage
e atualizar a HomePage
para mostrar a assinatura. Nesse quarto passo vamos também altera a orientação da nossa tela para DeviceOrientation.portraitUp
para voltar para o modo retrato.
Segue código atualizado da HomePage
.
class _HomePageState extends State<HomePage> {
Uint8List? _imageMemory;
bool _showSignature = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: ElevatedButton(
onPressed: () async {
final result =
await Navigator.pushNamed(context, '/signature')
as Uint8List?;
<span class="k">if</span> <span class="p">(</span><span class="n">result</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setState</span><span class="p">(()</span> <span class="p">{</span>
<span class="n">_imageMemory</span> <span class="o">=</span> <span class="n">result</span><span class="p">;</span>
<span class="n">_showSignature</span> <span class="o">=</span> <span class="n">_imageMemory</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="n">setState</span><span class="p">(()</span> <span class="p">{</span>
<span class="n">SystemChrome</span><span class="o">.</span><span class="na">setPreferredOrientations</span><span class="p">([</span>
<span class="n">DeviceOrientation</span><span class="o">.</span><span class="na">portraitUp</span><span class="p">,</span>
<span class="p">]);</span>
<span class="p">});</span>
<span class="p">},</span>
<span class="nl">child:</span> <span class="kd">const</span> <span class="n">Text</span><span class="p">(</span><span class="s">'Criar assinatura'</span><span class="p">),</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="n">Visibility</span><span class="p">(</span>
<span class="nl">visible:</span> <span class="n">_showSignature</span><span class="p">,</span>
<span class="nl">child:</span> <span class="n">_showSignature</span>
<span class="o">?</span> <span class="n">InteractiveViewer</span><span class="p">(</span><span class="nl">child:</span> <span class="n">Image</span><span class="o">.</span><span class="na">memory</span><span class="p">(</span><span class="n">_imageMemory</span><span class="o">!</span><span class="p">))</span>
<span class="o">:</span> <span class="kd">const</span> <span class="n">SizedBox</span><span class="o">.</span><span class="na">shrink</span><span class="p">(),</span>
<span class="p">),</span>
<span class="p">],</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="p">);</span>
}
}
Esse é o resultado final!!!!
Referência:
https://www.kodeco.com/25237210-building-a-drawing-app-in-flutter.
Repositório GitHub:
https://github.com/luisgustavoo/drawing
Top comments (0)