11.1B: Drawing on a Canvas object
Contents:
- What you should already KNOW
- What you will LEARN
- What you will DO
- App overview
- Task 1. Create a canvas and respond to user events
- Solution code
- Coding challenge
- Summary
- Related concept
- Learn more
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 theCanvas
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.
Task 1. Create a canvas and respond to user events
1.1 Create the CanvasExample project
- Create a CanvasExample project with the Empty Activity template. Do not add a layout file as you won't need it.
- Add the following two colors to the colors.xml file.
<color name="opaque_orange">#FFFF5500</color> <color name="opaque_yellow">#FFFFEB3B</color>
- In
styles.xml
, set the parent of the default style toNoActionBar
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
- In a separate file, create a new class called
MyCanvasView
. - Make the
MyCanvasView
class extend theView
class. - Add member variables for
Paint
andPath
objects. Importandroid.graphics.Path
for thePath
. Thepath
holds the path that you are currently drawing while the user moves their finger across the screen. - Add member variables for
Canvas
andBitmap
objects; call thesemExtraCanvas
andmExtraBitmap
, because they are not the default canvas and bitmap used in theonDraw()
method. - Add
int
variablesmDrawColor
andmBackgroundColor
.private Paint mPaint; private Path mPath; private int mDrawColor; private int mBackgroundColor; private Canvas mExtraCanvas; private Bitmap mExtraBitmap;
Add constructors to initialize the
mPath
,mPaint
, andmDrawColor
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)
}
In
MainActivity,
edit theonCreate()
method:- Create a variable
myCanvasView
of typeMyCanvasView
. - Create an instance of
MyCanvasView
and assign it tomyCanvasView
. - Set the
SYSTEM_UI_FLAG_FULLSCREEN
flag onmyCanvasView
so that the app fills the screen. - Set the
myCanvasView
view as the content view. You cannot get the size of the view in theonCreate()
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); }
- Create a variable
In
MyCanvasView
, override theonSizeChanged()
method.The
onSizeChanged()
method is called whenever a view changes size. Because the view starts out with no size, theonSizeChanged()
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 aCanvas
with theBitmap
, and fill theCanvas
with color.The width and height of this
Bitmap
are the same as the width and height of the screen. You will use thisBitmap
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); }
In
MyCanvasView
, override theonDraw()
method. All the drawing work forMyCanvasView
happens in theonDraw()
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 toonDraw()
is different than the one created in theonSizeChanged()
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); }
- 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.
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 theMotionEvent
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 thecase
statement because you do not want to callinvalidate()
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; }
- Add member variables to hold the latest x and y values, which are the starting point for the next path.
private float mX, mY;
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;
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; }
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 oflineTo()
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); } }
- Calculate the distance that has been moved (
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(); }
- 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.
- In
MyCanvasView
, add a member variable calledmFrame
that holds aRect
object. Update
onSizeChanged()
to create theRect
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); }
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); }
- Run the app. Does the frame appear? Why not?
- In
onDraw()
, move the code that draws the frame to after the call to draw the bitmap. - Run the app. Does the frame appear now? Does it still appear when you draw a sketch on the screen?
- Feel free to try drawing other shapes in
onDraw()
. Also experiment with creating a newPaint
object and drawing the frame in a different color than the path.
Solution code
Android Studio project: CanvasExample
Coding challenge
- 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()
andonSizeChanged()
methods. - Override the
onTouchEvent()
method to capture user touches and respond to them by drawing things.
Related concepts
The related concept documentation is in The Canvas class.
Learn more
Android developer documentation:
Canvas
classBitmap
classView
classPaint
classBitmap.config
configurationsPath
class- Bezier curves Wikipedia page
- Canvas and Drawables
- Graphics Architecture series of articles (advanced)