Header Ads

Drawing with Canvas in Android, Undo/Redo with Command Pattern

Update - Nov 21, 2010
Created a series over this topic over at Drawing with Canvas Series, more articles would appear in the future :)

Continuing our Drawing with Canvas in Android, lets implement Undo and Redo with Command Pattern

Introduction
We draw and make mistake, and when we do we may want to undo it and when we undo we might want to redo it. Lets use a common design pattern in dealing with undo (I believe this is a modified command pattern for the command manager but i might be wrong)

Notes
• This is a continuation of Drawing with Canvas in Android Renewed
• The files are uploaded in http://goo.gl/ecHpE
• The project was build in IntelliJ and it should be easy to import to Eclipse


What Do I Need
ICanvasCommand
public interface ICanvasCommand {
  public void draw(Canvas canvas);
  public void undo();
}


DrawingPath
public class DrawingPath implements ICanvasCommand{
  public Path path;
  public Paint paint;

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

  public void undo() {
    //Todo this would be changed later
  }
}


CommandManager
public class CommandManager {
  private List currentStack;
  private List redoStack;

  public CommandManager(){
    currentStack = Collections.synchronizedList(new ArrayList());
    redoStack = Collections.synchronizedList(new ArrayList());
  }

  public void addCommand(DrawingPath command){
    redoStack.clear();
    currentStack.add(command);
  }

  public void undo (){
    final int length = currentStackLength();
    if ( length > 0) {
      final DrawingPath undoCommand = currentStack.get( length - 1 );
      currentStack.remove( length - 1 );
      undoCommand.undo();
      redoStack.add( undoCommand );
    }
  }

  public int currentStackLength(){
    final int length = currentStack.toArray().length;
    return length;
  }

  public void executeAll( Canvas canvas){
    if( currentStack != null ){
      synchronized( currentStack ) {
        final Iterator i = currentStack.iterator();
        while ( i.hasNext() ){
          final DrawingPath drawingPath = (DrawingPath) i.next();
          drawingPath.draw( canvas );
        }
      }
    }
  }

  public boolean hasMoreRedo(){
    return redoStack.toArray().length > 0;
  }

  public boolean hasMoreUndo(){
    return currentStack.toArray().length > 0;
  }

  public void redo(){
    final int length = redoStack.toArray().length;
    if ( length > 0) {
      final DrawingPath redoCommand = redoStack.get( length - 1 );
      redoStack.remove( length - 1 );
      currentStack.add( redoCommand );
    }
  }
}


DrawingSurface
public class DrawingSurface extends SurfaceView implements SurfaceHolder.Callback {
  private CommandManager commandManager;

  public DrawingSurface(Context context, AttributeSet attrs) {
    ...
    commandManager = new CommandManager();
    ...
  }

  class DrawThread extends Thread{
    ...
    @Override
    public void run() {
      Canvas canvas = null;
      while (_run){
        try{
          canvas = mSurfaceHolder.lockCanvas(null);
          canvas.drawColor(0, PorterDuff.Mode.CLEAR);
          commandManager.executeAll(canvas);
        ....
  }

  public void addDrawingPath (DrawingPath drawingPath){
    commandManager.addCommand(drawingPath);
  }

  public boolean hasMoreRedo(){
    return commandManager.hasMoreRedo();
  }

  public void redo(){
    commandManager.redo();
  }

  public void undo(){
    commandManager.undo();
  }

  public boolean hasMoreUndo(){
    return commandManager.hasMoreUndo();
  }
}


DrawingActivity
See the source should be simple enough to be understood :)



Ads from Amazon:
Explanation
public interface ICanvasCommand {
  public void draw(Canvas canvas);
  public void undo();
}

In undo command pattern based we need need 2 methods, one is execute and the other is undo (we wont use undo currently)


public class DrawingPath implements ICanvasCommand{
  public Path path;
  public Paint paint;
  public void draw(Canvas canvas) {
    canvas.drawPath( path, paint );
  }
  public void undo() {
    //Todo this would be changed later
  }
}

Let us now implement our interface, the code should be easy to understand. canvas.drawPath( path, paint) would draw our path with the object's paint into our class.


currentStack = Collections.synchronizedList(new ArrayList());
redoStack = Collections.synchronizedList(new ArrayList());

We create 2 List (Its actually a Stack but seems Stack cannot be thread-safe and I dont freakin understand why there are a lot of Array variation in Java, its so messy), one called currentStack for the current paths, another is redoStack where we put the undo commands so we could do redos.


public void addCommand(DrawingPath command){
  redoStack.clear();
  currentStack.add(command);
}

Everytime we add a new command, we should clear the redoStack


Ads from Amazon:
public void undo (){
  final int length = currentStackLength();
  if ( length > 0) {
    final DrawingPath undoCommand = currentStack.get( length - 1 );
    currentStack.remove( length - 1 );
    undoCommand.undo();
    redoStack.add( undoCommand );
  }
}

When we do an undo, we pop the last command from the currentStack and push it to our redoStack. As you can see we implemented undoCommand.undo() we did this incase in the future we have some logics added to our undo function


public void executeAll( Canvas canvas){
  if( currentStack != null ){
    synchronized( currentStack ) {
      final Iterator i = currentStack.iterator();
      while ( i.hasNext() ){
        final DrawingPath drawingPath = (DrawingPath) i.next();
        drawingPath.draw( canvas );
      }
    }
  }
}

We loop through our currentStack and do the execute part (.draw) from our command pattern.


canvas = mSurfaceHolder.lockCanvas(null);
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
commandManager.executeAll(canvas);

In order for our undo/redo to work we have to somehow clear the canvas and drawColor(0, PorterDuff.Mode.CLEAR) helps us with that


Some of the codes are, i believe, logical thus i skip the explanation on them.

No comments:

Powered by Blogger.