androiddrawingandroid-canvasandroid-paint

Undo and Redo on canvas


I am developing drawing app, in which user can draw Shapes like Rectangle, circle etc. User can also make a free hand drawing(Pen).

I want to add undo, redo feature. I have searched and read most of SO answer for undo and redo, but all are related with paths. Means they are managing two List, one for drawn list of paths or second one for undo list of paths. Its a good way to work, but its ONLY work if Its a free hand drawing or if I work with paths.

Here I am calling different types of canvas methods for different shapes.

Help me to provide undo redo on canvas drawing.

Here is my code

public class DrawingView extends android.support.v7.widget.AppCompatImageView {
    public static final int RECTANGLE = 1;
    public static final int SQUARE = 2;
    public static final int CIRCLE = 3;
    public 

static final int LINE = 4;
    public static final int SMOOTH_LINE = 5;
    public static final int TRIANGLE = 6;
    public static final int IMPORT_IMAGE = 7;
    public static final int ERASER = 8;

private static final float TOUCH_TOLERANCE = 5;

private int color;
private int currentShape;

protected Paint mPaint;
protected Bitmap mBitmap;
protected Canvas mCanvas;

private float mx, my;
private float mStartX, mStartY;

private int width, height;

private boolean isDrawing = true;

public static int TOUCH_STROKE_WIDTH = 3;

public static int ERASER_WIDTH = 3;

private Path mPath = new Path();

int countTouch = 0;
float basexTriangle = 0;
float baseyTriangle = 0;

public DrawingView(Context context, int shape, int color) {
    super(context);
    initPaint();
}

protected void initPaint() {

    color = DrawingActivity.selectedColor;
    currentShape = DrawingActivity.currentShape;

    mPaint = new Paint(Paint.DITHER_FLAG);
    mPaint.setAntiAlias(true);
    mPaint.setDither(true);
    mPaint.setColor(color);
    if (DrawingActivity.isFill && !DrawingActivity.isEraser && currentShape != SMOOTH_LINE) {
        mPaint.setStyle(Paint.Style.FILL);
    } else {
        mPaint.setStyle(Paint.Style.STROKE);
    }
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    if (DrawingActivity.isEraser) {
        mPaint.setStrokeWidth(ERASER_WIDTH);
    } else {
        mPaint.setStrokeWidth(TOUCH_STROKE_WIDTH);
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    //Retrieve the point
    mx = event.getX();
    my = event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            initPaint();
            break;
    }

    switch (currentShape) {
        case RECTANGLE:
            onTouchEventRectangle(event);
            break;
        case SQUARE:
            onTouchEventSquare(event);
            break;
        case CIRCLE:
            onTouchEventCircle(event);
            break;
        case LINE:
            onTouchEventLine(event);
            break;
        case SMOOTH_LINE:
            onTouchEventSmoothLine(event);
            break;
        case TRIANGLE:
            onTouchEventTriangle(event);
            break;
    }

    return true;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mBitmap, 0, 0, mPaint);

    if (isDrawing) {
        switch (currentShape) {
            case RECTANGLE:
                onDrawRectangle(canvas);
                break;
            case SQUARE:
                onDrawSquare(canvas);
                break;
            case CIRCLE:
                onDrawCircle(canvas);
                break;
            case LINE:
                onDrawLine(canvas);
                break;
            case SMOOTH_LINE:
                onDrawLine(canvas);
                break;
            case TRIANGLE:
                onDrawTriangle(canvas);
                break;
        }
    }

    //draw your element
}

private void onTouchEventRectangle(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrawing = true;
            mStartX = mx;
            mStartY = my;
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            isDrawing = false;
            drawRectangle(mCanvas, mPaint);
            invalidate();
            break;
    }
}

private void onDrawRectangle(Canvas canvas) {
    drawRectangle(canvas, mPaint);
}

private void drawRectangle(Canvas canvas, Paint paint) {
    float right = mStartX > mx ? mStartX : mx;
    float left = mStartX > mx ? mx : mStartX;
    float bottom = mStartY > my ? mStartY : my;
    float top = mStartY > my ? my : mStartY;
    canvas.drawRect(left, top, right, bottom, paint);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    width = w;
    height = h;
    mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
    mCanvas = new Canvas(mBitmap);
}

private void onDrawSquare(Canvas canvas) {
    onDrawRectangle(canvas);
}

private void onTouchEventSquare(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrawing = true;
            mStartX = mx;
            mStartY = my;
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            adjustSquare(mx, my);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            isDrawing = false;
            adjustSquare(mx, my);
            drawRectangle(mCanvas, mPaint);
            invalidate();
            break;
    }
}

/**
 * Adjusts current coordinates to build a square
 *
 * @param x
 * @param y
 */
protected void adjustSquare(float x, float y) {
    float deltaX = Math.abs(mStartX - x);
    float deltaY = Math.abs(mStartY - y);

    float max = Math.max(deltaX, deltaY);

    mx = mStartX - x < 0 ? mStartX + max : mStartX - max;
    my = mStartY - y < 0 ? mStartY + max : mStartY - max;
}

