11.1B: Drawing on a Canvas object

Contents:

In a previous practical, 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:

  • Create apps with Android Studio and run them on a physical or virtual mobile device.
  • Add event handlers to views.
  • Create a custom View.
  • Create a bitmap and associate it with a view. Create a canvas for a bitmap. Create and customize a Paint object for styling. Draw on the Canvas and refresh the display.

What you will LEARN

You will learn how to:

  • Create a custom View, capture the user's motion event, and interpret it to draw lines onto the canvas.

What you will DO

  • Create an app that draws lines on the screen in response to motion events such as the user touching the screen.

App overview

The CanvasExample uses a custom view to display a line in response to user touches, as shown in the screenshot below.  Screenshot for the CanvasExample example app with some drawn lines

Task 1. Create a canvas and respond to user events

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>
    
  3. 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;
    
  6. Add constructors to initialize the mPath, mPaint, and mDrawColor variables. (You only need these two constructors of all that are available.)

    • Paint.Style specifies if the primitive being drawn is filled, stroked, or both (in the same color).
    • Paint.Join specifies how lines and curve segments join on a stroked path.
    • Paint.Cap specifies how the beginning and ending of stroked lines and paths.

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:

    • Create a variable myCanvasView of type MyCanvasView.
    • Create an instance of MyCanvasView and assign it to myCanvasView.
    • Set the SYSTEM_UI_FLAG_FULLSCREEN flag on myCanvasView so that the app fills the screen.
    • Set the myCanvasView view as the content view. You cannot get the size of the view in 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);
    }
    
  2. 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);
    }
    
  3. 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);
    }
    
  4. 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.

    • Get the x and y coordinates of the event.
    • Use a switch statement to handle the events you are interested in. There are many more touch events available. See the MotionEvent class documentation for a full list.
    • Call a utility method for each type of event. You implement those methods next.
    • You must call invalidate() to redraw the view after it changes. You call it inside the case statement because you do not want to call invalidate() when the event is not one of interest.

    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;
    }
    
  2. Add member variables to hold the latest x and y values, which are the starting point for the next path.
     private float mX, mY;
    
  3. Add a TOUCH_TOLERANCE float constant and set it to 4. This tolerance serves two functions:

    • If the finger has barely moved, there is no need to draw.
    • Using the path, it is not necessary to draw every pixel and request a refresh of the display. Instead, you can interpolate for much better performance.

    Here is the code:

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

    • When the user starts to draw a new line, set the beginning of the contour (line) to x, y and save the beginning coordinates.

    Here is the code:

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

    • Calculate the distance that has been moved (dx, dy).
    • If the movement was further than the touch tolerance, add a segment to the path.
    • Set the starting point for the next segment to the endpoint of this segment.
    • Using quadTo() instead of lineTo() creates a smoothly drawn line without corners. See Bezier Curves.

    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);
    }
    }
    
  6. 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();
    }
    
  7. 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);
     }
    
  3. 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);
     }
    
  4. Run the app. Does the frame appear? Why not?
  5. In onDraw(), move the code that draws the frame to after the call to draw the bitmap.
  6. Run the app. Does the frame appear now? Does it still appear when you draw a sketch on the screen?
  7. 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.

Solution code

Android Studio project: CanvasExample

Coding challenge

Note: All coding challenges are optional.
  • Create an app that lets the user draw overlapping rectangles. First, implement it so that tapping the screen creates the rectangles.
  • Add functionality where the rectangle starts at a very small size. If the user drags their finger, let the rectangle increase in size until the user lifts the finger off the screen.

Summary

  • A common pattern for working with a canvas is to create a custom view and override the onDraw() and onSizeChanged() methods.
  • Override the onTouchEvent() method to capture user touches and respond to them by drawing things.

The related concept documentation is in The Canvas class.

Learn more

Android developer documentation:

results matching ""

    No results matching ""