Flutter: Infinite scrolling canvas with CustomPainter
Tags: flutter, custompainter, canvas, dart
Published: 2020-11-22 12:03:49

# Overview

There are many tutorials on canvas drawing using CustomPainter. However, there are no tutorials on how to create an infinite drawing canvas using Flutter. In this article, I will show you a simple trick that can be used to implement infinite scrolling canvas with high performance. [Github Link](https://github.com/sweesenkoh/SmallProjectsCollections/tree/master/Flutter/infinite_canvas_custom_painter)



![ezgif-5-ee57c6228769 1](https://user-images.githubusercontent.com/48687942/99902855-ba5cf600-2cfb-11eb-98b8-3fb0b8565fe6.gif)


# 1. Setup CustomPainter for drawable canvas

I will briefly explain this section as there are many tutorials online on this section. The main idea is that we first need to create a `CustomPainter` class and implement the logic for drawing. Here is one way of implementing it: 

```dart
class CanvasCustomPainter extends CustomPainter {
  List<Offset> points;

  CanvasCustomPainter({@required this.points});

  @override
  void paint(Canvas canvas, Size size) {
    //define canvas background color
    Paint background = Paint()..color = Colors.white;

    //define canvas size
    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);

    canvas.drawRect(rect, background);
    canvas.clipRect(rect);

    //define the paint properties to be used for drawing
    Paint drawingPaint = Paint()
      ..strokeCap = StrokeCap.round
      ..isAntiAlias = true
      ..color = Colors.black
      ..strokeWidth = 1.5;

    //a single line is defined as a series of points followed by a null at the end
    for (int x = 0; x < points.length - 1; x++) {
      //drawing line between the points to form a continuous line
      if (points[x] != null && points[x + 1] != null) {
        canvas.drawLine(points[x], points[x + 1], drawingPaint);
      }
      //if next point is null, means the line ends here
      else if (points[x] != null && points[x + 1] == null) {
        canvas.drawPoints(PointMode.points, [points[x]], drawingPaint);
      }
    }
  }

  @override
  bool shouldRepaint(CanvasCustomPainter oldDelegate) {
    return true;
  }
}
```

With the CustomPainter setup done, we can now use a CustomPaint widget to display the canvas:

```dart
class _InfiniteCanvasPageState extends State<InfiniteCanvasPage> {
  List<Offset> points = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox.expand(
          child: ClipRRect(
            child: CustomPaint(
              painter: CanvasCustomPainter(points: points),
            ),
          ),
        ),
    );
  }
}
```

The last step is to use the `GestureDetector` to add the points into our `points` variable. The `CustomPaint` will then render those points onto the canvas. 

```dart
class _InfiniteCanvasPageState extends State<InfiniteCanvasPage> {
  List<Offset> points = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onPanDown: (details) {
          this.setState(() {
            points.add(details.localPosition);
          });
        },
        onPanUpdate: (details) {
          this.setState(() {
            points.add(details.localPosition);
          });
        },
        onPanEnd: (details) {
          this.setState(() {
            points.add(null);
          });
        },
        child: SizedBox.expand(
          child: ClipRRect(
            child: CustomPaint(
              painter: CanvasCustomPainter(points: points),
            ),
          ),
        ),
      ),
    );
  }
}
```

By this step we now have a basic drawable canvas!

![ezgif-5-edaaad2aa3fc](https://user-images.githubusercontent.com/48687942/99902853-b7620580-2cfb-11eb-96d8-50bdf4cdfd65.gif)


# 2. Making an infinite scrolling canvas

### Defining CanvasState and Changing between Them

At this point, whenever we touch the screen, it will immediately draw the line on the canvas. To make an infinite scrolling canvas, we must first have a way to pan around the canvas. This implies that our canvas should have two different states: Panning state and Drawing state. Panning state will allow user to pan around the canvas while Drawing state allows user to draw onto the canvas. We can make use of `Enum` for such scenario. 

```dart
enum CanvasState { pan, draw }
```

And we shall initialise a `CanvasState` variable in our `InfiniteCanvasPage` class:

```dart
class _InfiniteCanvasPageState extends State<InfiniteCanvasPage> {
  List<Offset> points = [];
  CanvasState canvasState = CanvasState.draw;

  @override
  Widget build(BuildContext context) {
    ...
  }
}
```

We can then use a button to switch between the two states. You can implement your button in whichever way you prefer. For demo purposes, I am using the `FloatingActionButton` in the `Scaffold`. 

```dart
@override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: Text(canvasState == CanvasState.draw ? "Draw" : "Pan"),
        backgroundColor:
            canvasState == CanvasState.draw ? Colors.red : Colors.blue,
        onPressed: () {
          this.setState(() {
            canvasState = canvasState == CanvasState.draw
                ? CanvasState.pan
                : CanvasState.draw;
          });
        },
      ),
      body: GestureDetector(
        ...
      ),
    );
  }
```

![ezgif-5-bd39a0069508](https://user-images.githubusercontent.com/48687942/99902852-b5984200-2cfb-11eb-9cc7-9ec881be9c64.gif)

### Implement the canvas pan logic

To create the effect of infinite scrolling canvas, the idea of having an infinitely large `CustomPaint` widget is not feasible and not a good way to implement it due to performance reasons. Instead, another method that we can use to achieve the same bahaviour is to make use of an `Offset` variable. Whenever the user pans the screen, we will add the pan distance to the offset variable. Whenever we render the lines onto the canvas, we will subtract this offset to the coordinates of each point in order to render the points at the correct position after offset. 

For example, if the user pans the screen from right to left for 50px, the offset variable is now `(-50,0)` . If we have a point that is originally at `(70,10)` we can now add this point with the offset, giving the final coordinate to be `(20,10)`  . 

First, we add an offset variable into our `InfiniteCanvasPage` class: 

```dart
class _InfiniteCanvasPageState extends State<InfiniteCanvasPage> {
  List<Offset> points = [];
  CanvasState canvasState = CanvasState.draw;
  
	//add the offset variable to keep track of the current offset
  Offset offset = Offset(0, 0);

	...
}
```

The next step is to modify our `CanvasCustomPainter` class. For each of the points being rendered, we need to account for the offset: 

```dart
class CanvasCustomPainter extends CustomPainter {
  List<Offset> points;
  Offset offset;

  CanvasCustomPainter({@required this.points, this.offset});

  @override
  void paint(Canvas canvas, Size size) {
    
		...
		...

    //added offset when rendering
    for (int x = 0; x < points.length - 1; x++) {
      
      if (points[x] != null && points[x + 1] != null) {
        canvas.drawLine(
            points[x] + offset, points[x + 1] + offset, drawingPaint);
      }
      
      else if (points[x] != null && points[x + 1] == null) {
        canvas.drawPoints(PointMode.points, [points[x] + offset], drawingPaint);
      }
    }
  }

	...
	...
}
```

We also have to update our `CustomPaint` component to pass in the offset variable

```dart
SizedBox.expand(
          child: ClipRRect(
            child: CustomPaint(
              painter: CanvasCustomPainter(points: points, offset: offset),
            ),
          ),
        )
```

The last step is to modify our logic in the various `onPan` method inside the `GestureDetector`.  The main idea is when `canvasState == CanvasState.pan`, we have to add the pan distance to the `offset` variable. Luckily, this can be conveniently achieved by using the `details.delta` variable provided by `onPanUpdate`. Here is how our final `GestureDetector` code will look like:

```dart
...

body: GestureDetector(
        onPanDown: (details) {
          this.setState(() {
            if (canvasState == CanvasState.draw) {
              points.add(details.localPosition - offset);
            }
          });
        },
        onPanUpdate: (details) {
          this.setState(() {
            if (canvasState == CanvasState.pan) {
              offset += details.delta;
            } else {
              points.add(details.localPosition - offset);
            }
          });
        },
        onPanEnd: (details) {
          this.setState(() {
            if (canvasState == CanvasState.draw) {
              points.add(null);
            }
          });
        },
        child: SizedBox.expand(
          child: ClipRRect(
            child: CustomPaint(
              painter: CanvasCustomPainter(points: points, offset: offset),
            ),
          ),
        ),
      ),

...
```

With this, we now have an infinite scrolling canvas!

![ezgif-5-ee57c6228769 1](https://user-images.githubusercontent.com/48687942/99902855-ba5cf600-2cfb-11eb-98b8-3fb0b8565fe6.gif)