在 canvas 上撤消和重做

Undo and Redo on canvas

我正在开发绘图应用程序,用户可以在其中绘制矩形、圆形等形状。用户还可以自由手绘(钢笔)。

我想添加撤消、重做功能。我已经搜索并阅读了大部分关于撤消和重做的 SO 答案,但所有这些都与路径相关。意味着他们正在管理两个列表,一个用于绘制路径列表,另一个用于撤消路径列表。这是一种很好的工作方式,但只有在自由手绘或使用路径时才有效。

这里我针对不同的形状调用了不同类型的canvas方法

帮我在canvas绘图上提供撤消重做。

这是我的代码

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;
}

}

每个形状都可以用路径表示。

您可能需要将使用 drawCircledrawArcdrawLine 绘制的每个形状转换为具有相同形状的 Path 个对象。 Path class 拥有创建预定义形状所需的所有方法。 示例:

  • 圆形路径 : path.addCircle(float x, float y, float radius, Path.Direction dir)

  • 矩形路径 : path.addRect(float left, float top, float right, float bottom, Path.Direction dir)

使用 class 这样的东西来表示用户的绘制动作:

public class DrawAction {
    public Path path;
    public Paint paint;

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

然后将此数据保存在列表中

//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();

实现你的 onDraw 方法只绘制列表中的所有路径

//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);
    }
}

现在,如果您需要重做一个动作,您可以从列表中删除最后一个元素并调用 invalidate() 请求调用视图 onDraw(),这样最后一个路径将不会再次抽奖。

显然您需要将撤消操作保存在另一个列表中才能执行重做,如下所示:

//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();
}

希望为您指明了正确的方向:)