Skip to content Skip to sidebar Skip to footer

Android - Undo And Redo In Canvas

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: Und

Solution 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

publicclassUndoCanvasextendsView {
    privatefinalintMAX_STACK_SIZE=50;
    private Stack<Pair<Float, Float>> undoStack = newStack<>();
    private Stack<Pair<Float, Float>> redoStack = newStack<>();

    private Bitmap originalBitmap;
    private Bitmap maskedBitmap;
    private Canvas originalCanvas;
    private Canvas maskedCanvas;
    private Paint paint;

    privatefloat drawRadius;

    private StackListener listener;

    publicUndoCanvas(Context context) {
        super(context);

        init();
    }

    publicUndoCanvas(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    publicUndoCanvas(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    privatevoidinit() {
        drawRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());

        paint = newPaint();
        // paint.setColor(Color.RED);

        paint.setAlpha(0);
        paint.setXfermode(newPorterDuffXfermode(PorterDuff.Mode.DST_IN));
        paint.setAntiAlias(true);
        paint.setMaskFilter(newBlurMaskFilter(15, BlurMaskFilter.Blur.SOLID));
    }

    publicvoidsetBitmap(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 = newCanvas(originalBitmap);
            maskedCanvas = newCanvas(maskedBitmap);
        } else {
            originalBitmap = null;
            originalCanvas = null;
            maskedBitmap = null;
            maskedCanvas = null;
        }

        intundoSize= undoStack.size();
        intredoSize= 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;
    }

    publicvoidsetListener(StackListener listener) {
        this.listener = listener;
    }

    publicbooleanonTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intundoSize= undoStack.size();
                intredoSize= redoStack.size();

                // Max stack size. Remove oldest item before adding newif (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(newPair<>(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: {
                intundoSize= undoStack.size();
                intredoSize= redoStack.size();

                // Max stack size. Remove oldest item before adding newif (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(newPair<>(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;

        }
        returntrue;
    }

    @OverrideprotectedvoidonDraw(Canvas canvas) {
        if (maskedBitmap != null) {
            canvas.drawBitmap(maskedBitmap, 0, 0, null);
        }
        super.onDraw(canvas);

    }

    publicbooleanundo() {
        if (!undoStack.empty()) {
            intundoSize= undoStack.size();
            intredoSize= 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());
                }
            }

            returntrue;
        }

        returnfalse;
    }

    publicbooleanredo() {
        if (!redoStack.empty()) {
            intundoSize= undoStack.size();
            intredoSize= 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());
                }
            }

            returntrue;
        }

        returnfalse;
    }

    privatevoidmaskPoint(Canvas canvas, float x, float y) {
        if (canvas != null) {
            canvas.drawCircle(x, y, drawRadius, paint);
        }
    }

    privatevoidunmaskPoint(Canvas canvas, float x, float y) {
        if (canvas != null) {
            Pathpath=newPath();
            path.addCircle(x, y, drawRadius, Path.Direction.CW);

            canvas.save();
            canvas.clipPath(path);
            canvas.drawBitmap(originalBitmap, 0, 0, newPaint());
            canvas.restore();
        }
    }

    privatevoidremaskCanvas(Canvas canvas) {
        if (canvas != null) {
            canvas.drawBitmap(originalBitmap, 0, 0, newPaint());

            for (inti=0; i < undoStack.size(); i++) {
                Pair<Float, Float> pair = undoStack.get(i);
                maskPoint(canvas, pair.first, pair.second);
            }
        }
    }

    publicinterfaceStackListener {
        voidonUndoStackChanged(int previousSize, int newSize);

        voidonRedoStackChanged(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

publicclassMainActivityextendsAppCompatActivity {
    @OverrideprotectedvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        finalUndoCanvascanvas= (UndoCanvas) findViewById(R.id.undoCanvas);
        finalButtonundoButton= (Button) findViewById(R.id.buttonUndo);
        finalButtonredoButton= (Button) findViewById(R.id.buttonRedo);
        undoButton.setEnabled(false);
        redoButton.setEnabled(false);

        canvas.setListener(newUndoCanvas.StackListener() {
            @OverridepublicvoidonUndoStackChanged(int previousSize, int newSize) {
                undoButton.setEnabled(newSize > 0);
            }

            @OverridepublicvoidonRedoStackChanged(int previousSize, int newSize) {
                redoButton.setEnabled(newSize > 0);
            }
        });

        undoButton.setOnClickListener(newView.OnClickListener() {
            @OverridepublicvoidonClick(View v) {
                canvas.undo();
            }
        });

        redoButton.setOnClickListener(newView.OnClickListener() {
            @OverridepublicvoidonClick(View v) {
                canvas.redo();
            }
        });

        canvas.setBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.image));
    }
}

Screenshots

Before Undo

Screen before undo operation(s)

After Undo

Screen after undo operation(s)

Post a Comment for "Android - Undo And Redo In Canvas"