10

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?

Android-Droid
  • 14,365
  • 41
  • 114
  • 185

5 Answers5

13

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;
}




}
Toon Krijthe
  • 52,876
  • 38
  • 145
  • 202
caiocpricci2
  • 7,714
  • 10
  • 56
  • 88
  • 1
    Thanks dude! This implementation works like a charm!!!! Just need to add `invalidate();` in `onClickUndo()` and `onClickRedo();` so you can get the result right after you click the button. – Android-Droid May 14 '12 at 07:42
  • Glad to help :) I added it to the answer! Cheers! – caiocpricci2 May 14 '12 at 08:57
  • I just saw a problem actually. When using `mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));` with your code, the result is black lines, it's not acting like a brush. And the other thing is if I try to set some effect to the stroke, it's changing all the old lines too. – Android-Droid May 14 '12 at 09:00
  • Well that's because you are setting a effect in the paint that is used for all the lines i guess. I really don't know how to help you on that, maybe update your answer with more details and then we can try to find something specific about what you need! – caiocpricci2 May 14 '12 at 21:33
  • @gameower : Hey I just tried this code. And this take me out of my problem. Now I want one more thing. I want to have canvas with one background bitmap. I tired several ways but not succeeded. Can you tell me where to write that drawBitmap method ? – Hardik Trivedi Oct 23 '12 at 10:29
  • @HardikTrivedi I strongly suggest you to ask a question in the forum and let all the community help you! I need to see some of your source so that way I can guide you. Add the link to your question here and I'll do my best to help you! – caiocpricci2 Oct 24 '12 at 10:42
  • @gameower Using your code I tried to implement Undo-Redo functionality but still I am not getting solution of, on the First Click of undo button the functionality is not working. I used Invalidate() still its not working. – anddev Jan 23 '13 at 13:05
  • @anddev Please open a new question for that, explain what is your issue and give us some code. This implementation definitely works! – caiocpricci2 Jan 23 '13 at 13:45
  • Yeh the code u provided works fine. But suppose i want to show the save images on the canvas, then i have to do this canvas.drawBitmap(bitmapToCanvas, 0f, 0f, null); inside onDraw(Canvas canvas) method. So as soon as i used this code, the undo and redo stop working, why this happen and how to solve this. – AndroidDev May 02 '13 at 08:50
  • The array only stores paths, not bitmaps. You need to keep track of both. Post it as a new question and i'm sure the community will help you more. – caiocpricci2 May 02 '13 at 09:12
  • @Android-Droid Hello can any one have solution of this problem? – Dhaval Khant May 16 '13 at 09:54
  • @DhavalKhant what problem? – caiocpricci2 May 16 '13 at 10:09
  • @gameower When using mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); with this code, the result is black lines. in api level< 11 for api level >11 setLayerType(View.LAYER_TYPE_SOFTWARE, null); this will work – Dhaval Khant May 16 '13 at 10:13
  • What is circleRadius? – MohsinSyd Feb 05 '15 at 09:29
4

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!

biquillo
  • 6,469
  • 7
  • 37
  • 40
4

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;

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Gunnar Karlsson
  • 28,350
  • 10
  • 68
  • 71
1

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]

ethan
  • 11
  • 3
0

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.

marwinXXII
  • 1,456
  • 14
  • 21