11

I have already tried to use Canvas.clipPath along with GestureDetector to be like eraser on the canvas where i use the CustomPaint inside a Container with imageDecoration set, so i thought maybe there is another workaround this by using Canvas.drawPath along setting

 final Paint _eraserPaint = Paint()
    ..color = Colors.transparent
    ..blendMode = BlendMode.clear
    ..strokeWidth = 8
    ..style = PaintingStyle.stroke
    ..isAntiAlias = true;

but it draws black lines instead of erasing

any idea how to get around this?

thanks

Mohammad Ersan
  • 12,304
  • 8
  • 54
  • 77

5 Answers5

11

The key is to call saveLayer before drawing anything that might require erasing. After that's done (thus creating a new layer for you to use), you can then draw with any Color to fill, or draw with BlendMode.clear to erase. Lastly, call restore to "merge" the new layer into other existing layers.

For example, let's draw a red square and subtract a circle from it:

void paint(Canvas canvas, Size size) {
  canvas.saveLayer(Rect.largest, Paint());
  canvas.drawRect(Rect.fromLTWH(0, 0, 80, 80), Paint()..color = Colors.red);
  canvas.drawCircle(Offset(40, 40), 40, Paint()..blendMode = BlendMode.clear);
  canvas.restore();
}

Sample result:

A square with a circle cut in the middle

WSBT
  • 33,033
  • 18
  • 128
  • 133
2

May this code can help you!

class DrawingPainter extends CustomPainter {

  List<DrawingPoints> pointsList;
  List<Offset> offsetPoints = List();
  
  DrawingPainter({
    this.pointsList,
  });

