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);
Related
I have a custom ImageView where user can draw using some colors.
I load image from server then allow user to draw on top of it.
When i load the next image , i want the previously drawn colors to be gone before user can draw again.
I tried many thing from internet but not able to achieve the same.
below is code for custom ImageView:
public class DrawingView extends androidx.appcompat.widget.AppCompatImageView {
//drawing path
static Path drawPath;
//drawing and canvas paint
private Paint drawPaint, canvasPaint;
//initial color
static int paintColor = 0xFFFFFFFF;
//stroke width
private float STROKE_WIDTH = 5f;
//canvas
private Canvas drawCanvas;
//canvas bitmap
private Bitmap canvasBitmap;
//eraser mode
private boolean erase = false;
private boolean cc = false;
//constructor
public DrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
setupDrawing();
setErase(erase);
}
private void setupDrawing() {
drawPath = new Path();
drawPaint = new Paint();
drawPaint.setColor(paintColor);
drawPaint.setAntiAlias(true);
drawPaint.setStrokeWidth(STROKE_WIDTH);
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeJoin(Paint.Join.ROUND);
drawPaint.setStrokeCap(Paint.Cap.ROUND);
canvasPaint = new Paint(Paint.DITHER_FLAG);
}
//*************************************** View assigned size ****************************************************
#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);
drawCanvas = new Canvas(canvasBitmap);
}
public void setPaintColor(int paintColor) {
this.paintColor = paintColor;
setupDrawing();
}
public void setImageOnImageview(Bitmap canvasBitmap) {
this.canvasBitmap = canvasBitmap;
}
#Override
public void setImageResource(int resId) {
super.setImageResource(resId);
}
public void setErase(boolean isErase) {
erase = isErase;
drawPaint = new Paint();
if (erase) {
setupDrawing();
int srcColor = getResources().getColor(R.color.white);
PorterDuff.Mode mode = PorterDuff.Mode.CLEAR;
PorterDuffColorFilter porterDuffColorFilter = new PorterDuffColorFilter(srcColor, mode);
drawPaint.setColorFilter(porterDuffColorFilter);
drawPaint.setColor(srcColor);
drawPaint.setXfermode(new PorterDuffXfermode(mode));
} else {
setupDrawing();
}
}
//************************************ draw view *************************************************************
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cc) {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
cc = false;
} else {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
}
}
public void clearCanvas() {
setupDrawing();
setErase(false);
cc = true;
invalidate();
}
//*************************** respond to touch interaction **************************************************
#Override
public boolean onTouchEvent(MotionEvent event) {
canvasPaint.setColor(paintColor);
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:
drawCanvas.drawPath(drawPath, drawPaint);
drawPath.lineTo(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;
}
//*********************************** return current alpha ***********************************************
public int getPaintAlpha() {
return Math.round((float) STROKE_WIDTH / 255 * 100);
}
//************************************** set alpha ******************************************************
public void setPaintAlpha(int newAlpha) {
STROKE_WIDTH = Math.round((float) newAlpha / 100 * 255);
drawPaint.setStrokeWidth(newAlpha);
}
}
I tried many solutions like clear path or clear paint.
Could any please help me with this ?
I assume you want to remove the drawn color on first image upon loading the new image
I think you can draw the color over canvas
Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
or reset the path itself
Path.reset();
I want to implement a canvas drawing application with custom brush & undo/redo operation. First of all my code works perfectly without using the custom brush (including the undo/redo operation). According to this answer How to make custom brush for canvas in android? I used simple image spikes for bitmap draw.
Now the issue are,
The undo, redo operation doesn't works, custom brush paints over and over again whenever moving the touch points.
Q: How to make the undo/redo operation work?
The custom brush stokers aren't smooth as they should. Right now they looks rough and artificial.
Q. How to make the painting smooth and natural with custom brush strokes?
Check my sample code here,
public class DrawingView extends View {
private Context ctx;
private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>();
private Map<Path, Float> brushMap = new HashMap<Path, Float>();
private Map<Path, List<Vector2>> customBrushMap = new HashMap<Path, List<Vector2>>();
private Bitmap mBitmapBrush;
private Vector2 mBitmapBrushDimensions;
private List<Vector2> mPositions = new ArrayList<Vector2>(100);
private boolean isCustomBrush = false;
private int selectedColor;
private float brushSize, lastBrushSize;
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private Path drawPath;
private Paint drawPaint, canvasPaint;
private int paintColor = 0xFF660000, paintAlpha = 255;
private Canvas drawCanvas;
private Bitmap canvasBitmap;
private static final class Vector2 {
public Vector2(float x, float y) {
this.x = x;
this.y = y;
}
public final float x;
public final float y;
}
public DrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
ctx = context;
setupDrawing();
}
private void setupDrawing() {
brushSize = getResources().getInteger(R.integer.small_size);
lastBrushSize = brushSize;
drawPath = new Path();
drawPaint = new Paint();
drawPaint.setColor(paintColor);
drawPaint.setAntiAlias(true);
drawPaint.setDither(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);
}
#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);
drawCanvas = new Canvas(canvasBitmap);
}
private void touch_start(float x, float y) {
undonePaths.clear();
drawPath.reset();
drawPath.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) {
drawPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
customBrushMap.put(drawPath, mPositions);
}
private void touch_up() {
drawPath.lineTo(mX, mY);
drawCanvas.drawPath(drawPath, drawPaint);
paths.add(drawPath);
brushMap.put(drawPath, brushSize);
drawPath = new Path();
drawPath.reset();
invalidate();
}
public void onClickUndo() {
if (paths.size() > 0) {
undonePaths.add(paths.remove(paths.size() - 1));
invalidate();
}
}
public void onClickRedo() {
if (undonePaths.size() > 0) {
paths.add(undonePaths.remove(undonePaths.size() - 1));
invalidate();
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
//detect user touch
float x = event.getX();
float y = event.getY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
if (isCustomBrush) {
mPositions.add(new Vector2(x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2));
}
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_POINTER_DOWN:
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
for (Path p : paths) {
drawPaint.setColor(colorsMap.get(p));
drawPaint.setShader(shaderMap.get(p));
drawPaint.setStrokeWidth(brushMap.get(p));
drawPaint.setAlpha(opacityMap.get(p));
if (isCustomBrush) {
if (customBrushMap.get(p) != null) {
for (Vector2 pos : customBrushMap.get(p)) {
Paint paint = new Paint();
ColorFilter filter = new PorterDuffColorFilter(selectedColor, PorterDuff.Mode.SRC_IN);
paint.setColorFilter(filter);
canvas.drawBitmap(mBitmapBrush, pos.x, pos.y, paint);
}
}
} else {
canvas.drawPath(p, drawPaint);
drawPaint.setColor(selectedColor);
drawPaint.setStrokeWidth(brushSize);
canvas.drawPath(drawPath, drawPaint);
}
}
canvas.restore();
}
public void setCustomBrush(Activity activity, String customBrush) {
isCustomBrush = true;
invalidate();
int patternID = getResources().getIdentifier(customBrush, "drawable", "com.androidapp.drawingstutorial");
mBitmapBrush = BitmapFactory.decodeResource(getResources(), patternID);
mBitmapBrushDimensions = new Vector2(mBitmapBrush.getWidth(), mBitmapBrush.getHeight());
}
}
To make your brush look "smoother" not 100% sure what is meant by that, but you would need to tighten up the dots on the image you are using for your custom brush. Making them more compact will make the edges look smoother.
As for undo redo, in your touchUp method your never adding to customBrushMap only brushMap. So in the ondraw there is nothing to draw since that map will always be empty.
I am developing drawing module for my app. All features working fine. The problem is, everytime when I change the color, or set the brushSize or change to eraser, the first touch on the screen will draw with the previous settings. The second touch then will have the newest changes. Same goes to undo. Nothing change on first click. For second click and so on, it is working fine. This is my reference. The sample app in the reference also has the same problem. This is my code:
public class DrawingView extends View {
//drawing path
private Path drawPath;
private Point pointDraw;
//drawing and canvas paint
private Paint drawPaint, canvasPaint;
//initial color
private int paintColor;
//canvas
private Canvas drawCanvas;
//canvas bitmap
private Bitmap canvasBitmap;
//brush sizes
private float brushSize, lastBrushSize;
//private int defaultBrush = 10;
//erase flag
private boolean erase=false;
private int tempColor;
private boolean checkDraw = false;
private ArrayList<PathPoints> paths = new ArrayList<PathPoints>();
private ArrayList<PathPoints> undonePaths = new ArrayList<PathPoints>();
private ArrayList<Point> tempCanvas = new ArrayList<Point>();
private int countPoint = 0;
int x,y;
public DrawingView(Context context, AttributeSet attrs){
super(context, attrs);
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);
paths.add(new PathPoints(drawPath, paintColor, brushSize, false));
drawCanvas = new Canvas();
}
//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);
drawCanvas = new Canvas(canvasBitmap);
}
//draw the view - will be called after touch event
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
//canvas.drawPath(drawPath, drawPaint);
for (PathPoints p : paths) {
drawPaint.setColor(p.getColor());
drawPaint.setStrokeWidth(p.getStrokeSize());
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(p.getPath(), drawPaint);
}
setCheckDraw(true);
}
public int getArr()
{
return paths.size();
}
public boolean isCheckDraw() {
return checkDraw;
}
public void setCheckDraw(boolean checkDraw) {
this.checkDraw = checkDraw;
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 0;
private void touch_start(float x, float y) {
drawPath.reset();
drawPath.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) {
drawPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
drawPath.lineTo(mX, mY);
// commit the path to our offscreen
drawCanvas.drawPath(drawPath, drawPaint);
// kill this so we don't double draw
drawPath = new Path();
paths.add(new PathPoints(drawPath, paintColor, brushSize, false));
}
//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:
touch_start(touchX, touchY);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
touch_up();
//invalidate();
break;
}
//redraw
invalidate();//do not call frequently
return true;
}
public void validateDraw()
{
invalidate();
}
//update color
public void setColor(String newColor){
//invalidate();
paintColor = Color.parseColor(newColor);
drawPaint.setColor(paintColor);
}
public void setColor(int newColor){
//invalidate();
paintColor = newColor;
drawPaint.setColor(paintColor);
tempColor = paintColor;
}
//set brush size
public void setBrushSize(float newSize){
float pixelAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
newSize, getResources().getDisplayMetrics());
brushSize=pixelAmount;
}
//get and set last brush size
public void setLastBrushSize(float lastSize){
lastBrushSize=lastSize;
}
public float getLastBrushSize(){
drawPath.reset();
return lastBrushSize;
}
//set erase true or false
public void setErase(boolean isErase)
{
//drawPath.reset();
erase=isErase;
if(erase)
this.setColor("#FFFFFFFF");
else
//drawPaint.setXfermode(null);
this.setColor(tempColor);
}
//// new drawing
public void startNew(){
//drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
paths.clear();
undonePaths.clear();
invalidate();
}
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
}
Thank in advance
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>
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;
}
}