Saturday, November 13, 2010

Drawing with Canvas in Android Renewed

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

Back one year ago, i blogged about Drawing with Canvas in Android, at that time i was creating a drawing app that never get released (lazy me). Now im planning to redo everything and share my learnings as i go on (I'm still doing the Shoot and Learn on the side). From the simple tutorial on how to draw canvas in android, lets explode this area further.

Introduction
Based on the Drawing with Canvas in Android lets put the SurfaceView into a separate class (you should know how to use custom view), create a DrawingPath class which would contain our Path and Paint object, and create a DrawingActivity where our touch events, buttons to change colors are located.

Notes
• The package i would use through this tutorial is com.almondmendoza.drawings
• The individual source are located at http://goo.gl/5GulF while the full ones is at https://sites.google.com/site/tutorialsformobileprogramming/android-tutorials/android-files
• In this tutorial we wont handle clearing up the images, undos/redos and screen rotations (all in future articles :) )


What Do I Need
Drawing Activity
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ....
>
    <com.almondmendoza.drawings.DrawingSurface
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/drawingSurface"
    />
    <LinearLayout
            .....
    >
    ......
    </LinearLayout>
</RelativeLayout>


Main Activity
Intent drawIntent = new Intent(this, DrawingActivity.class);
startActivity( drawIntent);


DrawingPath.java
public class DrawingPath {
  public Path path;
  public Paint paint;
}


Drawing Activity
public class DrawingActivity extends Activity implements View.OnTouchListener{
  private DrawingSurface drawingSurface;
  private DrawingPath currentDrawingPath;
  private Paint currentPaint;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.drawing_activity);
    setCurrentPaint();
    drawingSurface = (DrawingSurface) findViewById(R.id.drawingSurface);
    drawingSurface.setOnTouchListener(this);
  }

  private void setCurrentPaint(){
    currentPaint = new Paint();
    currentPaint.setDither(true);
    currentPaint.setColor(0xFFFFFF00);
    currentPaint.setStyle(Paint.Style.STROKE);
    currentPaint.setStrokeJoin(Paint.Join.ROUND);
    currentPaint.setStrokeCap(Paint.Cap.ROUND);
    currentPaint.setStrokeWidth(3);
  }

  public boolean onTouch(View view, MotionEvent motionEvent) {
    if(motionEvent.getAction() == MotionEvent.ACTION_DOWN){
      currentDrawingPath = new DrawingPath();
      currentDrawingPath.paint = currentPaint;
      currentDrawingPath.path = new Path();
      currentDrawingPath.path.moveTo(motionEvent.getX(), motionEvent.getY());
      currentDrawingPath.path.lineTo(motionEvent.getX(), motionEvent.getY());
    }else if(motionEvent.getAction() == MotionEvent.ACTION_MOVE){
      currentDrawingPath.path.lineTo(motionEvent.getX(), motionEvent.getY());
    }else if(motionEvent.getAction() == MotionEvent.ACTION_UP){
      currentDrawingPath.path.lineTo(motionEvent.getX(), motionEvent.getY());
      drawingSurface.addDrawingPath(currentDrawingPath);
    }
    return true;
  }

  public void onClick(View view){
    switch (view.getId()){
      case R.id.colorRedBtn:
        currentPaint = new Paint();
        currentPaint.setDither(true);
        currentPaint.setColor(0xFFFF0000);
        currentPaint.setStyle(Paint.Style.STROKE);
        currentPaint.setStrokeJoin(Paint.Join.ROUND);
        currentPaint.setStrokeCap(Paint.Cap.ROUND);
        currentPaint.setStrokeWidth(3);
      break;
      case R.id.colorBlueBtn:
        currentPaint = new Paint();
        currentPaint.setDither(true);
        currentPaint.setColor(0xFF00FF00);
        currentPaint.setStyle(Paint.Style.STROKE);
        currentPaint.setStrokeJoin(Paint.Join.ROUND);
        currentPaint.setStrokeCap(Paint.Cap.ROUND);
        currentPaint.setStrokeWidth(3);
      break;
      case R.id.colorGreenBtn:
        currentPaint = new Paint();
        currentPaint.setDither(true);
        currentPaint.setColor(0xFF0000FF);
        currentPaint.setStyle(Paint.Style.STROKE);
        currentPaint.setStrokeJoin(Paint.Join.ROUND);
        currentPaint.setStrokeCap(Paint.Cap.ROUND);
        currentPaint.setStrokeWidth(3);
      break;
    }
  }
}



Drawing Surface
public class DrawingSurface extends SurfaceView implements SurfaceHolder.Callback {
  private Boolean _run;
  protected DrawThread thread;

  public DrawingSurface(Context context, AttributeSet attrs) {
...
  }

