在 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;
}
}
每个形状都可以用路径表示。
您可能需要将使用 drawCircle
、drawArc
、drawLine
绘制的每个形状转换为具有相同形状的 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();
}
希望为您指明了正确的方向:)
我正在开发绘图应用程序,用户可以在其中绘制矩形、圆形等形状。用户还可以自由手绘(钢笔)。
我想添加撤消、重做功能。我已经搜索并阅读了大部分关于撤消和重做的 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;
}
}
每个形状都可以用路径表示。
您可能需要将使用 drawCircle
、drawArc
、drawLine
绘制的每个形状转换为具有相同形状的 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();
}
希望为您指明了正确的方向:)