Reputation: 1293
I have googled this problem and had various solutions proposed.
However, none worked for me.
I have a Drawing Canvas in an app.
The background of the canvas is set to a png Image in the Activity which uses the custom view (drawView);
Bundle extras = intent.getExtras();
if (extras != null) {
if (extras.containsKey("background")) {
//set the background to the resource in the extras
int imageResource = intent.getIntExtra("background",-1);
Drawable image = getResources().getDrawable(imageResource);
drawView.setBackground(image);
}
}
In the DrawingView class (drawview is the instance), I store the paths drawn in a collection of PathPaints, which has 3 properties (the path, the paint used and if it was an eraser);
private ArrayList<PathPaint> paths = new ArrayList<PathPaint>();
I then attempt to loop through these paths in OnDraw and redraw them each time with the paints that they were drawn with (mutiple colours);
protected void onDraw(Canvas canvas) {
//if the drawing is new - dont draw any paths
if (isNew != true) {
//go through every previous path and draw them
for (PathPaint p : paths) {
if (!p.isErase)
{
canvas.drawPath(p.myPath, p.myPaint);
}
else
{
//Paint eraserPaint = setDefaultPaint();
//eraserPaint.setAlpha(0xFF);
//eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
//eraserPaint.setColor(Color.TRANSPARENT);
//canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(p.myPath, p.myPaint);
}
canvas.drawBitmap(canvasBitmap, 0, 0, null);
}
}
I have tried lots of the proposed options online, but to no avail.
I have tried setting the paint on the drawpath to have all the various commented out properties set.
I have tried drawing on a bitmap and then loading that bitmap to the canvas (canvas.drawBitmap(canvasBitmap, 0, 0, null))
I have turned off hardware acceleration in this class' constructor
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
but either the line is not drawn or when the collection is redrawing the paths, the eraser draws a black line;
What is interesting is that if I perform the erasing using the bitmap without the loop aspect - the eraser works as expected;
//If we are making a new drawing we don't want to go through all the paths
if (isNew != true && erase ==false) {
//go through every previous path and draw them
for (PathPaint p : paths) {
if (!p.isErase)
{
canvas.drawPath(p.myPath, p.myPaint);
}
//this section now takes place in the elseIF
else
{
Paint eraserPaint = setDefaultPaint();
eraserPaint.setAlpha(0xFF);
eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
eraserPaint.setColor(Color.TRANSPARENT);
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(p.myPath, p.myPaint);
}
}
}
else if (isNew != true && erase ==true)
{
//This works correctly for Erasing but I dont have the ability to Undo/Redo with this approach!
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
}
This, however, is a problem since I want to be able to Undo/Redo erasing (thus the point of the collection)
Can anyone please help me?
Upvotes: 8
Views: 3124
Reputation: 209
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});
}
Upvotes: 0
Reputation: 1852
It looks like you only use one view (layer) where you first put a background image and then draw the paths, which replace the background. If so, when you erase, you are removing from that one and only view/layer, which includes the paths and the background. If you use two layers (two views inside a Framelayout), one in the back where you would load the background, and one in the front where you put all the paths, then erasing on the top layer only removes the paths and the background would come through.
There are different ways of doing the layering. As an example, this FrameLayout replaces the view that currently holds the background and the drawn paths (refer to as XXXView in the code.)
<FrameLayout
android:layout_width= ...copy from the existing XXXView ...
android:layout_height= ...copy from the existing XXXView ... >
<ImageView
android:id = "@+id/background"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
...
... the background is loaded here />
<XXXView (this the existing view where the paths are drawn onto)
android:layout_width="fill_parent"
android:layout_height="fill_parent"
...
... no background here />
</FrameLayout>
Upvotes: 3
Reputation: 87
Check your canvas bitmap, is it Config.ARGB8888?
Also check this answer Android canvas: draw transparent circle on image
I think your drawView is ImageView, isn't right?
Upvotes: 0