  @override
  void paint(Canvas canvas, Size size) {
    canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
    for (int i = 0; i < pointsList.length - 1; i++) {
      if (pointsList[i] != null && pointsList[i + 1] != null) {
        canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
        canvas.drawCircle(pointsList[i].points, pointsList[i].paint.strokeWidth/2, pointsList[i].paint);
      }
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(DrawingPainter oldDelegate) => true;
}

class DrawingPoints {
  Paint paint;
  Offset points;
  DrawingPoints({this.points, this.paint});
}

You need saveLayer, then restores to save Paint

Maybe you need to add this code to te Statefull widget.

void changeBrush(bool isErease){
    setState(() {
      if ( isErease ){
        paint = Paint();
        paint.blendMode = BlendMode.clear;
        paint.color = Colors.white;
        paint.strokeWidth = strokeWidth;
      }else{
        paint = Paint();
        paint.isAntiAlias = true;
        paint.color = selectedColor.withOpacity(opacity);
        paint.strokeWidth = strokeWidth;
      }
    });
  }
0

may this code help you...

import 'dart:convert';
    import 'dart:io';
    import 'dart:typed_data';
    import 'dart:ui' as ui;
    
    import 'package:animated_floatactionbuttons/animated_floatactionbuttons.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter/services.dart';
    import 'package:image_gallery_saver/image_gallery_saver.dart';
    import 'package:image_picker/image_picker.dart';
    import 'package:permission_handler/permission_handler.dart';
    
    
    var appbarcolor = Colors.blue;
    class CanvasPainting_test extends StatefulWidget {
    
      @override
      _CanvasPainting_testState createState() => _CanvasPainting_testState();
    }
    
    class _CanvasPainting_testState extends State<CanvasPainting_test> {
      GlobalKey globalKey = GlobalKey();
    
      List<TouchPoints> points = List();
      double opacity = 1.0;
      StrokeCap strokeType = StrokeCap.round;
      double strokeWidth = 3.0;
      double strokeWidthforEraser = 3.0;
      Color selectedColor;
    
      Future<void> _pickStroke() async {
        //Shows AlertDialog
        return showDialog<void>(
          context: context,
    
          //Dismiss alert dialog when set true
          barrierDismissible: true, // user must tap button!
          builder: (BuildContext context) {
            //Clips its child in a oval shape
            return ClipOval(
              child: AlertDialog(
                //Creates three buttons to pick stroke value.
                actions: <Widget>[
                  //Resetting to default stroke value
                  FlatButton(
                    child: Icon(
                      Icons.clear,
                    ),
                    onPressed: () {
                      strokeWidth = 3.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 24,
                    ),
                    onPressed: () {
                      strokeWidth = 10.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 40,
                    ),
                    onPressed: () {
                      strokeWidth = 30.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 60,
                    ),
                    onPressed: () {
                      strokeWidth = 50.0;
                      Navigator.of(context).pop();
                    },
                  ),
                ],
              ),
            );
          },
        );
      }
    
      Future<void> _opacity() async {
        //Shows AlertDialog
        return showDialog<void>(
          context: context,
    
          //Dismiss alert dialog when set true
          barrierDismissible: true,
    
          builder: (BuildContext context) {
            //Clips its child in a oval shape
            return ClipOval(
              child: AlertDialog(
                //Creates three buttons to pick opacity value.
                actions: <Widget>[
                  FlatButton(
                    child: Icon(
                      Icons.opacity,
                      size: 24,
                    ),
                    onPressed: () {
                      //most transparent
                      opacity = 0.1;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.opacity,
                      size: 40,
                    ),
                    onPressed: () {
                      opacity = 0.5;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.opacity,
                      size: 60,
                    ),
                    onPressed: () {
                      //not transparent at all.
                      opacity = 1.0;
                      Navigator.of(context).pop();
                    },
                  ),
                ],
              ),
            );
          },
        );
      }
    
      Future<void> _pickStrokeforEraser() async {
        //Shows AlertDialog
        return showDialog<void>(
          context: context,
    
          //Dismiss alert dialog when set true
          barrierDismissible: true, // user must tap button!
          builder: (BuildContext context) {
            //Clips its child in a oval shape
            return ClipOval(
              child: AlertDialog(
                //Creates three buttons to pick stroke value.
                actions: <Widget>[
                  //Resetting to default stroke value
                  FlatButton(
                    child: Icon(
                      Icons.clear,
                    ),
                    onPressed: () {
                      strokeWidthforEraser = 3.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 24,
                    ),
                    onPressed: () {
                      strokeWidthforEraser = 10.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 40,
                    ),
                    onPressed: () {
                      strokeWidthforEraser = 30.0;
                      Navigator.of(context).pop();
                    },
                  ),
                  FlatButton(
                    child: Icon(
                      Icons.brush,
                      size: 60,
                    ),
                    onPressed: () {
                      strokeWidthforEraser = 50.0;
                      Navigator.of(context).pop();
                    },
                  ),
                ],
              ),
            );
          },
        );
      }
    
      Future<void> _save() async {
        RenderRepaintBoundary boundary =
        globalKey.currentContext.findRenderObject();
        ui.Image image = await boundary.toImage();
        ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
        Uint8List pngBytes = byteData.buffer.asUint8List();
    
        //Request permissions if not already granted
        if (!(await Permission.storage.status.isGranted))
          await Permission.storage.request();
    
        final result = await ImageGallerySaver.saveImage(
            Uint8List.fromList(pngBytes),
            quality: 60,
            name: "canvas_image");
        print(result);
      }
      String erase = 'yes';
      List<Widget> fabOption() {
        return <Widget>[
          FloatingActionButton(
            backgroundColor: appbarcolor,
            heroTag: "camera",
            child: Icon(Icons.camera),
            tooltip: 'camera',
            onPressed: () {
              //min: 0, max: 50
              setState(() {
                erase = 'yes';
                this._showDialog();
                // _save();
              });
            },
          ),
          FloatingActionButton(
            backgroundColor: appbarcolor,
            heroTag: "paint_save",
            child: Icon(Icons.file_download),
            tooltip: 'Save',
            onPressed: () {
              //min: 0, max: 50
              setState(() {
                erase = 'yes';
                _save();
              });
            },
          ),
          FloatingActionButton(
            backgroundColor: appbarcolor,
            heroTag: "paint_stroke",
            child: Icon(Icons.brush),
            tooltip: 'Stroke',
            onPressed: () {
              //min: 0, max: 50
              setState(() {
                erase = 'yes';
                _pickStroke();
              });
            },
          ),
          // FloatingActionButton(
          //   heroTag: "paint_opacity",
          //   child: Icon(Icons.opacity),
          //   tooltip: 'Opacity',
          //   onPressed: () {
          //     //min:0, max:1
          //     setState(() {
          //       _opacity();
          //     });
          //   },
          // ),
          FloatingActionButton(
            backgroundColor: appbarcolor,
            heroTag: "Erase",
            child: Icon(Icons.ac_unit),
            tooltip: 'Erase',
            onPressed: () {
              //min: 0, max: 50
              setState(() {
                // _save();
                // selectedColor = Colors.transparent;
                // print(Platform.isAndroid);
                erase = 'no';
                _pickStrokeforEraser();
              });
            },
          ),
          FloatingActionButton(
              backgroundColor: appbarcolor,
              heroTag: "Clear All",
              child: Icon(Icons.clear),
              tooltip: "Clear All",
              onPressed: () {
                setState(() {
                  erase = 'yes';
                  points.clear();
                });
              }),
          FloatingActionButton(
            backgroundColor: Colors.white,
            heroTag: "color_red",
            child: colorMenuItem(Colors.red),
            tooltip: 'Color',
            onPressed: () {
              setState(() {
                selectedColor = Colors.red;
              });
            },
          ),
          FloatingActionButton(
            backgroundColor: Colors.white,
            heroTag: "color_green",
            child: colorMenuItem(Colors.green),
            tooltip: 'Color',
            onPressed: () {
              setState(() {
                erase = 'yes';
                selectedColor = Colors.green;
              });
            },
          ),
          FloatingActionButton(
            backgroundColor: Colors.white,
            heroTag: "color_pink",
            child: colorMenuItem(Colors.pink),
            tooltip: 'Color',
            onPressed: () {
              setState(() {
                selectedColor = Colors.pink;
              });
            },
          ),
          FloatingActionButton(
            backgroundColor: Colors.white,
            heroTag: "color_blue",
            child: colorMenuItem(Colors.blue),
            tooltip: 'Color',
            onPressed: () {
              setState(() {
                erase = 'yes';
                selectedColor = Colors.blue;
              });
            },
          ),
        ];
      }
    
    
      void _showDialog() {
        // flutter defined function
        showDialog(
          context: context,
          builder: (BuildContext context) {
            // return object of type Dialog
            return AlertDialog(
    //          title: new Text("Alert Dialog title"),
              content: Row(
                // mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  RaisedButton(
                    onPressed: getImageCamera,
                    child: Text('From Camera'),
                  ),
                  SizedBox(
                    width: 5,
                  ),
                  RaisedButton(
                    onPressed: getImageGallery,
                    child: Text('From Gallery'),
                  )
                ],
              ),
            );
          },
        );
      }
      File _image;
      Future getImageCamera() async {
        var image = await ImagePicker.pickImage(source: ImageSource.camera);
    
        print(image);
    
        if (image != null) {
          setState(() {
            _image = image;
          });
          Navigator.of(context, rootNavigator: true).pop('dialog');
        }
      }
    
