So I would like to implement an undo button for a drawing app I am working on for android. My current idea is to put the current draw path into a list of paths on MotionEvent.ACTION_UP. In onDraw(), simply draw everything that is in the list of paths. When the user presses undo, you remove the last drawn path from the list of paths and call invalidate(), forcing onDraw() to be called, which will draw everything in the list of paths. Since we removed the previous path from the list, that path should not be drawn and therefore should be "undone".
The problem I am having is the path never seems to actually be undone. It seems like the logic in my head is correct, but this implementation does not work correctly. Any help will be greatly appreciated.
Here is my code:
DrawingView.java:
Instance variables (to clarify following methods):
private Context context;
private Path drawPath;
private Paint drawPaint;
private Paint canvasPaint;
private Canvas drawCanvas;
private Bitmap canvasBitmap;
private int previousPaintColor;
private int paintColor;
private float brushSize;
private float eraserSize;
private float lastBrushSize;
private boolean isErasing = false;
private List<Path> moveList = null;
private List<Path> undoList = null;
private List<Path> currentMoveList = null;
Called from constructor:
private void setupDrawing() {
drawPath = new Path();
drawPaint = new Paint();
brushSize = getResources().getInteger(R.integer.default_brush_size);
lastBrushSize = brushSize;
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);
}
onDraw:
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
for (Path path : currentMoveList) {
canvas.drawPath(path, drawPaint);
}
for (Path path : moveList) {
canvas.drawPath(path, drawPaint);
}
}
onTouchEvent:
#Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
drawPath.moveTo(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
drawPath.lineTo(touchX, touchY);
currentMoveList.add(drawPath);
break;
case MotionEvent.ACTION_UP:
drawPath.lineTo(touchX, touchY);
drawCanvas.drawPath(drawPath, drawPaint);
moveList.add(drawPath);
drawPath = new Path();
currentMoveList.clear();
break;
default:
return false;
}
invalidate();
return true;
}
undo() :
public void undo() {
if (moveList.size() > 0) {
undoList.add(moveList.remove(moveList.size() - 1));
invalidate();
}
}
Regards,
Kyle.
After some trial and error, here is the fix:
Remove this line of code:
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
from onDraw() method and everything works perfectly :)
**Undo your onDraw override method ((TESTED)) **
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
for (Path path : currentMoveList) {
canvas.drawPath(path, drawPaint);
}
for (Path path : moveList) {
canvas.drawPath(path, drawPaint);
}
super.onDraw(canvas);
}
Related
I am making a drawing app and it has layers. Each time I draw a line it disappears. The process is like this :
Draw a line with path and add it to path ArrayList once TOUCH_UP happens (I lift my finger)
invalidate is called and then canvas draws all paths stored in the arraylist
Instead of such behavior my path disappears the moment it is drawn and canvas is blank. Methods :
(path,layer1,layer2,layer3 are all ArrayList of Path type)
// constructor where path arraylist is pointing to currently used layer arraylist (all contain path types)
public ArtView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
this.context = context;
setupDrawing();
layer = 1;
layer1 = new ArrayList<>();
layer2 = new ArrayList<>();
layer3 = new ArrayList<>();
path = layer1;
undo = new ArrayList<>();
}
// onDraw is called each time to update canvas with each change
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
// load a picture from galery if under is true, make a new bitmap if false
if (loadedBitmap != null) {
canvas.drawBitmap(loadedBitmap, 0, 0, paintLine);
} else {
canvas.drawBitmap(bitmap, 0, 0, canvasPaint);
}
// for each layer arraylist draw all their paths on canvas
for (Path path : layer1) {
canvas.drawPath(path, paintLine);
}
for (Path path : layer2) {
canvas.drawPath(path, paintLine);
}
for (Path path : layer3) {
canvas.drawPath(path, paintLine);
}
canvas.restore();
}
// touch method that handles drawing
#Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
path.add(mPath); // add currently created path to arraylist to be painted in onDraw
mPath.reset();
break;
default:
return false;
}
invalidate(); // call onDraw and update changes
return true;
}
// set up method called when starting this activity (from MainActivity class)
public void setupDrawing() {
mPath = new Path();
paintLine = new Paint();
paintLine.setAntiAlias(true);
paintLine.setDither(true);
paintLine.setStrokeJoin(Paint.Join.ROUND);
paintLine.setStyle(Paint.Style.STROKE);
paintLine.setColor(paintColor);
paintLine.setStrokeWidth(1);
paintLine.setStrokeCap(Paint.Cap.ROUND);
paintLine.setXfermode(null);
paintLine.setAlpha(255);
canvasPaint = new Paint(Paint.DITHER_FLAG);
lastBrushSize = paintLine.getStrokeWidth();
}
One more problem is that whenever I draw the first line when starting the app, it never shows the line that is being traced. Only once I finish drawing it is shows up then disappears. The next one I make is visible nicely as I trace it , it also disappears once its done.
Any tips on how to make all the lines remain on canvas and how to make my first line being visible while it is being drawn ?
I am drawing on the canvas properly and saving it into a bitmap.
However, I want to reset the canvas to white by clicking a button.
Here is my code:
public class Canvas extends View {
Paint paint;
Path path;
boolean cc = false;
public Canvas(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
path = new Path();
paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5f);
}
#Override
protected void onDraw(android.graphics.Canvas canvas) {
super.onDraw(canvas);
if (!cc) {
canvas.drawPath(path, paint);
}
else {
canvas.drawColor(Color.WHITE);
cc = false;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float xPos = event.getX();
float yPos = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(xPos, yPos);
return true;
case MotionEvent.ACTION_MOVE:
path.lineTo(xPos, yPos);
break;
case MotionEvent.ACTION_UP:
break;
default:
return false;
}
invalidate();
return true;
}
public void clear() {
cc = true;
invalidate();
}
my clear() function set cc to "true" then invalidate() calls the onDraw() function. But it seems that "cc" is not recognized inside the onDraw() or it has always the same value inside.
I tried the path.reset() with no result.
calling clear() does not return any error.
Seems you'd want the path to get cleared too when your clear() method is called, so do that, then use the fact that the path is empty to clear the canvas.
public void clear() {
path.reset();
invalidate();
}
#Override
protected void onDraw(android.graphics.Canvas canvas) {
super.onDraw(canvas);
if (path.isEmpty()) {
canvas.drawColor(Color.WHITE);
} else {
canvas.drawPath(path, paint);
}
}
That entirely eliminates the cc field.
To clear all your canvas use this:
Paint transparent = new Paint();
transparent.setAlpha(0);
Update:
Add this line in your button onclick():
canvas.drawColor(Color.WHITE);
And remove it from the draw function.
I wanted to draw the custom shape using multiple straight lines. For that, i used the canvas. But I can draw only one line. When I draw second, previous disappears.
My code is given.
public class CanvasBackground extends View {
public Paint paint;
public Context context;
public Canvas canvas;
public ScaleGestureDetector scaleGestureDetector;
float scalfactor = 1f;
boolean isDrawing;
private PointF startPoint, endPoint;
public CanvasBackground(Context context) {
super(context);
this.context = context;
paint = new Paint();
scaleGestureDetector = new ScaleGestureDetector(context, new CanvasScale());
setDrawingCacheEnabled(true);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
paint.setColor(Color.WHITE);
canvas.drawPaint(paint);
canvas.save();
DrawingZoomingCanvas(canvas);
DrawingLine(canvas);
canvas.restore();
Log.e("OnDraw >>>", "CALLING");
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startPoint = new PointF(event.getX(), event.getY());
endPoint = new PointF();
isDrawing = true;
break;
case MotionEvent.ACTION_MOVE:
if (isDrawing) {
endPoint.x = event.getX();
endPoint.y = event.getY();
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (isDrawing) {
endPoint.x = event.getX();
endPoint.y = event.getY();
//isDrawing = false;
invalidate();
}
break;
default:
break;
}
//scaleGestureDetector.onTouchEvent(event);
Log.e("OnTouch >>>", "CALLING" + isDrawing);
return true;
}
//drawing Matrix Canvas With Zoom
private void DrawingZoomingCanvas(Canvas canvas) {
//drawing Matarix
canvas.translate(scalfactor * 10, scalfactor * 10);
canvas.scale(scalfactor, scalfactor);
paint.setColor(Color.rgb(220, 220, 220));
for (int i = 0; i <= canvas.getHeight() * scalfactor; i += 10) {
canvas.drawLine(i, 0, i, canvas.getHeight(), paint);
canvas.drawLine(0, i, canvas.getWidth(), i, paint);
}
}
//drawing a line
private void DrawingLine(Canvas canvas) {
paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setAntiAlias(true);
if (isDrawing)
canvas.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y, paint);
}
private class CanvasScale extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scalfactor *= scaleGestureDetector.getScaleFactor();
scalfactor = Math.max(0.1f, Math.min(scalfactor, 10.0f));
invalidate();
return true;
}
}
}
You are clearing your canvas each time you draw a line, so the previous line will be erased as you draw the new one.
You need to store the previous lines in a bitmap so you can draw these when you draw the new one.
i'm working on a project where i create mirror of drawing. my main logic is working fine.Only thing causing Problem is Redo and undo functionality.
i have searched a lot. implement may methods but couldn't get success. Following is my Drawing Class.
DrawingView.java
private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>();
public DrawingView(Context context, AttributeSet attrs){
super(context, attrs);
this.context=context;
setupDrawing();
}
//setup drawing
private void setupDrawing(){
//prepare for drawing and setup paint stroke properties
brushSize = getResources().getInteger(R.integer.small_size);
lastBrushSize = brushSize;
drawPath = new Path();
drawPath1 = 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);
}
//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);
width=w;
height=h;
Log.d("width,height", w + " , " + h);
drawCanvas = new Canvas(canvasBitmap);
}
//draw the view - will be called after touch event
#Override
protected void onDraw(Canvas canvas) {
for (Path p : paths){canvas.drawPath(p, drawPaint);}
canvas.drawPath(drawPath, drawPaint);
Log.i("OnDRAWING", "REACH ON DRAW");
/*canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
canvas.drawPath(drawPath1, 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.reset();
undonePaths.clear();
drawPath.moveTo(touchX, touchY);
undonePaths.clear();
break;
case MotionEvent.ACTION_MOVE:
drawPath.lineTo(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
drawPath.lineTo(touchX, touchY);
drawCanvas.drawPath(drawPath, drawPaint);// commit the path to our offscreen
paths.add(drawPath);
drawCanvas.drawPath(drawPath, drawPaint);
drawPath.reset();
drawCanvas.drawPath(drawPath1, drawPaint);
drawPath1.reset();
break;
default:
return false;
}
//redraw
invalidate();
return true;
}
What i am missing here?
Any suggestions/ideas/examples which is the best way to implement this kind of functionality on my project?
Canvas drawing is sort of hierarchal in that drawing happens in the order that you do them.
So all drawing should be done in onDraw and only here.
Your draw events should be pushed on a draw stack. You do not store a canvas. You just store the operations that are supposed to happen when the drawing eventually occurs (coordinates, width, and color of a path for example).
An "undo" operation can be done by popping from the drawing stack and pushing the event to a "redo" stack. The "redo" event can be done by popping from the "redo" stack and pushing back on to the "drawing" stack.
In onDraw method of your View, just scan through the drawing events and draw on the canvas.
EDIT:
The onDraw() method would just iterate through your drawing operations. So first you could have an interface called DrawEvent like so:
public interface DrawEvent {
void draw(Canvas c);
}
Then in your View class have your collection of DrawEvents and iterate through them in onDraw(Canvas c).
public class MyView extends View {
Deque<DrawEvent> drawEvents = new LinkedList<DrawEvent>();
#Override
public void onDraw(Canvas c) {
for (DrawEvent e : drawEvents) {
e.draw(c);
}
}
}
Im making a basic app to test making paths.
So I got the paths to work, but when I am in the emulator and begin a path it starts the path about inch below from where I clicked.
Besides from the path being a inch off below, it will follow my mouse and make a path but always from a inch below where the mouse is.
This is my code:
public class Paths extends Activity {
Path newPath = new Path();
Paint paint = new Paint();
Canvas canvas = new Canvas();
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_paths);
}
public boolean onTouchEvent(MotionEvent event) {
LinearLayout relative = (LinearLayout) findViewById(R.id.drawing_view2);
float x = event.getRawX();
float y = event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
newPath.moveTo(x, y);
relative.addView(new DrawView2(relative.getContext(), newPath));
return true;
case MotionEvent.ACTION_MOVE:
newPath.lineTo(x, y);
break;
case MotionEvent.ACTION_UP:
default:
return false;
}
relative.invalidate();
return true;
}
This is the java class that handles the paint and canvas objects.
private final Path path;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
public DrawView2(Context context, Path path) {
super(context);
int mycolor = Color.parseColor("#6633CC");
paint.setAntiAlias(true);
paint.setDither(true);
paint.setColor(mycolor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(8f);
this.path = path;
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(path, paint);
}
and my layout file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/drawing_view2"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#33FF99"
You are getting coordinates with
float x = event.getRawX();
float y = event.getRawY();
Instead of this, try that:
float x = event.getX();
float y = event.getY();