2. Canvas Drawing
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.
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 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 Section 2. Example 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 1.
Example 2. Using Paint
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE);
Paint paint = new Paint();
canvas.drawLine(33, 0, 33, 100, paint);
paint.setColor(Color.RED); paint.setStrokeWidth(10); 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); } } |
With the addition of Paint, we
are prepared to understand most of the other tools necessary to create a
useful widget. Example 3. 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 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 * @param dots the dots we draw */ public DotView(Context context, Dots dots) { super(context); this.dots = dots; setMinimumWidth(180); setMinimumHeight(200); setFocusable(true); }
/** @see android.view.View#onMeasure(int, int) */ @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.