Skip to main content

Command Palette

Search for a command to run...

Learn Custom Painter in flutter | Drawing App

build complex app UI with custom painter

Updated
5 min read
Learn Custom Painter in flutter | Drawing App
S

i am a full stack developer from nepal, i use different languages and technology to build applications and. I love learning

Peek 2021-09-30 21-28.gif

Flutter has become widely popular among the developer as a reason to develop fast and beautiful application UI. sometimes you need more than that. in this article, we will see how to use a flutter custom painter to enhance your flutter development skills by building a drawing app.

if you feel lazy to type the code by own you can just copy and paste the code from the given rep

Github Repo link

What is Custom Painter

according to official documentation, the custom painter is an interface to paint something on the screen The paint method is called whenever the custom object needs to be repainted.

Start with creating a clean fresh application

$ flutter create drawing_app

once you have your project ready open to your favorite code editor or IDE & and define flutter basic structure

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: DrawingBoard(),
    );
  }
}

Define the DrawingBoard() as state full widget


class DrawingBoard extends StatefulWidget {
  @override
  _DrawingBoardState createState() => _DrawingBoardState();
}

class _DrawingBoardState extends State<DrawingBoard> {

  @override
  Widget build(BuildContext context) {

  }
}

Create a model class for Drawing Point later this will be used to draw on the screen

class DrawingPoint {
  Offset offset;
  Paint paint;

  DrawingPoint(this.offset, this.paint);
}

Define some variable in the DrawingBoard() widget to track the state

//track the selected color
 Color selectedColor = Colors.black; 

//thickness of drawing lines
 double strokeWidth = 5;   

//List of drawing point on screen
 List<DrawingPoint?> drawingPoints = []; 

//list of avilable colors
 List<Color> colors = [  
    Colors.pink,
    Colors.red,
    Colors.black,
    Colors.yellow,
  ];

once you have it ready we will start building our application

@override
  Widget build(BuildContext context) {
    return Scaffold(
     body: //TODO: 
      bottomNavigationBar: BottomAppBar(
        child: Container(
          color: Colors.grey[200],
          padding: EdgeInsets.all(10),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: List.generate(
              colors.length,
              (index) => _buildColorChose(colors[index]),
            ),
          ),
        ),
      ),
    );

let's start by building a color chose the option, that creates a bottomAppBar on the bottomNavigationBar() which is responsible to display color chooser on the screen with the help of _buildColorChose() method which takes color as only argument

_buildColorChose(Color color) {
    bool isSelected = selectedColor == color; //check if coor is selcted or not
    return GestureDetector(
      onTap: () => setState(() => selectedColor = color), //update the selectedColor when choser is taped
      child: Container(
        height: isSelected ? 47 : 40,
        width: isSelected ? 47 : 40,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle,
          border: isSelected ? Border.all(color: Colors.white, width: 3) : null,
        ),
      ),
    );
  }

image.png

once we have color choosing option let's start building our main functionality

class _DrawingPainter extends CustomPainter {
  final List<DrawingPoint> drawingPoints;

  _DrawingPainter(this.drawingPoints);

  List<Offset> offsetsList = [];

  @override
  void paint(Canvas canvas, Size size) {
    for (int i = 0; i < drawingPoints.length; i++) {
      if (drawingPoints[i] != null && drawingPoints[i + 1] != null) {
        canvas.drawLine(drawingPoints[i].offset, drawingPoints[i + 1].offset,
            drawingPoints[i].paint);
      } else if (drawingPoints[i] != null && drawingPoints[i + 1] == null) {
        offsetsList.clear();
        offsetsList.add(drawingPoints[i].offset);

        canvas.drawPoints(
            PointMode.points, offsetsList, drawingPoints[i].paint);
      }
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Create a private class you can call it whatever you want and extends with the CustomPainter() class after that, you have to override 2 methods

The paint method is called whenever the custom object needs to be repainted.

The shouldRepaint method is called when a new instance of the class is provided

and which takes a list of drawing points (List<DrawingPoint>) as an argument

and loop thorough the drawing points and check if the drawing point is not null and the next point is also not null

if (drawingPoints[i] != null && drawingPoints[i + 1] != null) {}

if true: Draw line from drawingPoints[i] to drawingPoints[i + 1]

canvas.drawLine(
drawingPoints[i].offset, 
drawingPoints[i + 1].offset,
drawingPoints[i].paint
);

drawLine() takes 3 arguments Starting line, end line, and painter

else if :

else if (drawingPoints[i] != null && drawingPoints[i + 1] == null) {}

if the current point is not null and the next point is null then clear the existing value in the offsets list (List<Offset> offsetsList = []) and draw point which will give smoothness to the lines

 else if (drawingPoints[i] != null && drawingPoints[i + 1] == null) {
        offsetsList.clear();
        offsetsList.add(drawingPoints[i].offset);

        canvas.drawPoints(
            PointMode.points, offsetsList, drawingPoints[i].paint);
}

And define the body of DrawingBoard() widget which contain the stack of gesture detector and Positioned widgets provide 3 gesture to GestureDetector()

  • onPanStart
  • onPanUpdate
  • onPanEnd

on onPanStart and onPanUpdate add the DrawingPoint() object to List<DrawingPoint>() as show in the below code on onPanEnd add null to the List<DrawingPoint>



 @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          GestureDetector(
            onPanStart: (details) {
              setState(() {
                drawingPoints.add(
                  DrawingPoint(
                    details.localPosition,
                    Paint()
                      ..color = selectedColor
                      ..isAntiAlias = true
                      ..strokeWidth = strokeWidth
                      ..strokeCap = StrokeCap.round,
                  ),
                );
              });
            },
            onPanUpdate: (details) {
              setState(() {
                drawingPoints.add(
                  DrawingPoint(
                    details.localPosition,
                    Paint()
                      ..color = selectedColor
                      ..isAntiAlias = true
                      ..strokeWidth = strokeWidth
                      ..strokeCap = StrokeCap.round,
                  ),
                );
              });
            },
            onPanEnd: (details) {
              setState(() {
                drawingPoints.add(null);
              });
            },
            child: CustomPaint(
              painter: _DrawingPainter(drawingPoints),
              child: Container(
                height: MediaQuery.of(context).size.height,
                width: MediaQuery.of(context).size.width,
              ),
            ),
          ),
          Positioned(
            top: 40,
            right: 30,
            child: Row(
              children: [
                Slider(
                  min: 0,
                  max: 40,
                  value: strokeWidth,
                  onChanged: (val) => setState(() => strokeWidth = val),
                ),
                ElevatedButton.icon(
                  onPressed: () => setState(() => drawingPoints = []),
                  icon: Icon(Icons.clear),
                  label: Text("Clear Board"),
                )
              ],
            ),
          ),
        ],
      ),
      bottomNavigationBar: BottomAppBar(
        child: Container(
          color: Colors.grey[200],
          padding: EdgeInsets.all(10),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: List.generate(
              colors.length,
              (index) => _buildColorChose(colors[index]),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildColorChose(Color color) {
    bool isSelected = selectedColor == color;
    return GestureDetector(
      onTap: () => setState(() => selectedColor = color),
      child: Container(
        height: isSelected ? 47 : 40,
        width: isSelected ? 47 : 40,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle,
          border: isSelected ? Border.all(color: Colors.white, width: 3) : null,
        ),
      ),
    );
  }
}

Run the code

image.png

Y
Yuvraj4y ago

Nice work buddy :)

Learn Custom Painter by build a Drawing app