How to erase/clip from Canvas CustomPaint? - flutter

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

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:

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;
}
});
}

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});
}

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.

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();

Related

Cannot perform drawing after canvas is zoom-in, zoom-out and translated in flutter with a an image on it

Aim : I want to make an image marking functionality i.e drawing on an image along with zoom-in and zoom-out functions. For that I have used a canvas with an image on it and flutter Transform widget to achieve zoom and translation. I have succeeded in making the image zoom and translate. Also I can perform drawing on that image.
Problem : The problem is I can perform drawing only on the area which was set when Image was in original size, i.e when Image expands then I can draw on only that area(original image area) which comes under the new expanded(zoomed) image, same problem occurs when I move the image.
NOTE - I have set the size of canvas to the size of my image.
On the below screen-shots 1.before_zoom displays my image when I load it from assets. On that I had drawn a rectangle which shows drawing can be done on any part of image. In after_zoom image I had again drawn a rectangle which show the available screen for drawing. If I try to perform drawing outside that rectangle, it doesn't works same problem occurs when I move the image.
The problem I think is that canvas drawing space gets fixed when image first loads and when the image expands or moves it just lets drawing on the area which was first set on time of image loading.
So anyone knows how to fix these problem or any other method to achieve these functionality.
Code :
/// DrawingScreen :
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
class _DrawingScreenState extends State<DrawingScreen> {
ui.Image _image;
Future _future;
Color _selectedColor;
double _strokeWidth;
List<TouchPoints> points = [];
#override
void initState() {
super.initState();
_selectedColor = Colors.yellow;
_strokeWidth = 14.0;
_future = load();
}
String imageLocation = 'images/2.jpg';
/// FUNCTION TO LOAD THE IMAGE FROM THE ASSETS FOLDER
Future<void> load() async {
ByteData data = await rootBundle.load(imageLocation);
ui.Codec codec1 = await ui.instantiateImageCodec(data.buffer.asUint8List());
ui.FrameInfo fi = await codec1.getNextFrame();
_image = fi.image;
tempIW = _image.width.toDouble();
tempIH = _image.height.toDouble();
}
double translateX = 0;
double translateY = 0;
double scaleFactor = 1;
double tempIW;
double tempIH;
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Scaffold(
bottomNavigationBar: Container(
color: Color(0xffF8F8FF),
child: Row(children: [
Flexible(
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 20,
onPressed: () {
setState(() {
if (scaleFactor > 0.11)
setState(() {
scaleFactor = scaleFactor - 0.1;
});
});
},
icon: Icon(Icons.zoom_in),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
if (scaleFactor < 0.91) {
scaleFactor = scaleFactor + 0.1;
}
});
},
icon: Icon(Icons.zoom_out),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateX = translateX - 100;
});
},
icon: Icon(Icons.arrow_back),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateX = translateX + 100;
});
},
icon: Icon(Icons.arrow_forward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateY = translateY - 100;
});
},
icon: Icon(Icons.arrow_upward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
setState(() {
translateY = translateY + 100;
});
},
icon: Icon(Icons.arrow_downward),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
this.setState(() {
points.clear();
});
},
icon: Icon(Icons.layers_clear),
),
),
Flexible(
child: IconButton(
iconSize: 20,
padding: EdgeInsets.zero,
onPressed: () {
undo();
setState(() {
points;
});
},
icon: Icon(Icons.wifi_protected_setup),
),
),
]),
),
body: SafeArea(
child: Center(
child: FittedBox(
fit: BoxFit.fill,
child: Transform(
transform: Matrix4(
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
translateX, translateY, 0, scaleFactor,
),
child: GestureDetector(
onPanStart: (details) {
setState(() {
if (details.localPosition.dx < _image.width.toDouble() && details.localPosition.dy < _image.height.toDouble())
points.add(
TouchPoints(
points: Offset(
details.localPosition.dx,
details.localPosition.dy,
),
paint: Paint()
..color = _selectedColor
..strokeWidth = _strokeWidth
..strokeCap = StrokeCap.round,
),
);
});
},
onPanUpdate: (details) {
setState(() {
if (details.localPosition.dx < _image.width.toDouble() && details.localPosition.dy < _image.height.toDouble())
points.add(
TouchPoints(
points: Offset(
details.localPosition.dx,
details.localPosition.dy,
),
paint: Paint()
..color = _selectedColor
..strokeWidth = _strokeWidth
..strokeCap = StrokeCap.round,
),
);
});
},
onPanEnd: (details) {
setState(() {
points.add(null);
});
print(details.velocity);
},
child: RepaintBoundary(
child: SizedBox(
width: _image.width.toDouble(),
height: _image.height.toDouble(),
child: ClipRect(
child: CustomPaint(
painter: FacePainter(
_image,
points,
),
),
),
),
),
),
),
),
),
),
);
} else {
return CircularProgressIndicator();
}
},
);
}
}
class FacePainter extends CustomPainter {
final ui.Image _image;
List<TouchPoints> pointsList = [];
FacePainter(
this._image,
this.pointsList,
) : super();
#override
void paint(Canvas canvas, Size size) async {
canvas.drawImage(_image, Offset(0.0, 0.0), Paint());
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null && pointsList[i + 1] != null) {
if (pointsList[i].points.dx < size.width && pointsList[i].points.dy < size.height)
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
}
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
class TouchPoints {
Paint paint;
Offset points;
TouchPoints({this.paint, this.points});
}
Images for reference.
Before Zoom
After Zoom

How to draw a single line in a certain area, and it continues from where it stopped

I just figured out how to draw a line with arrow buttons. However, it doesn't draw the line correctly. Therefore, I was wondering if someone can help me out with drawing just one line in a drawing area(Ideally in a scrollable box) and each time when releasing the button and tapping again, it should continue from where it stopped please?
This is what it looks like rn:
Drawing lines with arrow buttons
Here's part of my code:
Widget build(BuildContext context) {
return Transform.rotate(
angle: radians(0),
child: Stack(
alignment:
Alignment.lerp(Alignment.topCenter, Alignment.center, 0.50),
children: [
ImageInput(_selectImage),
_buildButton(0, color: Colors.red, icon: Icons.east),
_buildButton(45, color: Colors.green, icon: Icons.south_east),
_buildButton(90, color: Colors.orange, icon: Icons.south),
_buildButton(135, color: Colors.blue, icon: Icons.south_west),
_buildButton(180, color: Colors.black, icon: Icons.west),
_buildButton(225, color: Colors.indigo, icon: Icons.north_west),
_buildButton(270, color: Colors.pink, icon: Icons.north),
_buildButton(315, color: Colors.yellow, icon: Icons.north_east),
]));
}
_buildButton(double angle, {Color color, IconData icon}) {
final double rad = radians(angle);
return Transform(
transform: Matrix4.identity()
..translate(
100 * cos(rad),
100 * sin(rad),
),
child: Column(children: <Widget>[
SizedBox(
height: 150,
width: 150,
child: Container(),
),
CustomPaint(
foregroundPainter: LinePainter(_offset),
child: FloatingActionButton(
child: Icon(icon),
backgroundColor: color,
onPressed: () {
HoldDetector(
onHold: _incrementCounter(angle),
holdTimeout: Duration(milliseconds: 200),
enableHapticFeedback: true,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _incrementCounter(angle),
),
);
},
),
),
]),
);
}
_incrementCounter(double angle) {
_cnt += 10;
switch (angle.round()) {
case 0:
setState(() {
_offset = Offset(_cnt, 0);
});
break;
case 45:
setState(() {
_offset = Offset(_cnt, _cnt);
});
break;
case 90:
setState(() {
_offset = Offset(0, _cnt);
});
break;
case 135:
setState(() {
_offset = Offset(-_cnt, _cnt);
});
break;
case 180:
setState(() {
_offset = Offset(-_cnt, 0);
});
break;
case 225:
setState(() {
_offset = Offset(-_cnt, -_cnt);
});
break;
case 270:
setState(() {
_offset = Offset(0, -_cnt);
});
break;
case 315:
setState(() {
_offset = Offset(_cnt, -_cnt);
});
break;
}
}
}
class LinePainter extends CustomPainter {
Paint _paint;
Offset _offset;
LinePainter(Offset offset) {
_offset = offset;
_paint = Paint()
..color = Colors.black
..strokeWidth = 8.0;
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0, 0), _offset, _paint);
}
#override
bool shouldRepaint(LinePainter oldDelegate) {
return true;
}
}