  public void addDrawingPath (DrawingPath drawingPath){
    thread.addDrawingPath(drawingPath);
  }

  class DrawThread extends  Thread{
    private SurfaceHolder mSurfaceHolder;
    private List mDrawingPaths;
    public DrawThread(SurfaceHolder surfaceHolder){
      mSurfaceHolder = surfaceHolder;
      mDrawingPaths = Collections.synchronizedList(new ArrayList());
    }

...

    public void addDrawingPath(DrawingPath drawingPath){
      mDrawingPaths.add( drawingPath );
    }

    @Override
    public void run() {
      Canvas canvas = null;
      while (_run){
        try{
          canvas = mSurfaceHolder.lockCanvas(null);
          synchronized(mDrawingPaths) {
            Iterator i = mDrawingPaths.iterator();
            while (i.hasNext()){
              final DrawingPath drawingPath = (DrawingPath) i.next();
              canvas.drawPath(drawingPath.path, drawingPath.paint);
            }
          }
        } finally {
          mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
      }
    }
  }
....
}






Explanation
<com.almondmendoza.drawings.DrawingSurface
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:id="@+id/drawingSurface"
/>

We start off by using our custom view in our xml, its as simple as creating a class and using it on the layout.


public class DrawingPath {
  public Path path;
  public Paint paint;
}

From Drawing with Canvas in Android we learn to put an ArrayList of Path which have a single color, for us to have different colors on different path, we create a class which would be an List in our DrawThread later on.


private Paint currentPaint;
....
private void setCurrentPaint(){
  currentPaint = new Paint();
  currentPaint.setDither(true);
  currentPaint.setColor(0xFFFFFF00);
  currentPaint.setStyle(Paint.Style.STROKE);
  currentPaint.setStrokeJoin(Paint.Join.ROUND);
  currentPaint.setStrokeCap(Paint.Cap.ROUND);
  currentPaint.setStrokeWidth(3);
}

Here we create a Paint that would serve as our current paint, and set the currentPaint at the start of our activity.


  public void onClick(View view){
  switch (view.getId()){
    case R.id.colorRedBtn:
      currentPaint = new Paint();
      currentPaint.setDither(true);
      currentPaint.setColor(0xFFFF0000);
...
    break;
    case R.id.colorBlueBtn:
      currentPaint = new Paint();
      currentPaint.setDither(true);
      currentPaint.setColor(0xFF00FF00);
...

This is how we change the currentPaint's color, since we use the Paint object, you could use any of its property/methods thus making every Path beautiful :)


  public boolean onTouch(View view, MotionEvent motionEvent) {
  if(motionEvent.getAction() == MotionEvent.ACTION_DOWN){
    currentDrawingPath = new DrawingPath();
    currentDrawingPath.paint = currentPaint;
    currentDrawingPath.path = new Path();
    ....
    drawingSurface.addDrawingPath(currentDrawingPath);
  }
  return true;
}

In the previous tutorial, we bind our onTouch on the surfaceView directly, now we bind it in our activity, this way we could control the logic of the touches better (where you would apply your own shared touch library) and you could do things that doesnt harm the SurfaceView/Canvas.


public DrawThread(SurfaceHolder surfaceHolder){
  mSurfaceHolder = surfaceHolder;
  mDrawingPaths = Collections.synchronizedList(new ArrayList());
}

As stated earlier we would create a List of DrawingPath, but in able to manipulate it while our thread is running we need to make sure that its thread-safe thus we call Collections.synchronizedList(new ArrayList()); (I'm not sure about this since i never used threads in other language besides Javascript's webworker)


  public void addDrawingPath (DrawingPath drawingPath){
    thread.addDrawingPath(drawingPath);
  }

From our activity we would pass a DrawingPath to our surfaceView which we would then pass it to our thread, we do this so that our thread would not be accessible to the outside world of our DrawingSurface, which i believe is better for we can inject logics that would deal with our SurfaceView before passing it to our thread.


public void addDrawingPath(DrawingPath drawingPath){
  mDrawingPaths.add( drawingPath );
}

From our DrawingSurface to our DrawThread, we pass the a DrawingPath and add it to our list


synchronized(mDrawingPaths) {
  Iterator i = mDrawingPaths.iterator();
  while (i.hasNext()){
    final DrawingPath drawingPath = (DrawingPath) i.next();
    canvas.drawPath(drawingPath.path, drawingPath.paint);
  }
}

Here we use the Iterator class to iterate over our DrawingPath list and draw our Path on the canvas.

Conclusion
Now we learn how to better put our codes and make sure it can be scale up (im a webdeveloper, ahha) in the future.

Hope this helps :)
Post a Comment