This codelab is part of the Advanced Android Development training course, developed by the Google Developers Training team. You will get the most value out of this course if you work through the codelabs in sequence.

For complete details about the course, see the Advanced Android Development overview.

Introduction

In a previous codelab, you learned the fundamentals of 2D custom drawing in Android by drawing on a Canvas in response to user input.

A more common pattern for using the Canvas class is to subclass one of the View classes, override its onDraw() and onSizeChanged() methods to draw, and override the onTouchEvent() method to handle user touches.

In this practical, you write an app that uses that pattern.

What you should already know

You should be able to:

What you'll learn

What you'll do

The CanvasExample uses a custom view to display a line in response to user touches, as shown in the screenshot below.

1.1 Create the CanvasExample project

  1. Create a CanvasExample project with the Empty Activity template. Do not add a layout file as you won't need it.
  2. Add the following two colors to the colors.xml file.
<color name="opaque_orange">#FFFF5500</color>
<color name="opaque_yellow">#FFFFEB3B</color>
  1. In styles.xml, set the parent of the default style to NoActionBar to remove the action bar, so that you can draw fullscreen.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

1.2 Create the MyCanvasView class

  1. In a separate file, create a new class called MyCanvasView.
  2. Make the MyCanvasView class extend the View class.
  3. Add member variables for Paint and Path objects. Import android.graphics.Path for the Path. The path holds the path that you are currently drawing while the user moves their finger across the screen.
  4. Add member variables for Canvas and Bitmap objects; call these mExtraCanvas and mExtraBitmap, because they are not the default canvas and bitmap used in the onDraw() method.
  5. Add int variables mDrawColor and mBackgroundColor.
private Paint mPaint;
private Path mPath;
private int mDrawColor;
private int mBackgroundColor;
private Canvas mExtraCanvas;
private Bitmap mExtraBitmap;
  1. Add constructors to initialize the mPath, mPaint, and mDrawColor variables. (You only need these two constructors of all that are available.)

See Paint documentation for a list of attributes that can be set.

Here is the code:

MyCanvasView(Context context) {
   this(context, null);
}

public MyCanvasView(Context context, AttributeSet attributeSet) {
   super(context);

   mBackgroundColor = ResourcesCompat.getColor(getResources(),
                   R.color.opaque_orange, null);
   mDrawColor = ResourcesCompat.getColor(getResources(),
           R.color.opaque_yellow, null);

   // Holds the path we are currently drawing.
   mPath = new Path();
   // Set up the paint with which to draw.
   mPaint = new Paint();
   mPaint.setColor(mDrawColor);
   // Smoothes out edges of what is drawn without affecting shape.
   mPaint.setAntiAlias(true);
   // Dithering affects how colors with higher-precision device
   // than the are down-sampled.
   mPaint.setDither(true);
   mPaint.setStyle(Paint.Style.STROKE); // default: FILL
   mPaint.setStrokeJoin(Paint.Join.ROUND); // default: MITER
   mPaint.setStrokeCap(Paint.Cap.ROUND); // default: BUTT
   mPaint.setStrokeWidth(12); // default: Hairline-width (really thin)
}
  1. In MainActivity, edit the onCreate() method:

Here is the code:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);

   MyCanvasView myCanvasView;
   // No XML file; just one custom view created programmatically.
   myCanvasView = new MyCanvasView(this);
   // Request the full available screen for layout.
   myCanvasView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
   setContentView(myCanvasView);
}
  1. In MyCanvasView, override the onSizeChanged() method.

    The onSizeChanged() method is called whenever a view changes size. Because the view starts out with no size, the onSizeChanged() method is also called after the activity first inflates the view. This method is thus the ideal place to create and set up the canvas.

    Create a Bitmap, create a Canvas with the Bitmap, and fill the Canvas with color.

    The width and height of this Bitmap are the same as the width and height of the screen. You will use this Bitmap to store the path that the user draws on the screen.
@Override
protected void onSizeChanged(int width, int height,
                            int oldWidth, int oldHeight) {
   super.onSizeChanged(width, height, oldWidth, oldHeight);
   // Create bitmap, create canvas with bitmap, fill canvas with color.
   mExtraBitmap = Bitmap.createBitmap(width, height,
                         Bitmap.Config.ARGB_8888);
   mExtraCanvas = new Canvas(mExtraBitmap);
   // Fill the Bitmap with the background color.
   mExtraCanvas.drawColor(mBackgroundColor);
}
  1. In MyCanvasView, override the onDraw() method.
    All the drawing work for MyCanvasView happens in the onDraw() method.
    In this case, you draw the bitmap that contains the path that the user has drawn. You will create and save the path in response to user motion in the next series of steps.
    Notice that the canvas that is passed to onDraw() is different than the one created in the onSizeChanged() method.
    When the screen first displays, the user has not drawn anything so the screen simply displays the colored bitmap.

