android canvas remove previous path being drawn

21,006

Solution 1

I think the easiest way for doing that is having 2 bitmaps (1 additional backup bitmap for restoring previous state).

You need to save previous state of a bitmap before you start new drawings.

Here is how I would modify your code:

  private HashMap<Integer, Path> pathMap; // current Paths being drawn
  private HashMap<Integer, Point> previousPointMap; // current Points
  private Bitmap bitmap; // drawing area for display or saving
  private Bitmap bitmapBackup; 
  private Canvas bitmapCanvas; // used to draw on bitmap
  private Canvas bitmapBackupCanvas; 


  // remember last bitmap before new drawings...    
     private void touchStarted()   
     {
        bitmapBackupCanvas.drawBitmap(bitmap, 0, 0, null);
     } 
  // called when the user finishes a touch    
     private void touchEnded(int lineID)   
     {
        Path path = pathMap.get(lineID); // get the corresponding Path
        bitmapCanvas.drawPath(path, paintLine); // draw to bitmapCanvas
        path.reset(); // reset the Path
        rememberLineId = lineID;
     } // end method touch_ended

  //undo       
     private void undo()
     {
        Path path = pathMap.get(rememberLineId); // get the corresponding Path
        pathMap.remove(rememberLineId);
        bitmapCanvas.drawBitmap(bitmapBackup, 0, 0, null); // restore from backup
        path.reset(); // reset the Path
     } 

Solution 2

It is an old post, but I was looking for an answer for that issue as well. I was not satisfied with the chosen answer for that post and I found one myself after that. I actually think that having a full bitmap as a backup is not great memory-wise and it will limit the number of undo steps we can have.

I believe a better solution would be :

To have a stack of paths in your class

private Stack<Path> m_pathHistory = new Stack<Path>();

next to your canvas and your paintbrush (initialization skipped) :

private Canvas m_drawingCanvas;
private Paint m_paint;

Then every time a stroke is finished (on the touch up event), add a "clone" of the path to the undo history:

m_pathHistory.Push(new Path(currentPath));

And here is the undo function :

public void Undo()
{
    if(m_pathHistory.Count > 0)
    {
        m_pathHistory.Pop(); // Remove the last path from the history

        m_drawingCanvas.DrawColor(Color.Transparent, PorterDuff.Mode.Clear); // Clear the canvas with a transparent color

        // Draw the paths which are still in the history
        foreach (Path p in m_pathHistory)
        {
            m_drawingCanvas.DrawPath(p, m_paint);
        }
     }
}

The paths are much smaller to store in the memory than the full bitmaps, so we can have a much bigger history.

Solution 3

At first glance I see the following problems:

By adding your empty Path to paths as soon as you make it, you're going to have a problem as soon as you undo: you're popping that empty Path first, making the first undo not seem to work. Then if you draw into that Path, it's not added to paths. The solution is to add the completed Path to paths in touch_up() before creating a new one.

That is, remove

paths.add(mPath);

from the constructor, and in touch_up(), change

mPath = new Path();
paths.add(mPath);

to

paths.add(mPath);
mPath = new Path();

You'll also want to add

canvas.drawPath(mPath, mPaint);

after your for loop in onDraw() in order to draw the in-progress Path.

You're not emptying undonePaths when the user starts drawing again.

Solution 4

use path.reset()in the MotionEvent.ACTION_DOWN event of OnTouchEvent() method.

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPath.reset();
            invalidate();
            break;
    }
    return true;

}
Share:
21,006
pearmak
Author by

pearmak

Updated on February 04, 2020

