ANDROID - Undo and Redo in canvas

耗尽温柔 提交于 2019-12-23 22:12:33

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!