private void onDrawCircle(Canvas canvas) {
    canvas.drawCircle(mStartX, mStartY, calculateRadius(mStartX, mStartY, mx, my), mPaint);
}

private void onTouchEventCircle(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrawing = true;
            mStartX = mx;
            mStartY = my;
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            isDrawing = false;
            mCanvas.drawCircle(mStartX, mStartY,
                    calculateRadius(mStartX, mStartY, mx, my), mPaint);
            invalidate();
            break;
    }
}

/**
 * @return
 */
protected float calculateRadius(float x1, float y1, float x2, float y2) {

    return (float) Math.sqrt(
            Math.pow(x1 - x2, 2) +
                    Math.pow(y1 - y2, 2)
    );
}

private void onDrawLine(Canvas canvas) {

    float dx = Math.abs(mx - mStartX);
    float dy = Math.abs(my - mStartY);
    if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
        canvas.drawLine(mStartX, mStartY, mx, my, mPaint);
    }
}

private void onTouchEventLine(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrawing = true;
            mStartX = mx;
            mStartY = my;
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            isDrawing = false;
            mCanvas.drawLine(mStartX, mStartY, mx, my, mPaint);
            invalidate();
            break;
    }
}

private void onTouchEventSmoothLine(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isDrawing = true;
            mStartX = mx;
            mStartY = my;

            mPath.reset();
            mPath.moveTo(mx, my);

            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:

            float dx = Math.abs(mx - mStartX);
            float dy = Math.abs(my - mStartY);
            if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
                mPath.quadTo(mStartX, mStartY, (mx + mStartX) / 2, (my + mStartY) / 2);
                mStartX = mx;
                mStartY = my;
            }
            mCanvas.drawPath(mPath, mPaint);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            isDrawing = false;
            mPath.lineTo(mStartX, mStartY);
            mCanvas.drawPath(mPath, mPaint);
            mPath.reset();
            invalidate();
            break;
    }
}

private void onDrawTriangle(Canvas canvas) {

    if (countTouch < 3) {
        canvas.drawLine(mStartX, mStartY, mx, my, mPaint);
    } else if (countTouch == 3) {
        canvas.drawLine(mx, my, mStartX, mStartY, mPaint);
        canvas.drawLine(mx, my, basexTriangle, baseyTriangle, mPaint);
    }
}

private void onTouchEventTriangle(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            countTouch++;
            if (countTouch == 1) {
                isDrawing = true;
                mStartX = mx;
                mStartY = my;
            } else if (countTouch == 3) {
                isDrawing = true;
            }
            invalidate();
            break;
        case MotionEvent.ACTION_MOVE:
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            countTouch++;
            isDrawing = false;
            if (countTouch < 3) {
                basexTriangle = mx;
                baseyTriangle = my;
                mCanvas.drawLine(mStartX, mStartY, mx, my, mPaint);
            } else if (countTouch >= 3) {
                mCanvas.drawLine(mx, my, mStartX, mStartY, mPaint);
                mCanvas.drawLine(mx, my, basexTriangle, baseyTriangle, mPaint);
                countTouch = 0;
            }
            invalidate();
            break;
    }
}

public void clearDrawing()
{
    setDrawingCacheEnabled(false);
    onSizeChanged(width, height, width, height);
    invalidate();

    setDrawingCacheEnabled(true);
}

/**
 * Getter of currentShape
 */
public int getCurrentShape() {
    return currentShape;
}

/**
 * Setter of currentShape
 */
public void setCurrentShape(int currentShape) {
    this.currentShape = currentShape;
}

}


Solution

  • Every shapes can be represented by a path.

    You probably need to convert every shapes drawn with drawCircle, drawArc, drawLine with Path objects of the same shape. Path class has all the methods you need to create predefined shapes. Examples:

    Use a class something like this to represent a draw action of the user:

    public class DrawAction {
        public Path path;
        public Paint paint;
    
        public DrawAction(Path path, Paint paint){
            this.path = path;
            this.paint = paint;
        }
    }
    

    Then save this data in a list

    //class property
    List<DrawAction> actionsList = new ArrayList<>();
    ...    
    //add the path and the paint to a DrawAction object when the user
    //want to draw something
    actionsList.add(new DrawAction(path, paint));
    invalidate();
    

    Implement your onDraw method to draw all the paths in the list only

    //draw all the paths in your onDraw() method
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (DrawAction actionToDraw : actionsList){
            canvas.drawPath(actionToDraw.path, actionToDraw.paint);
        }
    }
    

    Now if you need to redo an action you can remove the last element from the list and call invalidate() to request the view onDraw() to be called, in this way the last path will not be drawn again.

    Obviously you need to save the undo operation in another list to be able to perform a redo, something like this:

    //class property
    List<DrawAction> removedPathList = new ArrayList<>();
    if (actionsList.size() > 0){
        DrawAction undoAction = actionsList.get(actionsList.size() - 1);
        removedPathList.add(undoAction);
        actionsList.remove(undoAction);
        invalidate();
    }
    

    Hope to have pointed you in the right direction :)