Canvas Drawing

Appportunity

Build your own Android App Dev Empire

Get Instant Access

Now that we've explored how widgets allocate the space on the screen in which they draw themselves, we can turn to coding some widgets that actually do some drawing.

The Android framework handles drawing in a way that should be familiar, now that you've read about measurement and arrangement. When some part of the application determines that the current screen drawing is stale because some state has changed, it calls the View method invalidate. This call causes a redraw event to be added to the event queue.

Eventually, when that event is processed, the framework calls the draw method at the top of the view tree. This time the call is propagated preorder, with each view drawing itself before it calls its children. This means that leaf views are drawn after their parents, which are, in turn, drawn after their parents. Views that are lower in the tree appear to be drawn on top of those nearer the root of the tree.

The draw method calls onDraw, which a subclass overrides to implement its custom rendering. When your widget's onDraw method is called, it must render itself according to the current application state and return. It turns out, by the way, that neither View.draw nor ViewGroup.dispatchDraw (responsible for the traversal of the view tree) is final! Override them at your peril!

In order to prevent extra painting, the framework maintains some state information about the view, called the clip rectangle. A key concept in the UI framework, the clip rectangle is part of the state passed in calls to a component's graphical rendering methods. It has a location and size that can be retrieved and adjusted through methods on the Canvas, and it acts like a stencil through which a component does all of its drawing. By correctly setting the size, shape, and location of the clip rectangle aperture, the framework can prevent a component from drawing outside its boundaries or redrawing regions that are already correctly drawn.

Before proceeding to the specifics of drawing, let's again put the discussion in the context of Android's single-threaded MVC design pattern. There are two essential rules:

• Drawing code should be inside the onDraw method. Your widget should draw itself completely, reflecting the program's current state, when onDraw is invoked.

• A widget should draw itself as quickly as possible when onDraw is invoked. The middle of the call to onDraw is no time to run a complex database query or to determine the status of some distant networked service. All the state you need to draw should be cached and ready for use at drawing time. Long-running tasks should use a separate thread and the Handler mechanism described in "Advanced Wiring: Focus and Threading" on page 179. The model state cached in the view is sometimes called the view-model.

The Android UI framework uses four main classes in drawing. If you are going to implement custom widgets and do your own drawing, you will want to become very familiar with them:

Canvas (a subclass of android.graphics.Canvas)

The canvas has no complete analog in real-life materials. You might think of it as a complex easel that can orient, bend, and even crumple the paper on which you are drawing in interesting ways. It maintains the clip rectangle, the stencil through which you paint. It can also scale drawings as they are drawn, like a photographic enlarger. It can even perform other transformations for which material analogs are more difficult to find: mapping colors and drawing text along paths. Paint (a subclass of android.graphics.Paint)

This is the medium with which you will draw. It controls the color, transparency, and brush size for objects painted on the canvas. It also controls font, size, and style when drawing text.

Bitmap (a subclass of android.graphics.Bitmap)

This is the paper you are drawing on. It holds the actual pixels that you draw. Drawables (likely a subclass of android.graphics.drawable.Drawable)

This is the thing you want to draw: a rectangle or image. Although not all of the things that you draw are Drawables (text, for instance, is not), many, especially the more complex ones, are.

Example 12-1 used only the Canvas, passed as a parameter to onDraw, to do its drawing. In order to do anything more interesting, we will need Paint, at the very least. Paint provides control over the color and transparency (alpha) of the graphics drawn with it. Paint has many, many other capabilities, some of which are described in "Bling" on page 243. Example 12-2, however, is enough to get you started. Explore the class documentation for other useful attributes.

The graphic created by the code in the example is shown in Figure 12-1.

Example 12-2. Using Paint ^Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE);

paint.setColor(Color.RED); paint.setStrokeWidth(lo); canvas.drawLine(56, 0, 56, 100, paint);

paint.setColor(Color.GREEN); paint.setStrokeWidth(5);