Comments

  • pearmak
    pearmak over 4 years

    I am making a drawing app, and would like to implement an undo function to remove the immediate previous drawn path.

    Coding:

    private HashMap<Integer, Path> pathMap; // current Paths being drawn
    private HashMap<Integer, Point> previousPointMap; // current Points
    private Bitmap bitmap; // drawing area for display or saving
    private Canvas bitmapCanvas; // used to draw on bitmap
    private Paint paintScreen; // use to draw bitmap onto screen
    private Paint paintLine; // used to draw lines onto bitmap
    
    public DrawView(Context context, AttributeSet attrs) 
    {
         super(context, attrs); // pass context to View's constructor
         this.context_new=context;
    
          paintScreen = new Paint(); // used to display bitmap onto screen
    
          // set the initial display settings for the painted line
          paintLine = new Paint();
          paintLine.setAntiAlias(true); // smooth edges of drawn line
          paintLine.setColor(Color.BLACK); // default color is black
          paintLine.setStyle(Paint.Style.STROKE); // solid line
          paintLine.setStrokeWidth(5); // set the default line width
          paintLine.setStrokeCap(Paint.Cap.ROUND); // rounded line ends
          pathMap = new HashMap<Integer, Path>();
          previousPointMap = new HashMap<Integer, Point>();
    } // end DrawView constructor
    
    @Override
    protected void onDraw(Canvas canvas)  
    {
        canvas.drawBitmap(bitmap, 0, 0, paintScreen); 
        for (Integer key : pathMap.keySet()) 
        canvas.drawPath(pathMap.get(key), paintLine);
    } 
    
    // called when the user finishes a touch    
       private void touchEnded(int lineID)   
       {
          Path path = pathMap.get(lineID); // get the corresponding Path
          bitmapCanvas.drawPath(path, paintLine); // draw to bitmapCanvas
          path.reset(); // reset the Path
          rememberLineId = lineID;
       } // end method touch_ended
    
    //undo       
       private void undo()
       {
          Path path = pathMap.get(rememberLineId); // get the corresponding Path
          pathMap.remove(rememberLineId);
          bitmapCanvas.clearPath(path, paintLine); 
          path.reset(); // reset the Path
       } 
    

    Question:

    However, it seems there is no bitmapCanvas.clearPath this method? If then how could it be modified?

    Codes Amended:

    Declarations:

    private Bitmap bitmap; // drawing area for display or saving
    private Canvas bitmapCanvas; // used to draw on bitmap
    private Paint paintScreen; // use to draw bitmap onto screen
    private Paint paintLine; // used to draw lines onto bitmap
    private HashMap<Integer, Path> pathMap; // current Paths being drawn
    private HashMap<Integer, Point> previousPointMap; // current Points
    
    private Bitmap bitmapBackup; 
    

    OnSizeChanged

    @Override
    public void onSizeChanged(int w, int h, int oldW, int oldH)
    { 
       super.onSizeChanged(w, h, oldW, oldH);
       DoodlzViewWidth = w;    
       DoodlzViewHeight = h;
    
       bitmapBackup = Bitmap.createBitmap(getWidth(), DoodlzViewHeight, Bitmap.Config.ARGB_8888);     
       bitmap =       Bitmap.createBitmap(getWidth(), DoodlzViewHeight, Bitmap.Config.ARGB_8888);
    
       bitmapCanvas = new Canvas(bitmap);
       bitmap      .eraseColor(Color.WHITE); // erase the BitMap with white 
       bitmapBackup.eraseColor(Color.WHITE); 
    } 
    

    FirsttoBackup method, will invoke when the below TouchedStart performs

    public void firsttobackup()
    { 
       bitmapBackup=bitmap;
           Toast message = Toast.makeText(getContext(), "backuped 123", Toast.LENGTH_SHORT);
       message.show(); //THIS TOAST CAN BE SUCESSFULLY PRESENTED when touching screen starting to draw
    } 
    

    OnDraw

    @Override
    protected void onDraw(Canvas canvas) 
    {
    canvas.drawBitmap(bitmap, 0, 0, paintScreen); 
        for (Integer key : pathMap.keySet()) 
         canvas.drawPath(pathMap.get(key), paintLine); 
    

    }

    OnTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) 
    {        
      int action = event.getActionMasked(); // event type 
      int actionIndex = event.getActionIndex(); // pointer (i.e., finger)
    
      if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) 
      {
          firsttobackup(); //TOAST CAN SHOW "BACKUP 123"
    
          touchStarted(event.getX(actionIndex), event.getY(actionIndex), 
            event.getPointerId(actionIndex));
      }
    

    Undo: user pressing the undo button will invoke this

    public void undo()
    {
      bitmap = bitmapBackup.copy(Bitmap.Config.ARGB_8888, true);
      bitmapCanvas = new Canvas(bitmap);        
    }  
    

    Question revised:

    A method firsttobackup() is used now such that bitmapBackup would set = bitmap when executing OnTouchEvent touchStarted. I have put a toast in it and it is successfully to be presented "backup 123" when user press the screen and started to draw.

    When user clicks undo button, it will invoke the undo method, but now pressing the undo button, no action can be seen...why?

  • pearmak
    pearmak over 11 years
    Thanks NagarjunaReddy.P for your ref! I have read through both links but inside both blog the way to clear paint/path is totally erase all the paint but not the immediate previous path(s). Your way is public void onClick(View v) {mBitmap.eraseColor(Color.TRANSPARENT);mPath.reset();mView.i‌​nvalidate();} --> It would be more perfect if it allows users to click undo one by one to clear previous actions. BTW, i am interested in how do you think for my another Q for placing imported bitmap in middle of extended view @ stackoverflow.com/questions/14675008/…
  • pearmak
    pearmak over 11 years
    yes i have also thought about this way. This no doubt is the simpliest & most clear: saving one by one. yet just curious, i really dont know whether if i allow user to undo more times, say, 5 actions (and save 5 previous bitmap), would it popup outofmemory error. it is because i have tested on S3 for importing some not too big photo to draw on it but it would crash upon direct import (yet wont outofmemory for S2!!)...all in all, let me first try running on different actual devices and revert to you =) thanks for your brillant idea and i would considered this is the last resort for the issue
  • pearmak
    pearmak over 11 years
    i tested using above code but crashes. logcat is at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747) at dalvik.system.NativeStart.main(Native Method) Shutting down VM threadid=1: thread exiting with uncaught exception FATAL EXCEPTION: main java.lang.NullPointerException at com.pear.draw.DrawView.touchStarted(DrawView.java:584) at com.pear.draw.DrawView.onTouchEvent(DrawView.java:560) at android.view.View.dispatchTouchEvent(View.java:5717) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGro‌​up.java:1962) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:168‌​9)
  • Pavel Dudka
    Pavel Dudka over 11 years
    yeap, you've got NullPointerException. Have you initialized everything correctly? My code is just a pseudocode w/o proper initialization routines. I assume you know what to do with it
  • pearmak
    pearmak over 11 years
    now there is no more nullpointerexception but no action can be seen when the user presses the undo button. Do you know why? Coding revised as above~
  • lenooh
    lenooh over 5 years
    path.reset() is what I was looking for.