Flutter canvas : how to avoid drawing outside image area

import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'dart:ui' as ui;
class EditImage extends StatefulWidget {
final String filePath;
EditImage({this.filePath});
#override
_EditImageState createState() => _EditImageState();
}
class _EditImageState extends State<EditImage> {
ui.Image decodedImage;
String newFilePath;
GlobalKey myCanvasKey = GlobalKey();
ImageEditor editor;
Color color = Colors.blue;
#override
void initState() {
loadImage(File(widget.filePath));
super.initState();
}
void loadImage(File image) async {
final data = await image.readAsBytes();
decodedImage = await decodeImageFromList(data);
editor = ImageEditor(image: decodedImage, strokeColor: color);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
onTap: () {
Navigator.pop(context, newFilePath ?? widget.filePath);
},
child: Icon(Icons.close),
),
//centerTitle: true,
title: Text('Edit'),
actions: [
InkWell(
onTap: () {
editor.undo();
myCanvasKey.currentContext.findRenderObject().markNeedsPaint();
},
child: Icon(Icons.undo),
),
SizedBox(
width: 10.0,
),
InkWell(
onTap: () async {
Color pickedColor;
bool isSelected = false;
await showDialog(
context: context,
child: AlertDialog(
contentPadding: const EdgeInsets.all(8.0),
title: const Text('Stroke Color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: color,
onColorChanged: (color) {
pickedColor = color;
},
enableAlpha: false,
showLabel: false,
pickerAreaHeightPercent: 0.6,
),
),
actions: <Widget>[
FlatButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context),
),
FlatButton(
child: const Text('Select'),
onPressed: () {
isSelected = true;
Navigator.of(context).pop();
},
),
],
),
);
if (isSelected) {
editor.updateStrokeColor(pickedColor);
setState(() {
color = pickedColor;
});
}
},
child: Container(
decoration: BoxDecoration(
//borderRadius: BorderRadius.circular(15.0),
border: Border.all(color: Colors.grey),
shape: BoxShape.circle,
color: color,
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Container(
decoration: BoxDecoration(
color: Colors.black26, shape: BoxShape.circle),
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Icon(
Icons.edit_outlined,
color: Colors.white,
semanticLabel: 'Stroke',
),
),
),
),
),
),
SizedBox(
width: 10.0,
),
InkWell(
onTap: () {
Navigator.pop(context, newFilePath ?? widget.filePath);
},
child: Icon(Icons.done),
),
SizedBox(
width: 10.0,
),
],
),
body: decodedImage == null
? Center(child: CircularProgressIndicator())
: Center(
child: FittedBox(
child: SizedBox(
height: decodedImage.height.toDouble(),
width: decodedImage.width.toDouble(),
child: GestureDetector(
onPanDown: (detailData) {
editor.update(detailData.localPosition);
myCanvasKey.currentContext
.findRenderObject()
.markNeedsPaint();
},
onPanUpdate: (detailData) {
editor.update(detailData.localPosition);
myCanvasKey.currentContext
.findRenderObject()
.markNeedsPaint();
},
onPanEnd: (detailData) {
editor.addNewPointsList();
},
child: CustomPaint(
key: myCanvasKey,
painter: editor,
),
),
),
),
),
);
}
}
class ImageEditor extends CustomPainter {
ImageEditor({
this.image,
this.strokeColor = Colors.black,
}) {
_strokes.add(_StrokeData());
}
final ui.Image image;
Color strokeColor;
List<_StrokeData> _strokes = [];
void updateStrokeColor(Color color) {
strokeColor = color;
}
void update(Offset offset) {
if (_strokes.last.color == null) _strokes.last.color = strokeColor;
_strokes.last.points.add(offset);
}
void addNewPointsList() {
print('pan end');
_strokes.add(_StrokeData());
}
void undo() {
if (_strokes.length > 1) _strokes.removeAt(_strokes.length - 2);
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawImage(image, Offset.zero, Paint());
for (int i = 0; i < _strokes.length; i++) {
Paint _painter = Paint();
_painter.color = _strokes[i].color ?? Colors.transparent;
_painter.style = PaintingStyle.stroke;
_painter.strokeWidth = 15;
_painter.isAntiAlias = true;
for (int j = 0; j < _strokes[i].points.length; j++) {
if (j > 0)
canvas.drawLine(
_strokes[i].points[j - 1], _strokes[i].points[j], _painter);
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class _StrokeData {
Color color;
List<Offset> points;
_StrokeData() {
points = List<Offset>();
}
}
How to restrict drawing within the image area ?
Use ClipRRect() as parent of FittedBox. This solves the problem
ClipRRect(child: FittedBox(child: SizedBox(...Painter widget goes here...) ))

Flutter: How to show CircularprogressIndicator?

void refresh() async {
await get.getFromFirestore(id);
await get.showData(get.data(), context);
setState(() {
markerList = get.getList();
})
}
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: GoogleMap(
mapType: MapType.normal,
mapToolbarEnabled: false,
initialCameraPosition: _currentlo,
onMapCreated: _onMapCreated,
markers: markerList
),
),
floatingActionButton: SpeedDial(
animatedIcon: AnimatedIcons.menu_close,
animatedIconTheme: IconThemeData(size: 22, color: Colors.black),
closeManually: false,
curve: Curves.bounceIn,
overlayColor: Colors.black,
children: [
SpeedDialChild(
backgroundColor: Colors.white,
child: Icon(Icons.refresh, color: Colors.black,),
onTap: (){
refresh();
}
),
],
),
);
}
This is my code. I programmed refresh method. I want to show CircularprogressIndicator when I tap the SpeedDialChild. My data comes from Firestore. So it takes long time when data size is big. How can I do this?
This is radial progressbar code which you can use to update UI
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
class RadialPainter extends CustomPainter {
final Color bgColor;
final Color lineColor;
final double width;
final double percent;
RadialPainter({this.bgColor, this.lineColor, this.width, this.percent});
#override
void paint(Canvas canvas, Size size) {
Paint bgLine = new Paint()
..color = bgColor
..strokeCap = StrokeCap.round
..style=PaintingStyle.stroke
..strokeWidth=width;
Paint completedLine = new Paint()
..color = lineColor
..strokeCap = StrokeCap.round
..style=PaintingStyle.stroke
..strokeWidth=width;
Offset center= Offset(size.width/2, size.height/2);
double radius= min(size.width/2, size.height/2);
double sweepAngle=2*pi*percent;
canvas.drawCircle(center, radius, bgLine);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
pi,
sweepAngle,
false,
completedLine
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
To call this function
child: CustomPaint(
foregroundPainter: RadialPainter(
bgColor: Colors.grey[200],
lineColor: getColor(context, percent),
percent: percent,
width: 15.0),
child: Center(
child: Text(
'\$${amountLeft.toStringAsFixed(2)} / \$${widget.category.maxAmount}',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
),
),
keep updating the percent value
double sweepAngle=2*pi*percent
on button click
change your code to this
void refresh() async {
setState(() {
isLoading = true;
});
await get.getFromFirestore(id);
await get.showData(data, context);
setState(() {
isLoading = false;
markerList = get.getList();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('title'),
),
body: isLoading ? Center(child: CircularProgressIndicator()) : ListView(here your list),
);
}

How to add an eraser in custom paint?

I'm currently building a painting app with custom paint and every works great, I have a button to clear everything has been painted on the screen, but I'm looking for a normal eraser that can clear any specific line have been drawn not all of them at once how can this be done with custom paint, here is an example code of a painting app near to what I'm working on
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
class Draw extends StatefulWidget {
#override
_DrawState createState() => _DrawState();
}
class _DrawState extends State<Draw> {
Color selectedColor = Colors.black;
Color pickerColor = Colors.black;
double strokeWidth = 3.0;
List<DrawingPoints> points = List();
bool showBottomList = false;
double opacity = 1.0;
StrokeCap strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round;
SelectedMode selectedMode = SelectedMode.StrokeWidth;
List<Color> colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.amber,
Colors.black
];
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
color: Colors.greenAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(Icons.album),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.StrokeWidth)
showBottomList = !showBottomList;
selectedMode = SelectedMode.StrokeWidth;
});
}),
IconButton(
icon: Icon(Icons.opacity),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.Opacity)
showBottomList = !showBottomList;
selectedMode = SelectedMode.Opacity;
});
}),
IconButton(
icon: Icon(Icons.color_lens),
onPressed: () {
setState(() {
if (selectedMode == SelectedMode.Color)
showBottomList = !showBottomList;
selectedMode = SelectedMode.Color;
});
}),
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
setState(() {
showBottomList = false;
points.clear();
});
}),
],
),
Visibility(
child: (selectedMode == SelectedMode.Color)
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: getColorList(),
)
: Slider(
value: (selectedMode == SelectedMode.StrokeWidth)
? strokeWidth
: opacity,
max: (selectedMode == SelectedMode.StrokeWidth)
? 50.0
: 1.0,
min: 0.0,
onChanged: (val) {
setState(() {
if (selectedMode == SelectedMode.StrokeWidth)
strokeWidth = val;
else
opacity = val;
});
}),
visible: showBottomList,
),
],
),
)),
),
body: GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
points.add(DrawingPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeCap
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth));
});
},
onPanStart: (details) {
setState(() {
RenderBox renderBox = context.findRenderObject();
points.add(DrawingPoints(
points: renderBox.globalToLocal(details.globalPosition),
paint: Paint()
..strokeCap = strokeCap
..isAntiAlias = true
..color = selectedColor.withOpacity(opacity)
..strokeWidth = strokeWidth));
});
},
onPanEnd: (details) {
setState(() {
points.add(null);
});
},
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(
pointsList: points,
),
),
),
);
}
getColorList() {
List<Widget> listWidget = List();
for (Color color in colors) {
listWidget.add(colorCircle(color));
}
Widget colorPicker = GestureDetector(
onTap: () {
showDialog(
context: context,
child: AlertDialog(
title: const Text('Pick a color!'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: pickerColor,
onColorChanged: (color) {
pickerColor = color;
},
enableLabel: true,
pickerAreaHeightPercent: 0.8,
),
),
actions: <Widget>[
FlatButton(
child: const Text('Save'),
onPressed: () {
setState(() => selectedColor = pickerColor);
Navigator.of(context).pop();
},
),
],
),
);
},
child: ClipOval(
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
height: 36,
width: 36,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.green, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)),
),
),
);
listWidget.add(colorPicker);
return listWidget;
}
Widget colorCircle(Color color) {
return GestureDetector(
onTap: () {
setState(() {
selectedColor = color;
});
},
child: ClipOval(
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
height: 36,
width: 36,
color: color,
),
),
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pointsList});
List<DrawingPoints> pointsList;
List<Offset> offsetPoints = List();
#override
void paint(Canvas canvas, Size size) {
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);
} else if (pointsList[i] != null && pointsList[i + 1] == null) {
offsetPoints.clear();
offsetPoints.add(pointsList[i].points);
offsetPoints.add(Offset(
pointsList[i].points.dx + 0.1, pointsList[i].points.dy + 0.1));
canvas.drawPoints(PointMode.points, offsetPoints, pointsList[i].paint);
}
}
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
class DrawingPoints {
Paint paint;
Offset points;
DrawingPoints({this.points, this.paint});
}
enum SelectedMode { StrokeWidth, Opacity, Color }
I faced the same issue, what I did was changing the brush color to the background color, you can change the strokeWidth too, you will draw more strokes but it will work as intended.
Remove this line from your code:
showBottomList = false;