DEV Community

Luís Gustavo
Luís Gustavo

Posted on

Utilizando o CustomPaint para criar assinatura.

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

Vamos selecionar o opção Empty Application.

Empty Application

Selecione a pasta onde irá salvar o projeto.

Folder project

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

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

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(),
      },
    );
  }
Enter fullscreen mode Exit fullscreen mode

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'),
              ),
            ),

          ],
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Criando Página de Assinatura

O próximo passo será criar o arquivo signature_page.dart dentro da pasta pages.

Creating signature_page.dart

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Example points

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) {}
Enter fullscreen mode Exit fullscreen mode

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 = [];
  } 
Enter fullscreen mode Exit fullscreen mode

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]);
    });
  }
Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Example drawline 2

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),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Assinatura'),
      ),
      body: Stack(
        alignment: Alignment.center,
        children: [
          _buildCurrentPath(),
        ],
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

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(),
      },
    );
  }
Enter fullscreen mode Exit fullscreen mode
ElevatedButton(
  onPressed: ()  {
    Navigator.pushNamed(context, '/signature');
  },
  child: const Text('Criar assinatura'),
)
Enter fullscreen mode Exit fullscreen mode

Se tudo estiver OK você já deve estar conseguindo fazer as linhas conforme exemplo abaixo.

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

SystemChrome.setPreferredOrientations([
  DeviceOrientation.landscapeLeft,
]);
Enter fullscreen mode Exit fullscreen mode
  @override
  void initState() {
    super.initState();
    _lines = [];

    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeLeft,
    ]);
  }
Enter fullscreen mode Exit fullscreen mode

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(),
          ),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

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(),
        ],
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Se tudo estiver OK, você estará vendo a seguinte tela.

Landscape orientation

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),
          ),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  @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,
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

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

                  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(),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Esse é o resultado final!!!!

Final result

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

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

Top comments (0)