Learn Custom Painter in flutter | Drawing App
build complex app UI with custom painter

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

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

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
shouldRepaintmethod 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


