Building a Drawing App in Flutter
Learn how to create a drawing app in Flutter and explore Flutter’s capability to render and control a custom UI with the help of CustomPaint widgets. By Samarth Agarwal.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building a Drawing App in Flutter
35 mins
- Getting Started
- Introducing Flutter Canvas and CustomPaint
- Using the CustomPaint Widget
- Understanding Canvas Basics
- Drawing Paths
- Changing the Stroke, Color and Width
- Diving Into Code
- Using GestureDetector
- Drawing a Single Path
- Drawing Multiple Paths
- Adding Stroke Color and Width
- Changing Stroke Color
- Changing Stroke Width
- Optimizing Your App
- Drawing Multiple Lines
- Using StreamBuilders and Two CustomPaint widgets
- Saving the Drawing
- Creating New and Save Buttons
- Using the Plugin
- Where to Go From Here
Diving Into Code
Now you know the basics of drawing simple shapes on the canvas, and so it’s time to start working on your drawing app. Exit this sample app and run the drawing app by using the command flutter run.
Then, you’ll start work on your app by drawing a simple Path
using Sketcher
— a CustomPainter
class for this project located in lib/sketcher.dart — in combination with CustomPaint
. You’ll use all of this together with GestureDetector
to find the coordinates of the points that the user touches.
Using GestureDetector
Start by implementing buildCurrentPath()
in lib/drawing_page.dart:
GestureDetector buildCurrentPath(BuildContext context) {
return GestureDetector(
onPanStart: onPanStart,
onPanUpdate: onPanUpdate,
onPanEnd: onPanEnd,
child: RepaintBoundary(
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
// CustomPaint widget will go here
),
),
);
}
In the code above, you return GestureDetector
from buildCurrentPath()
. You use GestureDetector
‘s onPanStart()
, onPanUpdate()
and onPanEnd()
events to detect the touches — and specifically the dragging — on the screen. You also use RepaintBoundary
to optimize the redrawing.
The next step is to implement the three missing methods. Create the methods as shown below:
void onPanStart(DragStartDetails details) {
print('User started drawing');
final box = context.findRenderObject() as RenderBox;
final point = box.globalToLocal(details.globalPosition);
print(point);
}
void onPanUpdate(DragUpdateDetails details) {
final box = context.findRenderObject() as RenderBox;
final point = box.globalToLocal(details.globalPosition);
print(point);
}
void onPanEnd(DragEndDetails details) {
print('User ended drawing');
}
In the code snippet above:
-
onPanStart()
is executed when the user touches the screen and starts dragging their finger around it. - When the user is dragging their finger on the screen without lifting it off the screen, the app executes
onPanUpdate()
. -
onPanEnd()
is executed when the user lifts their finger off the screen.
To find RenderBox
for GestureDetector
, you used findRenderObject()
. You also used globalToLocal()
to convert the global coordinates to the local coordinates you’ll use to draw the path.
For now, you are printing the points the user touches on the screen to the console, to ensure that the detection works as expected.
Finally, add buildCurrentPath()
to Stack
in the main build()
.
...
Stack(
children: [
// Add this
buildCurrentPath(context),
],
),
Save everything and hot restart. Touch the screen and you’ll see some logs on the console similar to the ones shown below.
I/flutter (21819): User started drawing
I/flutter (21819): Offset(157.5, 305.5)
I/flutter (21819): Offset(157.5, 305.5)
...
I/flutter (21819): Offset(158.9, 362.2)
I/flutter (21819): User ended drawing
Drawing a Single Path
Combining the three methods above will give us coordinates of all the points the user touches with their screen in one go, without lifting their finger. Next, you need to create a DrawnLine
using these three methods.
Modify the three methods as shown below:
void onPanStart(DragStartDetails details) {
...
setState((){
line = DrawnLine([point], selectedColor, selectedWidth);
});
}
void onPanUpdate(DragUpdateDetails details) {
...
final path = List.from(line.path)..add(point);
setState((){
line = DrawnLine(path, selectedColor, selectedWidth);
});
}
void onPanEnd(DragEndDetails details) {
setState((){
print('User ended drawing');
});
}
Here’s what the code above does:
- Inside
onPanStart()
, you create a newDrawnLine
and use the only point you have to createDrawnLine
. Additionally, you useselectedColor
for the color, andselectedWidth
for the stroke width. Both of these have default values. Then, you callsetState()
to update the UI. - Inside
onPanUpdate()
, you create a path that’s a type ofList<Offset>
, add new points to the list and update theline
. Finally, you callsetState()
to update the UI.
To see the DrawnLine
line, you need a CustomPaint
inside buildCurrentPath()
. Add the following code as the Container
‘s child and import the missing file – sketcher.dart.
GestureDetector buildCurrentPath(BuildContext context) {
return GestureDetector(
...
child: RepaintBoundary(
child: Container(
...
child: CustomPaint(
painter: Sketcher(lines: [line]),
),
),
),
);
}
In the code snippet above, you add a CustomPaint
widget. For the painter
parameter, you pass in a Sketcher
instance. It takes in the lines
property to which you pass in a List
containing the DrawnLine
that you created using gesture events.
Save all the files and hot restart, then try drawing with your finger on the screen.
Awesome, right? :]
Drawing Multiple Paths
Right now, when you draw a new path the older path just disappears. This happens because you reinitialize the line
inside onPanStart()
. As soon as you touch the screen, you lose the old path.
To draw multiple paths on the screen, you’ll need to store all the path points. You won’t always reinitialize the line
inside onPanStart()
. You only have to initialize it if the user is drawing the very first path.
When a path ends you’ll insert a null
value instead of an Offset
as the point, to know you need to initialize a new starting point.
Here are the changes you need to apply to GestureDetector
events:
void onPanStart(DragStartDetails details) {
print('User started drawing');
...
if (line == null) {
line = DrawnLine([point], selectedColor, selectedWidth);
}
...
}
void onPanUpdate(DragUpdateDetails details) {
...
}
void onPanEnd(DragEndDetails details) {
final path = List.from(line.path)..add(null);
setState(() {
line = DrawnLine(path, selectedColor, selectedWidth);
});
}
Here’s what’s happening in the code above:
- Inside
onPanStart()
, you initializeline
only if the line object isnull
. This means that it is only initialized when the user touches the screen for the first time to draw the very first path. This prevents theline
from being cleared on eachonPanStart()
event. - There are no changes in
onPanUpdate()
. - Inside
onPanEnd
, you insert anOffset
with anull
value to mark the end of path. TheSketcher
takes thesenull
values into account and draws paths accordingly.
Save everything and hot restart. Try drawing on the screen.
If you keep drawing, you’ll see that you’re able to draw multiple paths. It is actually a single path that is broken in various places with the help of null
values.
Adding Stroke Color and Width
If you scroll to the top in lib/drawing_page.dart, you’ll find two variables defined: selectedColor
and selectedWidth
:
final selectedColor = Colors.black;
final selectedWidth = 5.0;
These are the values you pass to DrawnLine
. This is the reason behind the default color and width of the paths you have been drawing so far.