I am using a Canvas class to draw lines on the canvas. Now I want to erase line drawn in the canvas in a similar way as we do in our notebook using an eraser. I go through several examples but nothing works for me.
If anyone knows a solution to this problem, please could you help me to solve this?
Java Code:
public class DrawView extends View implements OnTouchListener
{
private Canvas m_Canvas;
private Path m_Path;
private Paint m_Paint;
ArrayList<Pair<Path, Paint>> paths = new ArrayList<Pair<Path, Paint>>();
ArrayList<Pair<Path, Paint>> undonePaths = new ArrayList<Pair<Path, Paint>>();
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private Bitmap bitmapToCanvas;
private CanvasManager m_CanvasManagerObject;
private Paint mBitmapPaint;
public DrawView(Context context)
{
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
onCanvasInitialization();
}
public void onCanvasInitialization()
{
m_Paint = new Paint(Paint.DITHER_FLAG);
m_Paint.setAntiAlias(true);
m_Paint.setDither(true);
m_Paint.setColor(Color.parseColor("#37A1D1"));
m_Paint.setStyle(Paint.Style.STROKE);
m_Paint.setStrokeJoin(Paint.Join.ROUND);
m_Paint.setStrokeCap(Paint.Cap.ROUND);
m_Paint.setStrokeWidth(2);
m_Path = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
bitmapToCanvas = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
m_Canvas = new Canvas(bitmapToCanvas);
}
#Override
protected void onDraw(Canvas canvas)
{
canvas.drawBitmap(bitmapToCanvas, 0f, 0f, mBitmapPaint);
canvas.drawPath(m_Path, m_Paint);
}
public boolean onTouch(View arg0, MotionEvent event)
{
float x = event.getX();
float y = event.getY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
private void touch_start(float x, float y)
{
m_Path.reset();
m_Path.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y)
{
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
{
m_Path.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
}
private void touch_up()
{
m_Path.lineTo(mX, mY);
// commit the path to our offscreen
m_Canvas.drawPath(m_Path, m_Paint);
// kill this so we don't double draw
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
m_Path = new Path();
}
public void onClickEraser()
{
}
}
If you have solid color background all you need to do is set Paint color to your background color. For example, if you have a white background you can do:
paint.setColor(Color.White);
However, if you need to erase a line with a transparent background you try this:
In order to draw with a transparent color you must use Paint setXfermode which will only work if you set a bitmap to your canvas. If you follow the steps below you should get the desired result.
Create a canvas and set its bitmap.
mCanvas = new Canvas();
mBitmap= Bitmap.createBitmap(scrw, scrh, Config.ARGB_8888);
mCanvas.setBitmap(mBitmap);
When you want to erase something you just need to use setXfermode.
public void onClickEraser()
{
if (isEraserOn)
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
else
mPaint.setXfermode(null);
}
Now you should be able draw with a transparent color using:
mCanvas.drawPath(path, mPaint);
on par with the answer of Daniel Albert,
after using:
public void onClickEraser()
{
if (isEraserOn)
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
else
mPaint.setXfermode(null);
}
…you should commit every paint on your touch_move method to avoid solid path . and disable also drawpath if isErase = true
in order to erase, besides the brush color, you need to set the background color too.
please imagine that you are using a mspaint, the eraser itself is "painting" the background color on the canva.
if your background is 000
then the delbrush could be like
delPaint = new Paint();
delPaint.setColor(0x00000000);
delPaint.setXfermode(clear);
delPaint.setAlpha(0x00);
delPaint.setAntiAlias(true);
delPaint.setDither(true);
delPaint.setStyle(Paint.Style.STROKE);
delPaint.setStrokeJoin(Paint.Join.ROUND);
delPaint.setStrokeCap(Paint.Cap.ROUND);
relativeLayout = (RelativeLayout) findViewById(R.id.relativelayout1);
button = (Button)findViewById(R.id.button);
view = new SketchSheetView(slate.this);
paint = new Paint();
path2 = new Path();
relativeLayout.addView(view, new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT));
paint.setDither(true);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(5);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
path2.reset();
relativeLayout.removeAllViewsInLayout();
view = new SketchSheetView(slate.this);
relativeLayout.addView(view, new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT));
}
});
}
private class SketchSheetView extends View {
public SketchSheetView(slate slate) {
super(slate);
bitmap = Bitmap.createBitmap(820, 480, Bitmap.Config.ARGB_4444);
canvas = new Canvas(bitmap);
this.setBackgroundColor(Color.WHITE);
}
ArrayList<DrawingClass> DrawingClassArrayList= new ArrayList<DrawingClass>();
#Override
public boolean onTouchEvent(MotionEvent event) {
DrawingClass pathWithPaint = new DrawingClass();
canvas.drawPath(path2, paint);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
path2.moveTo(event.getX(), event.getY());
path2.lineTo(event.getX(), event.getY());
}
else if (event.getAction() == MotionEvent.ACTION_MOVE) {
path2.lineTo(event.getX(), event.getY());
pathWithPaint.setPath(path2);
pathWithPaint.setPaint(paint);
DrawingClassArrayList.add(pathWithPaint);
}
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (DrawingClassArrayList.size() > 0) {
canvas.drawPath(
DrawingClassArrayList.get(DrawingClassArrayList.size() - 1).getPath(),
DrawingClassArrayList.get(DrawingClassArrayList.size() - 1).getPaint());
}
}
}
public class DrawingClass {
Path DrawingClassPath;
Paint DrawingClassPaint;
public Path getPath() {
return DrawingClassPath;
}
public void setPath(Path path) {
this.DrawingClassPath = path;
}
public Paint getPaint() {
return DrawingClassPaint;
}
public void setPaint(Paint paint) {
this.DrawingClassPaint = paint;
}
}
Related
I am working on a painting app, with undo/redo function and would like to add eraser function.
Code for MainActivity
case R.id.undoBtn:
doodleView.onClickUndo();
break;
case R.id.redoBtn:
doodleView.onClickRedo();
break;
case R.id.eraserBtn:
Constants.mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); break;
DrawView
// Drawing Part
private Bitmap mBitmap;
private Paint mBitmapPaint;
private Canvas mCanvas;
private Path mPath;
private int selectedColor = Color.BLACK;
private int selectedWidth = 5;
private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>();
private Map<Path, Integer> colorsMap = new HashMap<Path, Integer>();
private Map<Path, Integer> widthMap = new HashMap<Path, Integer>();
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
Context context_new;
public DoodleView(Context c, AttributeSet attrs)
{
super(c, attrs);
context_new = c;
setFocusable(true);
setFocusableInTouchMode(true);
setLayerType(View.LAYER_TYPE_SOFTWARE, null); // for solely removing the black eraser
mPath = new Path();
mCanvas = new Canvas();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
Constants.mPaint = new Paint();
Constants.mPaint.setAntiAlias(true); // smooth edges of drawn line
Constants.mPaint.setDither(true);
Constants.mPaint.setColor(Color.BLACK); // default color is black
Constants.mPaint.setStyle(Paint.Style.STROKE); // solid line
Constants.mPaint.setStrokeJoin(Paint.Join.ROUND);
Constants.mPaint.setStrokeWidth(20); // set the default line width
Constants.mPaint.setStrokeCap(Paint.Cap.ROUND); // rounded line ends
Constants.mPaint.setXfermode(null);
Constants.mPaint.setAlpha(0xFF);
}
#Override
public void onSizeChanged(int w, int h, int oldW, int oldH)
{
super.onSizeChanged(w, h, oldW, oldH);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Constants.DRAW_W = w;
Constants.DRAW_H = h;
Log.d("TAG", "onSizeChanged!!!" + Constants.DRAW_W + Constants.DRAW_H + Constants.SCREEN_W + Constants.SCREEN_H);
// bitmap.eraseColor(Color.WHITE); // erase the BitMap with white
}
#Override
protected void onDraw(Canvas canvas)
{
canvas.drawBitmap(mBitmap, 0, 0, null); // draw the background screen
for (Path p : paths)
{
Constants.mPaint.setColor(colorsMap.get(p));
Constants.mPaint.setStrokeWidth(widthMap.get(p));
canvas.drawPath(p, Constants.mPaint);
}
Constants.mPaint.setColor(selectedColor);
Constants.mPaint.setStrokeWidth(selectedWidth);
canvas.drawPath(mPath, Constants.mPaint);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
float x = event.getX();
float y = event.getY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
private void touch_start(float x, float y)
{
undonePaths.clear();
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y)
{
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
{
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
startdrawing = true;
}
}
private void touch_up()
{
mPath.lineTo(mX, mY);
mCanvas.drawPath(mPath, Constants.mPaint);
paths.add(mPath);
colorsMap.put(mPath,selectedColor);
widthMap.put(mPath,selectedWidth);
mPath = new Path();
}
public void onClickUndo()
{
if (paths.size()>0)
{
undonePaths.add(paths.remove(paths.size()-1));
invalidate();
}
else Toast.makeText(getContext(), R.string.toast_nothing_to_undo, Toast.LENGTH_SHORT).show();
}
public void onClickRedo()
{
if (undonePaths.size()>0)
{
paths.add(undonePaths.remove(undonePaths.size()-1));
invalidate();
}
else
Toast.makeText(getContext(), R.string.toast_nothing_to_redo, Toast.LENGTH_SHORT).show();
}
public void setDrawingColor(int color)
{
selectedColor = color;
Constants.mPaint.setColor(color);
}
public int getDrawingColor()
{
return Constants.mPaint.getColor();
}
Question:
Normal painting and undo / redo can be performed perfectly. However, the eraser does not work.
After pressing (touch_start) the eraser button and touch the screen, all the previous drawn line immediately turn black.
When using the eraser upon touch_move , the eraser itself is drawing black lines, even though it was on CLEAR mode.
Upon touch_up, All other drawn lines maintained at black color. The "erased" area was also in black color. However, when drawing a new path subsequently, the original line turned to their original color, the area "erased" by the eraser turn its color to the last color chosen and the paths retained in the View.
How could the code on eraser be written in a proper way? (keeping undo / redo)
Thanks a lot in advance!
It's hard to answer your question, but. I'd implement the erase functionality as follows:
The erase function will by simple white path. So the erase mode means that you are drawing white path which will be wider than the drawing path. This way you will be able to select even the width of the eraser and the undo/redo functionality will stay the same.
Try this type of application with samsung spen sdk, all this funtions are simplified there.
SPen Sdk Tutorial
or
Make your erase paint color to your background color.
// Please use this is erasure concept . This is working and tested.
public void addErasure(){
drawView.setErase(true);
drawView.setBrushSize(20);
}
public void addPencil(){
drawView.setErase(false);
drawView.setBrushSize(20);
drawView.setLastBrushSize(20);
}
/// this my drawing view. you can add this view into your main layout.
public class DrawingView extends View {
//drawing path
private Path drawPath;
//drawing and canvas paint
private Paint drawPaint, canvasPaint;
//initial color
private int paintColor = 0xFF660000;
//canvas
private Canvas drawCanvas;
//canvas bitmap
private Bitmap canvasBitmap;
//brush sizes
private float brushSize, lastBrushSize;
//erase flag
private boolean erase=false;
private boolean isFirstTime = false;
public DrawingView(Context context, AttributeSet attrs){
super(context, attrs);
//setBackgroundColor(Color.CYAN);
setupDrawing();
}
//setup drawing
private void setupDrawing(){
//prepare for drawing and setup paint stroke properties
brushSize = getResources().getInteger(R.integer.medium_size);
lastBrushSize = brushSize;
drawPath = new Path();
drawPaint = new Paint();
drawPaint.setColor(paintColor);
drawPaint.setAntiAlias(true);
drawPaint.setStrokeWidth(brushSize);
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeJoin(Paint.Join.ROUND);
drawPaint.setStrokeCap(Paint.Cap.ROUND);
canvasPaint = new Paint(Paint.DITHER_FLAG);
//canvasPaint.setColor(Color.GREEN);
}
//size assigned to view
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Bitmap canvasBackGroundBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
canvasBackGroundBitmap = getResizedBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.pop_up_big_bg),h,w);
// drawCanvas = new Canvas(canvasBackGroundBitmap);
// drawCanvas.drawColor(Color.GREEN);
setBackgroundDrawable(new BitmapDrawable(canvasBackGroundBitmap));
drawCanvas = new Canvas(canvasBitmap);
}
public static Bitmap getResizedBitmap(Bitmap bm, int newHeight, int newWidth) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// CREATE A MATRIX FOR THE MANIPULATION
Matrix matrix = new Matrix();
// RESIZE THE BIT MAP
matrix.postScale(scaleWidth, scaleHeight);
// "RECREATE" THE NEW BITMAP
Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
return resizedBitmap;
}
//draw the view - will be called after touch event
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
}
//register user touches as drawing action
#Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
//respond to down, move and up events
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
drawPath.moveTo(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
drawPath.lineTo(touchX, touchY);
drawCanvas.drawPath(drawPath, drawPaint);
drawPath.reset();
drawPath.moveTo(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
//drawPath.lineTo(touchX, touchY);
//drawCanvas.drawPath(drawPath, drawPaint);
drawPath.reset();
break;
default:
return false;
}
//redraw
invalidate();
return true;
}
//update color
public void setColor(String newColor){
invalidate();
paintColor = Color.parseColor(newColor);
drawPaint.setColor(paintColor);
}
//set brush size
public void setBrushSize(float newSize){
float pixelAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
newSize, getResources().getDisplayMetrics());
brushSize=pixelAmount;
drawPaint.setStrokeWidth(brushSize);
}
//get and set last brush size
public void setLastBrushSize(float lastSize){
lastBrushSize=lastSize;
}
public float getLastBrushSize(){
return lastBrushSize;
}
//set erase true or false
public void setErase(boolean isErase){
erase=isErase;
if(erase) {
drawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}else {
drawPaint.setXfermode(null);
}
}
//start new drawing
public void startNew(){
drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
invalidate();
}
}
Try this solution - works great with undo/redo as well:
private float blur = 0F;
case R.id.eraserBtn:
Constants.mPaint.setColor(Color.WHITE); // or whatever color to match canvas color
Constants.mPaint.setShadowLayer(this.blur, 0F, 0F, Color.WHITE);
With this solution, no need to set:
Constants.mPaint.setXfermode(null);
I'm writing a subclass of EditText that allows the user to draw with his finger on it. For that, I created a boolean called drawing, that if it's true put the EditText on drawing mode and the keyboard it's not opened, and if it's false allow the user to open the keyboard and write on keeping his drawing that he does with the drawing mode.
The code for the drawing is based on the Google Android Example called FingerPaint. Before this intent to implement this into an EditText, the code was implemented on a subclass of view, and worked perfectly, but the problem was that combining an EditText and that view in a layout reduces the performance was really bad.
There's a lot of code to do this, because can be used to implement two different mask filters that add some lines to this code that are not used locally in this class.
My problem is that when I override the onDraw(), and drawing is false, the texts it's not showed to ther user, but the keyboard and the writing line (|) are showed.
Here my code:
import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.EditText;
public class DrawEditText extends EditText {
private Bitmap mBitmap;
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;
private Paint mPaint;
private MaskFilter mEmboss;
private MaskFilter mBlur;
private Boolean drawing;
public DrawEditText(Context c) {
super(c);
//This initializes all the objects
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
mEmboss = new EmbossMaskFilter(new float[] { 1, 1, 1 },
0.4f, 6, 3.5f);
mBlur = new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL);
drawing = true;
}
public DrawEditText(Context c, AttributeSet attrs) {
super(c, attrs);
//This initializes all the objects
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
mEmboss = new EmbossMaskFilter(new float[] { 1, 1, 1 },
0.4f, 6, 3.5f);
mBlur = new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL);
drawing = true;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, mPaint);
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
}
private void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
mPath.reset();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
//This conditional makes that if not it's drawing no points are saved and no points are drawed
if (drawing){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
} else {
return super.onTouchEvent(event);
}
}
public void changePaint(int stroke, int color){
mPaint.setColor(color);
mPaint.setStrokeWidth(stroke);
}
public void clear(){
mCanvas.drawColor(0x00AAAAAA);
mCanvas.drawColor( 0, PorterDuff.Mode.CLEAR );
}
public void changeBrush(int id){
//String[] ids={"emboss", "blur", "another"};
switch (id) {
case 0:
mPaint.setMaskFilter(mEmboss);
break;
case 1:
mPaint.setMaskFilter(mBlur);
break;
case 2:
mPaint.setMaskFilter(null);
break;
default:
mPaint.setMaskFilter(null);
break;
}
}
public void eraser(){
mPaint.setMaskFilter(null);
mPaint.setColor(0x00AAAAAA);
}
public void setDrawing(Boolean drawing){
this.drawing = drawing;
}
public Boolean isDrawing(){
return drawing;
}
}
Just give the EditText a chance to draw its own content:
#Override
protected void onDraw(Canvas canvas) {
if(drawing) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawPath(mPath, mPaint);
} else {
super.onDraw(canvas);
}
}
I got this code from a answer in one of the questions that was asking how to draw in Android, but then when using it and testing it in my app, I found out that it's not efficient when drawing big things or many paths. The problem comes from the code inside onDraw because each time invalidate() is called onDraw is called which contains a loop that draws all paths again to the canvas, and by adding more paths to it, it gets very very slow.
Here is the Class:
public class DrawingView extends View implements OnTouchListener {
private Canvas m_Canvas;
private Path m_Path;
private Paint m_Paint;
ArrayList<Pair<Path, Paint>> paths = new ArrayList<Pair<Path, Paint>>();
ArrayList<Pair<Path, Paint>> undonePaths = new ArrayList<Pair<Path, Paint>>();
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
public static boolean isEraserActive = false;
private int color = Color.BLACK;
private int stroke = 6;
public DrawingView(Context context, AttributeSet attr) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
setBackgroundColor(Color.WHITE);
this.setOnTouchListener(this);
onCanvasInitialization();
}
public void onCanvasInitialization() {
m_Paint = new Paint();
m_Paint.setAntiAlias(true);
m_Paint.setDither(true);
m_Paint.setColor(Color.parseColor("#000000"));
m_Paint.setStyle(Paint.Style.STROKE);
m_Paint.setStrokeJoin(Paint.Join.ROUND);
m_Paint.setStrokeCap(Paint.Cap.ROUND);
m_Paint.setStrokeWidth(2);
m_Canvas = new Canvas();
m_Path = new Path();
Paint newPaint = new Paint(m_Paint);
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
#Override
public void setBackground(Drawable background) {
mBackground = background;
super.setBackground(background);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
public boolean onTouch(View arg0, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
#Override
protected void onDraw(Canvas canvas) {
for (Pair<Path, Paint> p : paths) {
canvas.drawPath(p.first, p.second);
}
}
private void touch_start(float x, float y) {
if (isEraserActive) {
m_Paint.setColor(Color.WHITE);
m_Paint.setStrokeWidth(50);
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
} else {
m_Paint.setColor(color);
m_Paint.setStrokeWidth(stroke);
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
m_Path.reset();
m_Path.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
m_Path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
m_Path.lineTo(mX, mY);
// commit the path to our offscreen
m_Canvas.drawPath(m_Path, m_Paint);
// kill this so we don't double draw
m_Path = new Path();
Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
paths.add(new Pair<Path, Paint>(m_Path, newPaint));
}
public void onClickUndo() {
if (!paths.isEmpty()) {//paths.size() > 0) {
undonePaths.add(paths.remove(paths.size() - 1));
undo = true;
invalidate();
}
}
public void onClickRedo() {
if (!undonePaths.isEmpty()){//undonePaths.size() > 0) {
paths.add(undonePaths.remove(undonePaths.size() - 1));
undo = true;
invalidate();
}
}}
But I searched on the internet again to find a better way for drawing, so I found the following:
1 Add the following to the constructor:
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
2 Override onSizeChanged with the following code:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
m_Canvas = new Canvas(mBitmap);
}
3 put this in onDraw:
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty())
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
}
This approach works and it doesn't slow down the view, but the problem with this approach is that I can't have undo and redo functionalities.
I tried many many things to do the undo and redo with the second approach, but I couldn't do it. So what I'm asking here is one of three things:
1. A way to do undo and redo with the second approach
2. Another approach that makes it possible to do undo and redo
3. A whole new class that has everything already done, like an open source library or something.
Please help if you can.
Thanks
EDIT 1
OK, so I limited it down to this and then I couldn't do anything more, I have been trying for over 8 hours now. It works up until undo (you can undo as many paths as you want), then when drawing again all remaining paths disappear, I don't know what makes it do that.
#Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null)
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty() && !undo)
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
if (undo) {
setBackground(mBackground);
for (Pair<Path, Paint> p : paths)
canvas.drawPath(p.first, p.second);
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
m_Canvas = new Canvas(mBitmap);
undo = false;
}
}
so basically what I did is use the first approach at first (before undo is called), then if undo is clicked, undo is set to true and the code under if (undo) is executed which is actually the first approach (calculating all paths again), then I draw the result of calculating all paths again into mBitmap so whenever the onDraw is called again it draws on top of that, but that part is still needs working, I hope someone can help with that part.
The way to handle such a case is to have a Bitmap that has the size of the view. On touch events, draw into the bitmap's canvas. in onDraw, just draw the bitmap into the canvas at 0,0. For undo/redo,. you can erase the bitmap and re-draw all the paths. It make take a bit longer, but it happens only once per undo/redo.
If users typically do one undo/redo. you can optimize by having another bitmap for just one step back.
Ok, here is what I came up with at the end, the problem was that I draw the paths to the canvas before creating the bitmap on undo, which lead to loss of the paths onDraw after undo:
#Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null)
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (!paths.isEmpty()) {
canvas.drawPath(paths.get(paths.size() - 1).first, paths.get(paths.size() - 1).second);
}
}
public void onClickUndo() {
if (paths.size() >= 2) {
undonePaths.add(paths.remove(paths.size() - 2));
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
m_Canvas = new Canvas(mBitmap);
for (Pair<Path, Paint> p : paths)
m_Canvas.drawPath(p.first, p.second);
invalidate();
}
}
public void onClickRedo() {
if (undonePaths.size() >= 2){
paths.add(undonePaths.remove(undonePaths.size() - 2));
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
m_Canvas = new Canvas(mBitmap);
for (Pair<Path, Paint> p : paths)
m_Canvas.drawPath(p.first, p.second);
invalidate();
}
}
Drawing all paths again and again is still there but not in onDraw(), which improves the performance of drawing quite very much.
But the user might experience little bit of delay in onClickUndo() and onClickRedo() if he has drawn a lot of paths because there where the paths are getting drawn again from scratch, but just one time per click.
I am not sure if this is the best way for undo and redo. However the below worked on my device (Samsung galaxy s3). The Draw seems to be fast and the undo works fine. I do think the below can be modified to further enhance performance.
public class MainActivity extends Activity {
MyView mv;
LinearLayout ll;
private ArrayList<Path> undonePaths = new ArrayList<Path>();
private ArrayList<Path> paths = new ArrayList<Path>();
Button b;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mv= new MyView(this);
mv.setDrawingCacheEnabled(true);
ll= (LinearLayout) findViewById(R.id.ll);
ll.addView(mv);
b= (Button) findViewById(R.id.button1);
b.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (paths.size() > 0) {
undonePaths.add(paths
.remove(paths.size()-2));
mv.invalidate();
}
}
});
}
public class MyView extends View implements OnTouchListener {
private Canvas mCanvas;
private Path mPath;
private Paint mPaint;
// private ArrayList<Path> undonePaths = new ArrayList<Path>();
private float xleft, xright, xtop, xbottom;
public MyView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
this.setOnTouchListener(this);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPath = new Path();
paths.add(mPath);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
protected void onDraw(Canvas canvas) {
for (Path p : paths) {
canvas.drawPath(p, mPaint);
}
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 0;
private void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
mPath = new Path();
paths.add(mPath);
}
#Override
public boolean onTouch(View arg0, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<LinearLayout
android:id="#+id/ll"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:orientation="vertical" >
</LinearLayout>
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Undo" />
</LinearLayout>
In my Paint App I have implemented UNDO function and it's working fine. But if a change the paint brush color (or) paint brush stroke then all my previous drawn paths changing to the new paint color. The code is as follows:
public class CustomView extends View implements OnTouchListener {
public Canvas mCanvas;
private Path mPath;
public Paint mPaint, mBitmapPaint;
Bitmap mBitmap;
Canvas canvas;
public ArrayList<Path> paths = new ArrayList<Path>();
public ArrayList<Path> undonePaths = new ArrayList<Path>();
private Bitmap im;
public CustomView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
setOnTouchListener(this);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(0xFFFFFFFF);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPath = new Path();
im = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher);
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
int w = metrics.widthPixels;
int h = metrics.heightPixels;
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
// mBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
protected void onDraw(Canvas canvas) {
// mPath = new Path();
// canvas.drawPath(mPath, mPaint);
canvas.drawColor(Color.TRANSPARENT);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
for (Path p : paths) {
canvas.drawPath(p, mPaint);
}
canvas.drawPath(mPath, mPaint);
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private void touch_start(float x, float y) {
undonePaths.clear();
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
paths.add(mPath);
mPath = new Path();
}
public void onClickUndo() {
if (paths.size() > 0) {
undonePaths.add(paths.remove(paths.size() - 1));
invalidate();
} else {
}
// toast the user
}
public void onClickRedo() {
if (undonePaths.size() > 0) {
paths.add(undonePaths.remove(undonePaths.size() - 1));
invalidate();
} else {
}
// toast the user
}
#Override
public boolean onTouch(View arg0, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
}
This is an old question, but I wanted to leave an answer for anyone that comes here. The solution is to keep track of separate Paint instances for each stroke (there should be exactly 1 Paint object per Path object).
Change your touch_up method to reset the mPaint, in addition to resetting the mPath.
private void touch_up() {
mPath.lineTo(mX, mY);
mCanvas.drawPath(mPath, mPaint);
paths.add(mPath);
mPath = new Path();
mPaints.add(mPaint)
// Creates a new Paint reference, but keeps the previous state (color, width, etc.)
mPaint = new Paint(mPaint);
}
Then your onDraw method looks something like this.
for (int i = 0; i < mPaths.size(); i++) {
canvas.drawPath(mPaths.get(i), mPaints.get(i));
}
I have an activity which has an ImageView in it. What I want to do is be able to draw where the user touches that Imageview with an image from the drawable folder. I've read that the best way is to use Canvas, but I'm not sure where and how I integrate the onDraw method with the onTouchListener. This is what I have so far:
public class Main extends Activity
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final TextView textView = (TextView)findViewById(R.id.textView);
final ImageView image = (ImageView) findViewById(R.id.imageView2);
//Bitmap
Bitmap viewBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(viewBitmap);
image.draw(canvas);
image.setOnTouchListener(new View.OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event)
{
textView.setText("Touch coordinates : " + String.valueOf(event.getX()) + "x" + String.valueOf(event.getY()));
return false;
}
});
}
}
So what I want to do is when the user touches the ImageView, an image will be drawn exactly where he touched it.
You're going to want to subclass ImageView in order to override its onDraw() method. By doing so, you can also to the custom touch handling in onTouchEvent() instead of attaching a listener. This is not a complete example, but something like the following:
public class CustomImageView extends ImageView {
private ArrayList<Point) mTouches;
private Bitmap mMarker;
//Java constructor
public CustomImageView(Context context) {
super(context);
init();
}
//XML constructor
public CustomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mTouches = new ArrayList<Point>();
mMarker = BitmapFactory.decodeResource(context.getResources(), R.drawable.my_marker_image);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
//Capture a reference to each touch for drawing
if(event.getAction() == MotionEvent.ACTION_DOWN) {
mTouches.add( new Point(event.getX(), event.getY()) );
return true;
}
return super.onTouchEvent(event);
}
#Override
protected void onDraw(Canvas c) {
//Let the image be drawn first
super.onDraw(c);
//Draw your custom points here
Paint paint = new Paint();
for(Point p : mTouches) {
c.drawBitmap(mMarker, p.x, p.y, paint);
}
}
}
HTH!
I edited your class like this and it works for me
public class MyImageView extends androidx.appcompat.widget.AppCompatImageView {
private Bitmap mMarker;
private Path mPath, circlePath ;
private Paint paint, circlePaint;
private Canvas mCanvas;
//Java constructor
public MyImageView(Context context) {
super(context);
init();
}
//XML constructor
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mTouches = new ArrayList<Point>();
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(12);
mPath = new Path();
Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
circlePaint = new Paint();
circlePath = new Path();
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStrokeJoin(Paint.Join.MITER);
circlePaint.setStrokeWidth(4f);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMarker = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mMarker);
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
circlePath.reset();
circlePath.addCircle(mX, mY, 30, Path.Direction.CW);
}
}
private void touch_up() {
mPath.lineTo(mX, mY);
circlePath.reset();
// commit the path to our offscreen
mCanvas.drawPath(mPath, paint);
// kill this so we don't double draw
mPath.reset();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
#Override
protected void onDraw(Canvas c) {
//Let the image be drawn first
super.onDraw(c);
c.drawBitmap(mMarker, 0, 0, paint);
c.drawPath( mPath, paint);
c.drawPath( circlePath, circlePaint);
}
}