for (int y = 30, alpha = 255; alpha > 2; alpha >>= 1, y += 10) { paint.setAlpha(alpha); canvas.drawLine(0, y, 100, y, paint);

Figure 12-1. Using Paint

With the addition of Paint, we are prepared to understand most of the other tools necessary to create a useful widget. Example 12-3, for instance, is the widget used previously in Example 10-7. While still not very complex, it demonstrates all the pieces of a fully functional widget. It handles layout and highlighting, and reflects the state of the model to which it is attached.

Example 12-3. Dot widget package com.oreilly.android.intro.view;

import android.content.Context;

import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style;

import android.view.View;

import com.oreilly.android.intro.model.Dot; import com.oreilly.android.intro.model.Dots;

public class DotView extends View {

private final Dots dots;

* @param context the rest of the application

public DotView(Context context, Dots dots) { super(context); this.dots = dots; setMinimumWidth(180); setMinimumHeight(200); setFocusable(true);

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(

getSuggestedMinimumWidth(), getSuggestedMinimumHeight());

/** @see android.view.View#onDraw(android.graphics.Canvas) */ ^Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE);

Paint paint = new Paint(); paint.setStyle(Style.STROKE);

paint.setColor(hasFocus() ? Color.BLUE : Color.GRAY); canvas.drawRect(0, 0, getWidth() - 1, getHeight() -1, paint);

paint.setStyle(Style.FILL); for (Dot dot : dots.getDots()) { paint.setColor(dot.getColor()); canvas.drawCircle( dot.getX(), dot.getY(), dot.getDiameter(), paint);

As with Paint, we have only enough space to begin an exploration of Canvas methods. There are two groups of functionality, however, that are worth special notice.

Drawing text

The most important Canvas methods are those used to draw text. Although some Canvas functionality is duplicated in other places, text-rendering capabilities are not. In order to put text in your widget, you will have to use the Canvas (or, of course, subclass some other widget that uses it).

Canvas methods for rendering text come in pairs: three sets of two signatures. Example 12-4 shows one of the pairs.

Example 12-4. A pair of text drawing methods public void drawText(String text, float x, float y, Paint paint)

public void drawText(char[] text, int index, int count, float x, float y, Paint paint)

There are several pairs of methods. In each pair, the first of the two methods in the pair uses String, and the second uses three parameters to describe the text: an array of char, the index indicating the first character in that array to be drawn, and the number of total characters in the text to be rendered. In some cases, there are additional convenience methods.

Example 12-5 contains an onDraw method that demonstrates the use of the first style of each of the three pairs of text rendering methods. The output is shown in Figure 12-2.

Android

Figure 12-2. Output from three ways of drawing text Example 12-5. Three ways of drawing text @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE);

paint.setColor(Color.RED); canvas.drawText("Android", 25, 30, paint);

path.addArc(new RectF(10, 50, 90, 200), 240, 90); paint.setColor(Color.CYAN);

canvas.drawTextOnPath("Android", path, 0, 0, paint);

float[] pos = new float[] { 20, 80, 29, 83, 36, 80, 46, 83, 52, 80, 62, 83, 68, 80

paint.setColor(Color.GREEN); canvas.drawPosText("Android", pos, paint);

As you can see, the most elementary of the pairs, drawText, simply draws text at the passed coordinates. With DrawTextOnPath, on the other hand, you can draw text along any Path. The example path is just an arc. It could just as easily have been a line drawing or a Bezier curve.

For those occasions on which even DrawTextOnPath is insufficient, Canvas offers DrawPosText, which lets you specify the exact position of each character in the text. Note that the character positions are specified by alternating array elements: x1,y1,x2,y2, and so on.

Matrix transformations

The second interesting group of Canvas methods are the Matrix transformations and their related convenience methods, rotate, scale, and skew. These methods transform what you draw in ways that will immediately be recognizable to those familiar with 3D

graphics. They allow a single drawing to be rendered in ways that can make it appear as if the viewer were moving with respect to the objects in the drawing.

The small application in Example 12-6 demonstrates the Canvas's coordinate transformation capabilities.

Example 12-6. Using a Canvas import android.app.Activity;

import android.content.Context;

import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect;

import android.os.Bundle;

import android.view.View;

import android.widget.LinearLayout;

public class TranformationalActivity extends Activity {

private interface Transformation { void transform(Canvas canvas); String describe();

private static class TransfomedViewWidget extends View { private final Transformation transformation;

public TransfomedViewWidget(Context context, Transformation xform) { super(context);

transformation = xform;

setMinimumWidth(l60); setMinimumHeight(l05);

^Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(

getSuggestedMinimumWidth(), getSuggestedMinimumHeight());

^Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE);

canvas.save();

transformation.transform(canvas);

paint.setTextSize(12); paint.setColor(Color.GREEN); canvas.drawText("Hello", 40, 55, paint);

paint.setTextSize(16); paint.setColor(Color.RED); canvas.drawText("Android", 35, 65, paint);

paint.setColor(Color.BLACK); paint.setStyle(Paint.Style.STROKE); Rect r = canvas.getClipBounds(); canvas.drawRect(r, paint);

paint.setTextSize(10); paint.setColor(Color.BLUE);

canvas.drawText(transformation.describe(), 5, 100, paint);

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.transformed);

LinearLayout v1 = (LinearLayout) findViewById(R.id.v_left);0 v1.addView(new TransfomedViewWidget( this, new Transformation^ {

@Override public String describe() { return "identity"; } @Override public void transform(Canvas canvas) { } } ));

v1.addView(new TransfomedViewWidget( this, new Transformation^ {

@Override public String describe() { return "rotate(-30)"; } @Override public void transform(Canvas canvas) {

v1.addView(new TransfomedViewWidget( this, new Transformation^ {

@Override public String describe() { return "scale(.5,.8)"; } @Override public void transform(Canvas canvas) {

v1.addView(new TransfomedViewWidget( this, new Transformation() {

^Override public String describe() { return "skew(.1,.3)"; } ^Override public void transform(Canvas canvas) {

LinearLayout v2 = (LinearLayout) findViewById(R.id.v_right);©

v2.addView(new TransfomedViewWidget( this, new Transformation() {

^Override public String describe() { return "translate(30,10)"; } ^Override public void transform(Canvas canvas) {

v2.addView(new TransfomedViewWidget( this, new Transformation() {

^Override public String describe()

{ return "translate(110,-20),rotate(85)"; } ^Override public void transform(Canvas canvas) { canvas.translate(110.0F, -20.0F); canvas.rotate(85.0F); } }));

v2.addView(new TransfomedViewWidget( this, new Transformation() {

^Override public String describe()

{ return "translate(-50,-20),scale(2,1.2)"; } ^Override public void transform(Canvas canvas) { canvas.translate(-50.0F, -20.0F); canvas.scale(2F, 1.2F); } }));

v2.addView(new TransfomedViewWidget( this, new Transformation() {

^Override public String describe() { return "complex"; } ^Override public void transform(Canvas canvas) { canvas.translate(-100.0F, -100.0F); canvas.scale(2.5F, 2F); canvas.skew(0.1F, 0.3F); } }));

The results of this protracted exercise are shown in Figure 12-3.

Here are some of the highlights of the code:

O Definition of the new widget, TransfomedViewWidget.

© Gets the actual transformation to perform from the second argument of the constructor.

© onDraw method of TransfomedViewWidget.

O Pushes the state on the stack through save before performing any transformation.

Figure 12-3. Transformed views © Performs the transformation passed in item 2.

© Restores the old state saved in item 4, having finished the transformation. O The Activity's onCreate method. © Creates the first layout view.

© Instantiations of TransfomedViewWidget, added to layout view v1.

© Creates a transformation as part of the parameter list to the constructor of TransfomedViewWidget.

© Creates the second layout view.

© Instantiations of TransfomedViewWidget, added to layout view v2.

This small application introduces several new ideas and demonstrates the power of Android graphics for maintaining state and nesting changes.

The application defines a single widget, TransformedViewWidget, of which it creates eight instances. For layout, the application creates two views named v1 and v2, retrieving their parameters from resources. It then adds four instances of TransformedView Widget to each LinearLayout view. This is an example of how applications combine resource-based and dynamic views. Note that the creation both of the layout views and the new widgets take place within the Activity's onCreate method.

This application also makes the new widget flexible through a sophisticated division of labor between the widget and its Transformation. Several simple objects are drawn directly within the definition of TransformedViewWidget, in its onDraw method:

• A white background

• The word "Hello" in 12-point green type

• The word "Android" in 16-point red type

In the middle of this, the onDraw method performs a transformation specified at its creation. The application defines its own interface, called Transformation, and the constructor for TransformedViewWidget accepts a Transformation as a parameter. We'll see in a moment how the caller actually codes a transformation.

It's important to see first how the widget onDraw preserves its own text from being affected by the Transformation. In this example, we want to make sure that the frame and label are drawn last, so that they are drawn over anything else drawn by the widget, even if they might overlap. On the other hand, we do not want the transformation applied earlier to affect them.

Fortunately, the Canvas maintains an internal stack onto which we can record and recover the translation matrix, clip rectangle, and many other elements of mutable state in the Canvas. Taking advantage of this stack, onDraw calls save to preserve its state before the transformation, and restore afterward to recover the saved state.

The rest of the application controls the transformation used in each of the eight instances of TransformedViewWidget. Each new instance of the widget is created with its own anonymous instance of Tranformation. The image in the area labeled "identity" has no translation applied. The other seven areas are labeled with the transformations they demonstrate.

The base methods for Canvas translation are setMatrix and concatMatrix. These two methods allow you to build any possible transformation. The getMatrix method allows you to recover a dynamically constructed matrix for later use. The methods introduced in the example—translate, rotate, scale, and skew—are convenience methods that compose specific, constrained matrixes into the current Canvas state.

Although it may not be obvious at first, these transformation functions can be tremendously useful. They allow your application to appear to change its point of view with respect to a 3D object. It doesn't take too much imagination, for instance, to see the scene in the square labeled "scale(.5,.8)" as the same as that seen in the square labeled "identity", but viewed from farther away. With a bit more imagination, the image in the box labeled "skew(.1,.3)" again could be the untransformed image, but this time viewed from above and slightly to the side. Scaling or translating an object can make it appear to a user as if the object has moved. Skewing and rotating can make it appear that the object has turned. We will make good use of this technique in animation.

When you consider that these transformation functions apply to everything drawn on a canvas—lines, text, and even images—their importance in applications becomes even more apparent. A view that displays thumbnails of photos could be implemented trivially, though perhaps not optimally, as a view that scales everything it displays to 10% of its actual size. An application that simulates what you see as you look to your left while driving down the street might be implemented in part by scaling and skewing a small number of images.

Was this article helpful?

+1 0

Responses

  • zula
    How to draw a wire rectangle on canvas in android?
    8 years ago
  • osman
    How to use canvas in android implementing in widgets?
    8 years ago

Post a comment