问题
I am drawing a circle (on touch) on bitmap to erase the overlay bitmap for that area in the circle.How do I add undo and redo functionality for this?
EDIT:Please refer Android: Undo redo in CustomView as it has the problem I'm currently facing with the given solution.
@Override
protected void onDraw(Canvas canvas) {
pcanvas.drawCircle(x, y, 10, mPaint);
canvas.drawBitmap(bitmap, 0, 0, null);
super.onDraw(canvas);
}
onTouchEvent
public boolean onTouchEvent(MotionEvent ev)
{
switch (ev.getAction())
{
case MotionEvent.ACTION_DOWN:
{
x = (int) ev.getX();
y = (int) ev.getY();
invalidate();
break;
}
case MotionEvent.ACTION_MOVE:
{
x = (int) ev.getX();
y = (int) ev.getY();
invalidate();
break;
}
case MotionEvent.ACTION_UP:
invalidate();
break;
}
return true;
}
回答1:
As mentioned in the comments, you could keep Stacks to track the xy coordinate history.
Undo and Redo operations revolve around pushing and popping from the separate stacks.
UndoCanvas
public class UndoCanvas extends View {
private final int MAX_STACK_SIZE = 50;
private Stack<Pair<Float, Float>> undoStack = new Stack<>();
private Stack<Pair<Float, Float>> redoStack = new Stack<>();
private Bitmap originalBitmap;
private Bitmap maskedBitmap;
private Canvas originalCanvas;
private Canvas maskedCanvas;
private Paint paint;
private float drawRadius;
private StackListener listener;
public UndoCanvas(Context context) {
super(context);
init();
}
public UndoCanvas(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public UndoCanvas(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
drawRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
paint = new Paint();
// paint.setColor(Color.RED);
paint.setAlpha(0);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
paint.setAntiAlias(true);
paint.setMaskFilter(new BlurMaskFilter(15, BlurMaskFilter.Blur.SOLID));
}
public void setBitmap(Bitmap bitmap) {
if (bitmap != null) {
originalBitmap = bitmap.copy(bitmap.getConfig(), true); // Copy of the original, because we will potentially make changes to this
maskedBitmap = originalBitmap.copy(originalBitmap.getConfig(), true);
originalCanvas = new Canvas(originalBitmap);
maskedCanvas = new Canvas(maskedBitmap);
} else {
originalBitmap = null;
originalCanvas = null;
maskedBitmap = null;
maskedCanvas = null;
}
int undoSize = undoStack.size();
int redoSize = redoStack.size();
undoStack.clear();
redoStack.clear();
invalidate();
if (listener != null) {
if (undoSize != undoStack.size()) {
listener.onUndoStackChanged(undoSize, undoStack.size());
}
if (redoSize != redoStack.size()) {
listener.onRedoStackChanged(redoSize, redoStack.size());
}
}
}
public StackListener getListener() {
return listener;
}
public void setListener(StackListener listener) {
this.listener = listener;
}
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
int undoSize = undoStack.size();
int redoSize = redoStack.size();
// Max stack size. Remove oldest item before adding new
if (undoStack.size() == MAX_STACK_SIZE) {
// The undo history does not go further back, so make the change permanent by updating the original canvas/bitmap
Pair<Float, Float> pair = undoStack.remove(0);
maskPoint(originalCanvas, pair.first, pair.second);
}
undoStack.push(new Pair<>(ev.getX(), ev.getY()));
redoStack.clear();
invalidate();
if (listener != null) {
if (undoSize != undoStack.size()) {
listener.onUndoStackChanged(undoSize, undoStack.size());
}
if (redoSize != redoStack.size()) {
listener.onRedoStackChanged(redoSize, redoStack.size());
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
int undoSize = undoStack.size();
int redoSize = redoStack.size();
// Max stack size. Remove oldest item before adding new
if (undoStack.size() == MAX_STACK_SIZE) {
// The undo history does not go further back, so make the change permanent by updating the original canvas/bitmap
Pair<Float, Float> pair = undoStack.remove(0);
maskPoint(originalCanvas, pair.first, pair.second);
}
maskPoint(maskedCanvas, ev.getX(), ev.getY());
undoStack.push(new Pair<>(ev.getX(), ev.getY()));
redoStack.clear();
invalidate();
if (listener != null) {
if (undoSize != undoStack.size()) {
listener.onUndoStackChanged(undoSize, undoStack.size());
}
if (redoSize != redoStack.size()) {
listener.onRedoStackChanged(redoSize, redoStack.size());
}
}
break;
}
case MotionEvent.ACTION_UP:
invalidate();
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
if (maskedBitmap != null) {
canvas.drawBitmap(maskedBitmap, 0, 0, null);
}
super.onDraw(canvas);
}
public boolean undo() {
if (!undoStack.empty()) {
int undoSize = undoStack.size();
int redoSize = redoStack.size();
Pair<Float, Float> pair = undoStack.pop();
// Redraw a single part of the original bitmap
//unmaskPoint(maskedCanvas, pair.first, pair.second);
// Redraw the original bitmap, along with all the points in the undo stack
remaskCanvas(maskedCanvas);
redoStack.push(pair); // Do not need to check for > 50 here, since redoStack can only contain what was in undoStack
invalidate();
if (listener != null) {
if (undoSize != undoStack.size()) {
listener.onUndoStackChanged(undoSize, undoStack.size());
}
if (redoSize != redoStack.size()) {
listener.onRedoStackChanged(redoSize, redoStack.size());
}
}
return true;
}
return false;
}
public boolean redo() {
if (!redoStack.empty()) {
int undoSize = undoStack.size();
int redoSize = redoStack.size();
Pair<Float, Float> pair = redoStack.pop();
maskPoint(maskedCanvas, pair.first, pair.second);
undoStack.push(pair); // Do not need to check for > 50 here, since redoStack can only contain what was in undoStack
invalidate();
if (listener != null) {
if (undoSize != undoStack.size()) {
listener.onUndoStackChanged(undoSize, undoStack.size());
}
if (redoSize != redoStack.size()) {
listener.onRedoStackChanged(redoSize, redoStack.size());
}
}
return true;
}
return false;
}
private void maskPoint(Canvas canvas, float x, float y) {
if (canvas != null) {
canvas.drawCircle(x, y, drawRadius, paint);
}
}
private void unmaskPoint(Canvas canvas, float x, float y) {
if (canvas != null) {
Path path = new Path();
path.addCircle(x, y, drawRadius, Path.Direction.CW);
canvas.save();
canvas.clipPath(path);
canvas.drawBitmap(originalBitmap, 0, 0, new Paint());
canvas.restore();
}
}
private void remaskCanvas(Canvas canvas) {
if (canvas != null) {
canvas.drawBitmap(originalBitmap, 0, 0, new Paint());
for (int i = 0; i < undoStack.size(); i++) {
Pair<Float, Float> pair = undoStack.get(i);
maskPoint(canvas, pair.first, pair.second);
}
}
}
public interface StackListener {
void onUndoStackChanged(int previousSize, int newSize);
void onRedoStackChanged(int previousSize, int newSize);
}
}
You would want to limit the size of these stack so they don't overflow as a user drags across the screen. You can play around with the number, but 50 seems like a good start for me.
EDIT
As a side note, it might be good to redo / undo multiple entries at a time. Since onTouchEvent
will trigger for very fine movements. Movements that the user would not notice when undo / redo are pressed.
EDIT 2
I have added to the above implementation, to handle the undo as well. I found that the strategy that only redraws at the specific point is insufficient as overlapping points are incorrect. (Point A and B overlap, removing B results in a subsection of A being cleared).
Because of this I remask the entire bitmap on an undo operation, this means that an intermediate bitmap is required for undoing. Without the intermediate bitmap, the undo operation will result in points no longer in the stack (50 max) from being removed as well. Since we do not support undo passed that point, using the original bitmap as the intermediate bitmap is sufficient.
Both methods are in the code so you can test both of them.
Lastly, I added a Listener to allow the Activity to know the state of the stacks. To Enable / Disable the buttons.
MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final UndoCanvas canvas = (UndoCanvas) findViewById(R.id.undoCanvas);
final Button undoButton = (Button) findViewById(R.id.buttonUndo);
final Button redoButton = (Button) findViewById(R.id.buttonRedo);
undoButton.setEnabled(false);
redoButton.setEnabled(false);
canvas.setListener(new UndoCanvas.StackListener() {
@Override
public void onUndoStackChanged(int previousSize, int newSize) {
undoButton.setEnabled(newSize > 0);
}
@Override
public void onRedoStackChanged(int previousSize, int newSize) {
redoButton.setEnabled(newSize > 0);
}
});
undoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
canvas.undo();
}
});
redoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
canvas.redo();
}
});
canvas.setBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.image));
}
}
Screenshots
Before Undo
After Undo
来源:https://stackoverflow.com/questions/36081385/android-undo-and-redo-in-canvas