Android FingerPaint Undo/Redo implementation

2019-02-02 16:56发布

问题:

I'm working on a test project which is something similar to FingerPaint example in Android SDK Demos. I was trying to implement undo/redo functionality in my project,but the things that I tried didn't work as I expect. I find some questions similar to this over internet and here,but they didn't help me, that's why I'm asking a new question.

Here is some idea what I'm doing actually :

    public class MyView extends View {

    //private static final float MINP = 0.25f;
    //private static final float MAXP = 0.75f;



    private Path    mPath;
    private Paint   mBitmapPaint;

    public MyView(Context c) {
        super(c);

        mPath = new Path();
        mBitmapPaint = new Paint(Paint.DITHER_FLAG);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

        mCanvas = new Canvas(mBitmap);
        mCanvas.drawColor(Color.WHITE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);

        canvas.drawPath(mPath, mPaint);
    }

    private float mX, mY;
    private static final float TOUCH_TOLERANCE = 4;

    private void touch_start(float x, float y) {
        mPath.reset();
        mPath.moveTo(x, y);
        mX = x;
        mY = y;
    }
    private void touch_move(float x, float y) {
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);

        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
            mX = x;
            mY = y;
        }
    }
    private void touch_up() {

        mPath.lineTo(mX, mY);
        // commit the path to our offscreen
        mCanvas.drawPath(mPath, mPaint);
        // kill this so we don't double draw
        mPath.reset();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touch_start(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                touch_move(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                touch_up();
                invalidate();
                break;
        }
        return true;
    }
}

Any suggestions/ideas/examples which is the best way to implement this kind of functionality on my project?

回答1:

I don't know if this is what you had in mind but it's how i am doing it. Instead of storing it in only one path, you store an array with all the paths, like this the user can draw many lines, with a small modification you can add multi touch too.

To make the undo and redo, just remove or add the last path path from the paths variable and store them in a new array. Something like:

public void onClickUndo () { 
    if (paths.size()>0) { 
       undonePaths.add(paths.remove(paths.size()-1))
       invalidate();
     }
    else
     //toast the user 
}

public void onClickRedo (){
   if (undonePaths.size()>0) { 
       paths.add(undonePaths.remove(undonePaths.size()-1)) 
       invalidate();
   } 
   else 
     //toast the user 
}

Here is my modified panel, I cant try it right now but the methods above should work! Hope it helps! (there are few extra variables just remove them :)

private ArrayList<Path> undonePaths = new ArrayList<Path>(); 
public class DrawingPanel extends View implements OnTouchListener {

private Canvas  mCanvas;
private Path    mPath;
private Paint   mPaint,circlePaint,outercirclePaint;   
private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>(); 
private float xleft,xright,xtop,xbottom;

public DrawingPanel(Context context) {
    super(context);
    setFocusable(true);
    setFocusableInTouchMode(true);

    this.setOnTouchListener(this);


    circlePaint = new Paint();
    mPaint = new Paint();
    outercirclePaint = new Paint();
    outercirclePaint.setAntiAlias(true);
    circlePaint.setAntiAlias(true);
    mPaint.setAntiAlias(true);        
    mPaint.setColor(0xFFFFFFFF);
    outercirclePaint.setColor(0x44FFFFFF);
    circlePaint.setColor(0xAADD5522);
    outercirclePaint.setStyle(Paint.Style.STROKE);
    circlePaint.setStyle(Paint.Style.FILL);        
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setStrokeWidth(6);
    outercirclePaint.setStrokeWidth(6);        
    mCanvas = new Canvas();
    mPath = new Path();
    paths.add(mPath);             


    cx = 400*DrawActivity.scale;
    cy = 30*DrawActivity.scale;
    circleRadius = 20*DrawActivity.scale;
    xleft = cx-10*DrawActivity.scale;
    xright = cx+10*DrawActivity.scale;
    xtop = cy-10*DrawActivity.scale;
    xbottom = cy+10*DrawActivity.scale;

}


public void colorChanged(int color) {
    mPaint.setColor(color);
}


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {            

        for (Path p : paths){
            canvas.drawPath(p, mPaint);
        }

    }

    private float mX, mY;
    private static final float TOUCH_TOLERANCE = 0;

