Utilizando o CustomPaint para criar assinatura.
Tags: flutter, dart, custompaint
Published: 2024-01-21 23:32:51

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.

![New project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abat225sbu4vrlpb3xh5.png)

Vamos selecionar o opção Empty Application.

![Empty Application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kymh8fm0o9riuv16rbm9.png)

Selecione a pasta onde irá salvar o projeto.

![Folder project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e71l8wx01scydzarb3pm.png)

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.

![Project name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wfyxu23x53rl7qk2v8d7.png)

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.

![Creating home_page.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cl9bmfed7qd8acdjhi11.png)

No arquivo `main.dart` vamos definir a rota inicial da nossa aplicação como `'/'` e apontar ela para a nossa `home_page.dart`.

```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".

```dart
  @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`.

![Creating signature_page.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/opqgz5b8sk4ouzk5li1o.png)

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`. 

![Creating draw_line.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eps3493m9dw1wyjkaeii.png)

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.

```dart
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.

```dart
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.

```dart
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. 

```dart
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`.

![Example lines](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c2nd2infzoa9nzult8xe.png)

![Example points](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7lc7mnhz2zpa31c00k8j.png)

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.

```dart
  @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()`.

```dart
  @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`. 

```dart
  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`.

```dart
  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.

```dart
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**_ 

![Example drawline 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5tmqy9rmpep61czsx94.png)

![Example drawline 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bncrip7hmpcgin8aph6n.png)

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.

```dart
  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),
        ),
      ),
    );
  }
```

```dart
  @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`

```dart
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => const HomePage(),
        '/signature': (context) => const SignaturePage(),
      },
    );
  }
```

```dart
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.

![Signature gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2j1xecl75w6vgodiq6t0.gif)


### 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.

```dart
SystemChrome.setPreferredOrientations([
  DeviceOrientation.landscapeLeft,
]);
```

```dart
  @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.

```dart
  @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`.


```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.

![Landscape orientation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nogpm1zrbi7gu60ahibt.png)

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`.

```dart
  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`

```dart
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);
    }
  }
}
```

```dart
  @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`.

```dart
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?;

                  if (result != null) {
                    setState(() {
                      _imageMemory = result;
                      _showSignature = _imageMemory != null;
                    });
                  }

                  setState(() {
                    SystemChrome.setPreferredOrientations([
                      DeviceOrientation.portraitUp,
                    ]);
                  });
                },
                child: const Text('Criar assinatura'),
              ),
            ),
            Visibility(
              visible: _showSignature,
              child: _showSignature
                  ? InteractiveViewer(child: Image.memory(_imageMemory!))
                  : const SizedBox.shrink(),
            ),
          ],
        ),
      ),
    );
  }
}
```

## Esse é o resultado final!!!!

![Final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cl1k5aw7fmaicg4hwmh4.gif)

Referência:
https://www.kodeco.com/25237210-building-a-drawing-app-in-flutter.

Repositório GitHub:
https://github.com/luisgustavoo/drawing