      Future getImageGallery() async {
        var image = await ImagePicker.pickImage(source: ImageSource.gallery);
    
        print(image);
    
        if (image != null) {
          setState(() {
            _image = image;
            print(_image);
          });
          Navigator.of(context, rootNavigator: true).pop('dialog');
        }
      }
      /*-------------------------------------*/
    
    
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('paint on image and erase'),backgroundColor: Colors.blueGrey
            // leading: IconButton(
            //   icon: Icon(Icons.arrow_back_ios),onPressed: (){
            //   Navigator.pop(context);
            // },),
          ),
          body: GestureDetector(
            onPanUpdate: (details) {
              setState(() {
                RenderBox renderBox = context.findRenderObject();
                erase!='no'?   points.add(TouchPoints(
                    points: renderBox.globalToLocal(details.globalPosition),
                    paint: Paint()
                      ..strokeCap = strokeType
                      ..isAntiAlias = true
                      ..color = selectedColor.withOpacity(opacity)
                      ..strokeWidth = strokeWidth))
    
                    : points.add(TouchPoints(
                    points:  renderBox.globalToLocal(details.globalPosition),
                    paint: Paint()
                      ..color = Colors.transparent
                      ..blendMode = BlendMode.clear
                      ..strokeWidth = strokeWidthforEraser
                      ..style = PaintingStyle.stroke
                      ..isAntiAlias = true
                ));
              });
            },
            onPanStart: (details) {
              setState(() {
                RenderBox renderBox = context.findRenderObject();
                erase!='no'?   points.add(TouchPoints(
                    points: renderBox.globalToLocal(details.globalPosition),
                    paint: Paint()
                      ..strokeCap = strokeType
                      ..isAntiAlias = true
                      ..color = selectedColor.withOpacity(opacity)
                      ..strokeWidth = strokeWidth))
    
                    : points.add(TouchPoints(
                    points:  renderBox.globalToLocal(details.globalPosition),
                    paint: Paint()
                      ..color = Colors.transparent
                      ..blendMode = BlendMode.clear
                      ..strokeWidth = strokeWidthforEraser
                      ..style = PaintingStyle.stroke
                      ..isAntiAlias = true
                ));
    
    
              });
            },
            onPanEnd: (details) {
              setState(() {
                points.add(null);
              });
            },
            child: RepaintBoundary(
              key: globalKey,
              child: Stack(
                children: <Widget>[
                  Center(
                    child: _image == null
                        ? Image.asset(
                      "assets/images/helo.jfif",
                    )
                        : Image.file(_image),
                  ),
                  CustomPaint(
                    size: Size.infinite,
                    painter: MyPainter(
                      pointsList: points,
                    ),
                  ),
                ],
              ),
            ),
          ),
          floatingActionButton: AnimatedFloatingActionButton(
              fabButtons: fabOption(),
              colorStartAnimation: appbarcolor,
              colorEndAnimation: Colors.red[300],
              animatedIconData: AnimatedIcons.menu_close),
        );
      }
    
      Widget colorMenuItem(Color color) {
        return GestureDetector(
          onTap: () {
            setState(() {
              selectedColor = color;
            });
          },
          child: ClipOval(
            child: Container(
              padding: const EdgeInsets.only(bottom: 8.0),
              height: 36,
              width: 36,
              color: color,
            ),
          ),
        );
      }
    
    
    
    
    
    
    
    
    }
    
    class MyPainter extends CustomPainter {
      MyPainter({this.pointsList});
    
      //Keep track of the points tapped on the screen
      List<TouchPoints> pointsList;
      List<Offset> offsetPoints = List();
    
      //This is where we can draw on canvas.
      @override
      void paint(Canvas canvas, Size size) {
        canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
        for (int i = 0; i < pointsList.length - 1; i++) {
          if (pointsList[i] != null && pointsList[i + 1] != null) {
            canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
            canvas.drawCircle(pointsList[i].points, pointsList[i].paint.strokeWidth/2, pointsList[i].paint);
          }
        }
        canvas.restore();
      }
    
      //Called when CustomPainter is rebuilt.
      //Returning true because we want canvas to be rebuilt to reflect new changes.
      @override
      bool shouldRepaint(MyPainter oldDelegate) => true;
    }
    
    //Class to define a point touched at canvas
    class TouchPoints {
      Paint paint;
      Offset points;
      TouchPoints({this.points, this.paint});
    }
Major Akash
  • 189
  • 5
0

Wrap your custom paint widget into an Opacity widget with opacity of 0.99:

        Opacity(
          opacity: .99,
          child: CustomPaint(
            size: const Size(double.infinity, 100),
            painter: MyPainter(),
          ),
        ),

I don't know why, but this fix the problem without any change in your painter class.

amir_a14
  • 1,478
  • 10
  • 15
0

Came here looking for the opposite (clip the corners of the square and keep the inside). The goal was to draw a rounded rectangle out of several non rounded overlapping rectangles.

This is what worked for this:

canvas.save();

RRect clipRectangle = RRect.fromRectAndRadius(
  Rect.fromLTWH(0, 0, 80, 80),
  Radius.circular(4),
);
canvas.clipRRect(clipRectangle);

canvas.drawRect(Rect.fromLTWH(0, 0, 80, 80), Paint()..color = Colors.red);
canvas.drawRect(Rect.fromLTWH(0, 20, 80, 80), Paint()..color = Colors.blue);

canvas.restore();
Quentin
  • 758
  • 9
  • 11