Build a Whiteboard in Flutter

Build a Whiteboard in Flutter

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