3. Drawables
A Drawable is an object that knows how to render itself on a Canvas. Because a Drawable
has complete control during rendering, even a very complex
rendering process can be encapsulated in a way that makes it fairly easy
to use.
Examples Example 7 and Example 8 show the changes necessary to
implement the previous example, Figure 3, using a Drawable. The code that draws the red and
green text has been refactored into a HelloAndroidTextDrawable class, used in
rendering by the widget’s onDraw
method.
Example 7. Using a TextDrawable
private static class HelloAndroidTextDrawable extends Drawable { private ColorFilter filter; private int opacity;
public HelloAndroidTextDrawable() {}
@Override public void draw(Canvas canvas) { Paint paint = new Paint();
paint.setColorFilter(filter); paint.setAlpha(opacity);
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); }
@Override public int getOpacity() { return PixelFormat.TRANSLUCENT; }
@Override public void setAlpha(int alpha) { }
@Override public void setColorFilter(ColorFilter cf) { } }
|
Using the new Drawable
implementation requires only a few small changes to the onDraw method.
Example 8. Using a Drawable widget
package com.oreilly.android.intro.widget;
import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View;
/**A widget that renders a drawable with a transformation */ public class TransformedViewWidget extends View {
/** A transformation */ public interface Transformation { /** @param canvas */ void transform(Canvas canvas); /** @return text descriptiont of the transform. */ String describe(); }
private final Transformation transformation; private final Drawable drawable;
/** * Render the passed drawable, transformed. * * @param context app context * @param draw the object to be drawn, in transform * @param xform the transformation */ public TransformedViewWidget( Context context, Drawable draw, Transformation xform) { super(context);
drawable = draw; transformation = xform;
setMinimumWidth(160); setMinimumHeight(135); }
/** @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);
canvas.save(); transformation.transform(canvas); drawable.draw(canvas); canvas.restore();
Paint paint = new 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, getMeasuredHeight() - 5, paint); } }
|
This code begins to demonstrate the power of using a Drawable. This implementation of TransformedViewWidget will transform any
Drawable, no matter what it happens
to draw. It is no longer tied to rotating and scaling our original,
hardcoded text. It can be reused to transform both the text from the
previous example and a photo captured from the camera, as Figure 4 demonstrates. It could
even be used to transform a Drawable
animation.
The ability to encapsulate complex drawing tasks in a single
object with a straightforward API is a valuable—and even necessary—tool
in the Android toolkit. Drawables
make complex graphical techniques such as nine-patches and animation
tractable. In addition, since they wrap the rendering process
completely, Drawables can be nested
to decompose complex rendering into small reusable pieces.
Consider for a moment how we might extend the previous example to
make each of the six images fade to white over a period of a minute.
Certainly, we could just change the code in Example 8 to do the fade. A
different—and very appealing—implementation involves writing one new
Drawable.
This new Drawable, FaderDrawable, will take, in its constructor,
a reference to its target, the Drawable that it will fade to white. In
addition, it must have some notion of time, probably an integer—let’s
call it t—that is incremented by a
timer. Whenever the
draw method of
FaderDrawable is called, it first
calls the draw method of its target.
Next, it paints over exactly the same area with the color white, using
the value of t to determine the
transparency (alpha value) of the paint (as demonstrated in Example 2). As time passes, t gets larger, the white gets more and more
opaque, and the target Drawable fades
to white.
This hypothetical FaderDrawable
demonstrates some of the important features of Drawables. First, note that FaderDrawable is nicely reusable: it will fade
just about any Drawable. Also note that, since
FaderDrawable extends Drawable, we can use it anywhere that we would
have used its target, the Drawable
that it fades to white. Any code that uses a Drawable in its rendering process can use a
FaderDrawable without change.
Of course, a FaderDrawable
could itself be wrapped. In fact, it seems possible to achieve very
complex effects, simply by building a chain of Drawable wrappers. The Android toolkit
provides Drawable wrappers that
support this strategy, including ClipDrawable,
RotateDrawable, and ScaleDrawable.
At this point you may be mentally redesigning your entire UI in
terms of Drawables. Although a
powerful tool, they are not a panacea. There are several issues to keep
in mind when considering the use of Drawables.
You may well have noticed that they share a lot of the
functionality of the View class:
location, dimensions, visibility, etc. It’s not always easy to decide
when a View should draw directly on
the Canvas, when it should delegate
to a subview, and when it should delegate to one or more Drawable objects. There is even a DrawableContainer class that allows grouping several child
Drawables within a parent. It is possible to build
trees of Drawables that parallel the trees of
Views we’ve been using so far. In dealing with the
Android framework, you just have to accept that sometimes there is more
than one way to scale a cat.
One difference between the two choices is that
Drawables do not implement the View measure/layout protocol, which allows a
container view to negotiate the layout of its components in response to
changing view size. When a renderable object needs to add, remove, or
lay out internal components, it’s a pretty good indication that it
should be a full-fledged View instead
of a Drawable.
A second issue to consider is that Drawables completely wrap the drawing process
because they are not drawn like String or Rect objects. There are, for instance, no
Canvas
methods that will render a Drawable at specific coordinates. You may find
yourself deliberating over whether, in order to render a certain image
twice, a View
onDraw method should use two different,
immutable Drawables or a single
Drawable twice, resetting its
coordinates.
Perhaps most important, though, is a more generic problem. The
idea of a chain of Drawables works
because the Drawable interface
contains no information about the internal implementation of the
Drawable. When your code is passed a
Drawable, there is no way for it to
know whether it is something that will render a simple image or a
complex chain of effects that rotates, flashes, and bounces. Clearly
this can be a big advantage. But it can also be a problem.
Quite a bit of the drawing process is stateful. You set up
Paint and then draw with it. You set
up Canvas clip regions and
transformations and then draw through them. When cooperating in a chain,
if Drawables change state, they must be very careful
that those changes never collide. The problem is that, when constructing
a Drawable chain, the possibility of
collision cannot be explicit in the object’s type by definition (they
are all just Drawables). A seemingly small change
might have an effect that is not desirable and is difficult to
debug.
To illustrate the problem, consider two Drawable wrapper classes, one that is meant to
shrink its contents and another that is meant to rotate them by 90
degrees. If either is implemented by setting the transformation matrix
to a specific value (instead of composing its transformation with any
that already exist), composing the two Drawables may
not have the desired effect. Worse, it might work perfectly if A wraps
B, but not if B wraps A! Careful documentation of how a Drawable is implemented is essential.
4. Bitmaps
The Bitmap is the last member of the four essentials for drawing:
something to draw (a String, Rect, etc.), Paint with which to draw, a Canvas on which to draw, and the Bitmap to hold the bits. Most of the time, you
don’t have to deal directly with a Bitmap, because the Canvas provided as an argument to the onDraw method already has one behind it.
However, there are circumstances under which you may want to use a
Bitmap
directly.
A common use for a Bitmap is as
a way to cache a drawing that is time-consuming to draw but unlikely to
change frequently. Consider, for example, a drawing program that allows
the user to draw in multiple layers. The layers act as transparent
overlays on a base image, and the user turns them off and on at will. It
might be very expensive to actually draw each individual layer every
time onDraw gets called. Instead, it
might be faster to render the entire drawing with all visible layers
once, and only update it when the user changes which are visible.
The implementation of such an application might look something
like Example 9.
Example 9. Bitmap caching
private class CachingWidget extends View { private Bitmap cache;
public CachingWidget(Context context) { super(context); setMinimumWidth(200); setMinimumHeight(200); }
public void invalidateCache() { cache = null; invalidate(); }
@Override protected void onDraw(Canvas canvas) { if (null == cache) { cache = Bitmap.createBitmap( getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
drawCachedBitmap(new Canvas(cache)); }
canvas.drawBitmap(cache, 0, 0, new Paint()); }
// ... definition of drawCachedBitmap }
|
This widget normally just copies the cached Bitmap, cache, to the Canvas passed to onDraw. If the cache is marked stale (by
calling invalidateCache), only then
will drawCachedBitmap be called to
actually render the widget.
The most common way to encounter a Bitmap is as the programmatic representation
of a graphics resource. Resources.getDrawable returns a BitmapDrawable when the resource is an
image.
Combining these two ideas—caching an image and wrapping it in a
Drawable—opens yet another interesting window. It
means that anything that can be drawn can also be postprocessed.