Whiteboards have been an essential tool for students and professionals alike to share ideas, brainstorm and jot down notes in real time. With the advent of technology, digital whiteboards have become increasingly popular, offering more features and functionalities than their traditional counterparts. In this blog, we'll be diving into the world of Flutter and creating our very own digital whiteboard application. From selecting colors to adjusting the stroke width, our whiteboard will have everything you need to get your ideas flowing. So, let's get started and bring your ideas to life with the power of Flutter!
You can check out the whole code here: https://github.com/mnnkhndlwl/flutter_whiteboard
This is a Flutter app that provides a whiteboard for users to draw. It has a slider to adjust the stroke width of the drawing and a button to clear the board. A bottom app bar provides a color palette to choose the color of the drawing. The app uses the CustomPainter
class to paint the lines on a canvas and the GestureDetector
widget to handle the user's touch events. The lines are drawn by updating the drawingPoints
list when the user interacts with the canvas. The selectedColor
variable holds the currently selected color.
I am adding coding snippets with explanations of all of them below that you can follow along with this blog
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Whiteboard',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DrawingBoard(),
);
}
}
As you can see above the homepage of our app is a DrawingBoard widget.
class DrawingBoard extends StatefulWidget {
const DrawingBoard({Key key}) : super(key: key);
@override
State<DrawingBoard> createState() => _DrawingBoardState();
}
The createState
the method returns a new instance of the _DrawingBoardState
class, which will hold the mutable state of this widget. The StatefulWidget
and its associated state class are used together to manage the dynamic elements of the UI, allowing the user to interact with the app and modify the user interface.
class DrawingPoint {
Offset offset;
Paint paint;
DrawingPoint(this.offset, this.paint);
}
The DrawingPoint
class is a simple data structure that holds the position of a point on a canvas (stored as an Offset
) and its associated Paint
object, which determines how the point will be drawn. The class takes offset
and paint
as arguments in the constructor and initializes its instance variables. This class will be used to store the points of a drawing made on canvas.
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;
}
This is the _DrawingPainter class in Flutter. It's a custom painter that allows you to draw on a canvas. The class takes in a list of "DrawingPoint" objects which represent the points being drawn on the canvas.
The class implements two methods, paint and shouldRepaint. The paint method is responsible for drawing on the canvas. It loops through the list of DrawingPoint objects and checks if the current point and the next point are both present. If they are, it uses the canvas.drawLine method to connect the two points with a line. If only the current point is present, it uses canvas.drawPoints to draw a single point on the canvas.
The shouldRepaint method always returns true, which means that the canvas will be repainted every time there's a change.
@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);
}
}
}
The above code is part of the _DrawingPainter
class that extends the CustomPainter
class in Flutter. It provides a custom implementation for the paint
method that is called by Flutter to render the widget on the screen.
The paint
method receives a Canvas
object and a Size
object as its parameters. The Canvas
object is used to draw shapes and the Size
object represents the size of the widget being painted.
The paint
method iterates over a list of DrawingPoint
objects, which represent the points where the user has drawn on the screen. For each point, the method checks if it is not null
and if there is a next point. If there is a next point, a line is drawn between the current point and the next point. If there is no next point, it means that the current point is the end of the drawing and the method draws a single point on the canvas.
class _DrawingBoardState extends State<DrawingBoard> {
Color selectedColor = Colors.black;
double strokeWidth = 5;
List<DrawingPoint> drawingPoints = [];
List<Color> colors = [
Colors.pink,
Colors.red,
Colors.black,
Colors.yellow,
Colors.amberAccent,
Colors.purple,
Colors.green,
];
@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"),
)
],
),
),
],
),
appBar: AppBar(
title: Text('WhiteBoard'),
),
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]),
),
),
),
),
);
}
Here is the code for the _DrawingBoardState
class. It defines the state for a DrawingBoard
widget. The build
method returns a Scaffold
widget that contains a Stack
of widgets, including a GestureDetector
for detecting user drawing gestures, a CustomPaint
widget for rendering the drawing, a positioned widget for displaying a slider and a clear button, and a BottomAppBar
for selecting colors. The _buildColorChose
method is used to create color choice widgets in the bottom navigation bar.
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);
});
},
The code above is setting up the behavior for a GestureDetector
widget. The GestureDetector
listens to pan gestures, which are dragging gestures made with a single finger.
When the pan gesture starts (onPanStart
), it creates a new DrawingPoint
object and adds it to the drawingPoints
list. The DrawingPoint
object is a custom object that holds the position of the gesture and a Paint
object to control how the line is drawn on the screen.
When the pan gesture is updated (onPanUpdate
), it adds another DrawingPoint
to the drawingPoints
list.
When the pan gesture ends (onPanEnd
), it adds a null
value to the drawingPoints
list. This acts as a separator between different drawing segments.
In the end, this will allow the user to draw on the screen by moving their finger. The CustomPaint
widget will then use this information to draw the lines on the screen.
I hope you will understand how this flutter whiteboard works by reading this blog I have skipped the explanation of some code but if you are familiar with flutter then you can understand it easily