Here is the code:

@Override
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   // Draw the bitmap that stores the path the user has drawn.
   // Initially the user has not drawn anything
   // so we see only the colored bitmap.
   canvas.drawBitmap(mExtraBitmap, 0, 0, null);
}
  1. Run your app. The whole screen should fill with orange color.

1.3 Respond to motion on the display

The onTouchEvent() method of the view is called whenever the user touches the display.

  1. Override the onTouchEvent() method.

Here is the code:

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

   // Invalidate() is inside the case statements because there are many
   // other types of motion events passed into this listener,
   // and we don't want to invalidate the view for those.
   switch (event.getAction()) {
       case MotionEvent.ACTION_DOWN:
           touchStart(x, y);
           // No need to invalidate because we are not drawing anything.
           break;
       case MotionEvent.ACTION_MOVE:
           touchMove(x, y);
           invalidate();
           break;
       case MotionEvent.ACTION_UP:
           touchUp();
           // No need to invalidate because we are not drawing anything.
           break;
       default:
           // Do nothing.
   }
   return true;
}
  1. Add member variables to hold the latest x and y values, which are the starting point for the next path.
private float mX, mY;
  1. Add a TOUCH_TOLERANCE float constant and set it to 4. This tolerance serves two functions:

Here is the code:

private static final float TOUCH_TOLERANCE = 4;
  1. Implement the touchStart() method.

Here is the code:

private void touchStart(float x, float y) {
   mPath.moveTo(x, y);
   mX = x;
   mY = y;
}
  1. Add the touchMove() method.

Here is the code:

private void touchMove(float x, float y) {
   float dx = Math.abs(x - mX);
   float dy = Math.abs(y - mY);
   if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
       // QuadTo() adds a quadratic bezier from the last point,
       // approaching control point (x1,y1), and ending at (x2,y2).
       mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
       // Reset mX and mY to the last drawn point.
       mX = x;
       mY = y;
       // Save the path in the extra bitmap,
       // which we access through its canvas.
       mExtraCanvas.drawPath(mPath, mPaint);
   }
}
  1. Finally, add the touchUp() method. Reset the path so it doesn't get drawn again when you draw more lines on the screen.

Here is the code:

private void touchUp() {
   // Reset the path so it doesn't get drawn again.
   mPath.reset();
}
  1. Run your app. When the app opens, use your finger to draw. (Rotate the device to clear the screen.)

1.4 Draw a frame around the sketch

As the user draws on the screen, your app constructs the path and saves it in the bitmap mExtraBitmap. The onDraw() method displays the extra bitmap in the view's canvas. You can do more drawing in onDraw() if you want. For example, you could draw shapes after drawing the bitmap.

In this step you will draw a frame around the edge of the picture.

  1. In MyCanvasView, add a member variable called mFrame that holds a Rect object.
  2. Update onSizeChanged() to create the Rect that will be used for the frame.
@Override
protected void onSizeChanged(int width, int height,
       int oldWidth, int oldHeight) {
   // rest of method is here ...

   // Calculate the rect a frame around the picture.
   int inset = 40;
   mFrame = new Rect (inset, inset, width - inset, height - inset);
}
  1. Update onDraw() to draw a rectangle inset slightly from the edge of the frame. Draw the frame before drawing the bitmap:
@Override
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);

   // Draw a frame around the picture.
   canvas.drawRect(mFrame, mPaint);

   // Draw the bitmap that has the saved path.
   canvas.drawBitmap(mExtraBitmap, 0, 0, null);
}
  1. Run the app. Does the frame appear? Why not?
  2. In onDraw(), move the code that draws the frame to after the call to draw the bitmap.
  3. Run the app. Does the frame appear now? Does it still appear when you draw a sketch on the screen?
  4. Feel free to try drawing other shapes in onDraw(). Also experiment with creating a new Paint object and drawing the frame in a different color than the path.

Android Studio project: CanvasExample

The related concept documentation is in 11.1 The Canvas class.

Android developer documentation:

To see all the codelabs in the Advanced Android Development training course, visit the Advanced Android Development codelabs landing page.