    private void touch_start(float x, float y) {
        mPath.reset();
        mPath.moveTo(x, y);
        mX = x;
        mY = y;
    }
    private void touch_move(float x, float y) {
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
            mX = x;
            mY = y;
        }
    }
    private void touch_up() {
        mPath.lineTo(mX, mY);
        // commit the path to our offscreen
        mCanvas.drawPath(mPath, mPaint);
        // kill this so we don't double draw            
        mPath = new Path();
        paths.add(mPath);
    }



@Override
public boolean onTouch(View arg0, MotionEvent event) {
      float x = event.getX();
      float y = event.getY();

      switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN:
              if (x <= cx+circleRadius+5 && x>= cx-circleRadius-5) {
                  if (y<= cy+circleRadius+5 && cy>= cy-circleRadius-5){
                      paths.clear();
                      return true;
                      }
              }
              touch_start(x, y);
              invalidate();
              break;
          case MotionEvent.ACTION_MOVE:
              touch_move(x, y);
              invalidate();
              break;
          case MotionEvent.ACTION_UP:
              touch_up();
              invalidate();
              break;
      }
      return true;
}




}


回答2:

The best solution is that you implement your own Undo / Redo engine.

  1. Save into an array every single action you perform in an array (ie. [0] circle in position x1, y1, [1] line from x2, y2 to x3, y3, etc)

  2. draw

  3. if you need to undo, clear the canvas and repaint all the n - 1 actions from [0] to [n - 1]

  4. if you undo more just have to to paint from [0] to [n - 2] etc

I hope it gives you a hint

Cheers!



回答3:

One way to implement a do/redo functionality is to encapsulate a method call and all info needed for the call in an object so that you can store it and call it later - the Command Pattern.

In this pattern, each action has its own object: DrawCircleCommand, DrawPathCommand, FillColorCommand, etc. In each object the draw() method is implemented in a unique way but is always called as draw(Canvas canvas) which allows the CommandManager to iterate through the commands. To undo you iterate over the objects calling the undo() method;

Each command object implements an Interface

public interface IDrawCommand {  
  public void draw(Canvas canvas);  
  public void undo();  
}  

An object would look like:

public class DrawPathCommand implements IDrawCommand{  
  public Path path;  
  public Paint paint;  

  public void setPath(path){
    this.path = path
  }

  public void draw(Canvas canvas) {  
    canvas.drawPath( path, paint );  
  }  

  public void undo() {  
   //some  action to remove the path
 }  

}

Your commands are added to the CommandManager:

mCommandManager.addCommand(IDrawCommand command)

and to undo a command you just call:

mCommandManager.undo();

The CommandManager stores the commands in a data structure e.g. list that allows it to iterative over the command objects.

You can find a complete tutorial here on how to implement the Command Pattern with do/undo for Canvas drawing on Android.

Here is a another tutorial on how implement the Command Pattern in Java;



回答4:

Fortunately,I solved this today. I find a way to do this.Something like:

private ArrayList<Path> paths       = new ArrayList<>();
private ArrayList<Path> undonePaths = new ArrayList<>();

public void undo() {
    if (paths.size() > 0) {
        LogUtils.d("undo " + paths.size());
        clearDraw();
        undonePaths.add(paths.remove(paths.size() - 1));
        invalidate();
    }
}

public void redo() {
    if (undonePaths.size() > 0) {
        LogUtils.d("redo " + undonePaths.size());
        clearDraw();
        paths.add(undonePaths.remove(undonePaths.size() - 1));
        invalidate();
    }
}


public void clearDraw() {
    mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
    mCanvas.setBitmap(mBitmap);
    invalidate();
}

public void clear() {
    paths.clear();
    undonePaths.clear();
    invalidate();
}

in Activity.

private DrawView            mDrawView;

if (v == mIvUndo) {
        mDrawView.undo();

    } else if (v == mIvRedo) {
        mDrawView.redo();

in xml

<com.cinread.note.view.DrawView
        android:id="@+id/paintView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

If you have a better solution you can contact me.

[here is my blog that I write in

http://blog.csdn.net/sky_pjf/article/details/51086901]



回答5:

I think that in this case you can use two canvases. You know when user starts drawing and when finishes. So, in touch_start you can create copy of your current canvas. When user clicks undo you replace your current canvas with previously saved.

This should guarantee that you will have previous state of picture, but I am not sure about performance.