Mobile Phone Handheld Hardware Hardware Rick Rogers John Lombardo O'Reilly Media, Inc. O'Reilly Media Android Application Development, 1st Edition12.2. BlingThe Android UI framework is a lot more
than a just an intelligent, well-put-together GUI toolkit. When it takes
off its glasses and shakes out its hair, it can be downright sexy! The
tools mentioned here certainly do not make an exhaustive catalog. They
might get you started, though, on the path to making your application
Filthy Rich.  | Several of the techniques discussed in this section are close to
the edges of the Android landscape. As such, they are less well
established: the documentation is not as thorough, some of the features
are clearly in transition, and you may even find bugs. If you run into
problems, the Google Group "Android Developers" is an invaluable
resource. Questions about a particular aspect of the toolkit have
sometimes been answered by the very person responsible for implementing
that aspect. Be careful about checking the dates on solutions you find by
searching the Web. Some of these features are changing rapidly, and code
that worked as recently as six months ago may not work now. A corollary,
of course, is that any application that gets wide distribution is likely
to be run on platforms that have differing implementations of the
features discussed here. By using these techniques, you may limit the
lifetime of your application and the number of devices that it will
support. |
|
The rest of this section considers a single application, much like
the one used in Example 12-6: a
couple of LinearLayouts that contain multiple instances
of a single widget, each demonstrating a different graphics effect. Example 12-10 contains the key parts of the
widget, with code discussed previously elided for brevity. The widget
simply draws a few graphical objects and defines an interface through
which various graphics effects can be applied to the rendering. Example 12-10. Effects widgetpublic class EffectsWidget extends View {
/** The effect to apply to the drawing */
public interface PaintEffect { void setEffect(Paint paint); }
// ...
// PaintWidget's widget rendering method
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
effect.setEffect(paint);
paint.setColor(Color.DKGRAY);
paint.setStrokeWidth(5);
canvas.drawLine(10, 10, 140, 20, paint);
paint.setTextSize(26);
canvas.drawText("Android", 40, 50, paint);
paint = new Paint();
paint.setColor(Color.BLACK);
canvas.drawText(String.valueOf(id), 2.0F, 12.0F, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
canvas.drawRect(canvas.getClipBounds(), paint);
}
} |
The application that uses this widget, shown in Example 12-11, should also feel familiar. It
creates several copies of the EffectsWidget, each with its own effect. There
are two special widgets: the bottom widget in the right column is
animated, and the bottom widget in the left column uses OpenGL
animation. Example 12-11. Effects applicationprivate void buildView() {
setContentView(R.layout.main);
LinearLayout view = (LinearLayout) findViewById(R.id.v_left);
view.addView(new EffectsWidget(
this,
1,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) {
paint.setShadowLayer(1, 3, 4, Color.BLUE);
} }));
view.addView(new EffectsWidget(
this,
3,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) {
paint.setShader(
new LinearGradient(
0.0F,
0.0F,
160.0F,
80.0F,
new int[] { Color.BLACK, Color.RED, Color.YELLOW },
new float[] { 0.2F, 0.3F, 0.2F },
Shader.TileMode.REPEAT));
} }));
view.addView(new EffectsWidget(
this,
5,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) {
paint.setMaskFilter(
new BlurMaskFilter(2, BlurMaskFilter.Blur.NORMAL));
} }));
// Not and EffectsWidget: this is the OpenGL Anamation widget.
glWidget = new GLDemoWidget(this);
view.addView(glWidget);
view = (LinearLayout) findViewById(R.id.v_right);
view.addView(new EffectsWidget(
this,
2,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) {
paint.setShadowLayer(3, -8, 7, Color.GREEN);
} }));
view.addView(new EffectsWidget(
this,
4,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) {
paint.setShader(
new LinearGradient(
0.0F,
40.0F,
15.0F,
40.0F,
Color.BLUE,
Color.GREEN,
Shader.TileMode.MIRROR));
} }));
// A widget with an animated background
View w = new EffectsWidget(
this,
6,
new EffectsWidget.PaintEffect() {
@Override public void setEffect(Paint paint) { }
});
view.addView(w);
w.setBackgroundResource(R.drawable.throbber);
// This is, alas, necessary until Cupcake.
w.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
((AnimationDrawable) v.getBackground()).start();
} });
} |
Figure 12-5 shows what the
code looks like when run. The bottom two widgets are animated: the green
checkerboard moves from left to right across the widget, and the
bottom-right widget has a throbbing red background. 
12.2.1. Shadows, Gradients, and FiltersPathEffect, MaskFilter, ColorFilter, Shader, and ShadowLayer are all attributes of Paint. Anything drawn with Paint can be
drawn under the influence of one or more of these transformations. The
top several widgets in Figure 12-5 give examples of some of
these effects. Widgets 1 and 2 demonstrate shadows. Shadows are currently
controlled by the setShadowLayer method. The arguments,
a blur radius and X and Y displacements, control the apparent distance
and position of the light source that creates the shadow, with respect
to the shadowed object. Although this is a very neat feature, the
documentation explicitly warns that it is a temporary API. However, it
seems unlikely that the setShadowLayer method will completely
disappear or even that future implementations will be
backward-incompatible. The Android toolkit contains several prebuilt shaders. Widgets 3
and 4 demonstrate one of them, the LinearGradient shader. A gradient is a regular
transition between colors that might be used, for example, to give a
page background a bit more life, without resorting to expensive bitmap
resources. A LinearGradient is specified
with a vector that determines the direction and rate of the color
transition, an array of colors through which to transition, and a mode.
The final argument, the mode, determines what happens when a single
complete transition through the gradient is insufficient to cover the
entire painted object. For instance, in widget 4, the transition is only
15 pixels long, whereas the drawing is more than 100 pixels wide. Using
the mode Shader.TileMode.Mirror causes the transition
to repeat, alternating direction across the drawing. In the example, the
gradient transitions from blue to green in 15 pixels, then from green to
blue in the next 15, and so on across the canvas. 12.2.2. AnimationThe Android UI toolkit offers several different animation
tools. Transition animations—which
the Google documentation calls tweened
animations—are subclasses of android.view.animation.Animation: RotateAnimation, translateAnimation, ScaleAnimation, etc. These animations
are used as transitions between pairs of views. A second type of
animation, subclasses of android.graphics.drawable.AnimationDrawable,
can be put into the background of any widget to provide a wide variety
of effects. Finally, there is full-on animation, on top of a SurfaceView that gives you full control to do
your own seat-of-the-pants animation. Because both of the first two types of animation, transition and
background, are supported by View—the
base class for all widgets—every widget, toolkit, and custom will
potentially support them. 12.2.2.1. Transition animationA transition animation is started by calling the View method startAnimation with an instance of Animation (or, of course, your own
subclass). Once installed, the animation runs to completion:
transition animations have no pause state. The heart of the animation is its applyTransformation method. This method is
called to produce successive frames of the animation. Example 12-12 shows the
implementation of one transformation. As you can see, it does not
actually generate entire graphical frames for the animation. Instead,
it generates successive transformations to be applied to a single
image being animated. You will recall, from the section Section 12.1.2.2, that matrix
transformations can be used to make an object appear to move.
Transition animations depend on exactly this trick. Example 12-12. Transition animation@Override
protected void applyTransformation(float t, Transformation xf) {
Matrix xform = xf.getMatrix();
float z = ((dir > 0) ? 0.0f : -Z_MAX) - (dir * t * Z_MAX);
camera.save();
camera.rotateZ(t * 360);
camera.translate(0.0F, 0.0F, z);
camera.getMatrix(xform);
camera.restore();
xform.preTranslate(-xCenter, -yCenter);
xform.postTranslate(xCenter, yCenter);
} |
This particular implementation makes its target appear to spin
in the screen plane (the rotate
method call), and at the same time, to shrink into the
distance (the translate method
call). The matrix that will be applied to the target image is
obtained from the TRansformation
object passed in that call. This implementation uses camera, an instance of the utility class
Camera. The Camera class—not to be confused with the
camera in the phone—is a utility that makes it possible to record
rendering state. It is used here to compose the rotation and
translations transformations into a single matrix, which is then
stored as the animation transformation. The first parameter to applyTransformation, named t, is effectively the frame number. It is
passed as a floating-point number between 0.0 and 1.0, and might also
be understood as the percent of the animation that is complete. This
example uses t to increase the apparent distance
along the Z-axis (a line perpendicular to the plane of the screen) of
the image being animated, and to set the proportion of one complete
rotation through which the image has passed. As t increases, the animated image appears to
rotate further and further counter-clockwise and to move farther and
farther away, along the Z-axis,
into the distance. The preTranslate and postTranslate operations are necessary in
order to translate the image around its center. By default, matrix
operations transform their target around the origin. If we did not
perform these bracketing translations, the target image would appear
to rotate around its upper-left corner. preTranslate effectively moves the origin to
the center of the animation target for the translation, and postTranslate causes the default to be
restored after the translation. If you consider what a transition animation must do, you'll
realize that it is likely to compose two animations: the previous
screen must be animated out and the next one animated in. Example 12-12 supports this using
the remaining, unexplained variable dir. Its value is either 1 or –1, and it
controls whether the animated image seems to shrink into the distance
or grow into the foreground. We need only find a way to compose a
shrink and a grow animation. This is done using the familiar Listener pattern. The Animation class defines a listener named
Animation.AnimationListener. Any instance of
Animation that has a nonnull
listener calls that listener once when it starts, once when it stops,
and once for each iteration in between. Creating a listener that
notices when the shrinking animation completes and spawns a new
growing animation will create exactly the effect we desire. Example 12-13 shows the rest of the
implementation of the animation. Example 12-13. Transition animation compositionpublic void runAnimation() {
animateOnce(new AccelerateInterpolator(), this);
}
@Override
public void onAnimationEnd(Animation animation) {
root.post(new Runnable() {
public void run() {
curView.setVisibility(View.GONE);
nextView.setVisibility(View.VISIBLE);
nextView.requestFocus();
new RotationTransitionAnimation(-1, root, nextView, null)
.animateOnce(new DecelerateInterpolator(), null);
} });
}
void animateOnce(
Interpolator interpolator,
Animation.AnimationListener listener)
{
setDuration(700);
setInterpolator(interpolator);
setAnimationListener(listener);
root.startAnimation(this);
} |
The runAnimation method
starts the transition. The overridden AnimationListener method, onAnimationEnd, spawns the second half.
Called when the target image appears to be far in the distance, it
hides the image being animated out (the curView) and replaces it with the newly
visible image, nextView. It then
creates a new animation that, running in reverse, spins and grows the
new image into the foreground. The Interpolater class
represents a nifty attention to detail. The values for t, passed to applyTransformation, need not be linearly
distributed over time. In this implementation the animation appears to
speed up as it recedes, and then to slow again as the new image
advances. This is accomplished by using the two interpolators:
AccelerateInterpolator for the
first half of the animation and DecelerateInterpolator for the second.
Without the interpolator, the difference between successive values of
t, passed to applyTransformation, would be constant. This
would make the animation appear to have a constant speed. The AccelerateInterpolator converts those
equally spaced values of t into values that are
close together at the beginning of the animation and much further
apart toward the end. This makes the animation appear to speed up.
DecelerateInterpolator has exactly
the opposite effect. Android also provides a CycleInterpolator and LinearInterpolator, for use as
appropriate. Animation composition is actually built into the toolkit, using
the (perhaps confusingly named) AnimationSet class. This class provides a
convenient way to specify a list of animations to be played, in order
(fortunately not a Set: it is
ordered and may refer to a given
animation more than once). In addition, the toolkit provides several
standard transitions: AlphaAnimation, RotateAnimation, ScaleAnimation, and TRanslateAnimation. Certainly, there is
no need for these transitional animations to be symmetric, as they are
in the previous implementation. A new image might alpha fade in as the
old one shrinks into a corner or slide up from the bottom as the old
one fades out. The possibilities are endless. 12.2.2.2. Background animationFrame-by-frame animation, as it is called in the Google documentation, is
completely straightforward: a set of frames, played in order at
regular intervals. This kind of animation is implemented by subclasses
of AnimationDrawable. As subclasses of Drawable,
AnimationDrawable objects can be
used in any context that any other Drawable is used. The mechanism that
animates them, however, is not a part of the Drawable itself. In order to animate, an
AnimationDrawable relies on an
external service provider—an implementation of the Drawable.Callback interface—to animate
it. The View class implements
this interface and can be used to animate an AnimationDrawable. Unfortunately, it will
supply animation services only to the one
Drawable object that is installed
as its background with one of the two methods setBackgroundDrawable or setBackgroundResource. The good news, however, is that this is probably sufficient. A
background animation has access to the entire widget canvas.
Everything it draws will appear to be behind anything drawn by the
View.onDraw method, so it would be
hard to use the background to implement full-fledged sprites
(animation integrated into a static scene). Still, with clever use of
the DrawableContainer class (which
allows you to animate several different animations simultaneously) and
because the background can be changed at any time, it is possible to
accomplish quite a bit without resorting to implementing your own
animation framework. An AnimationDrawable in a
view background is entirely sufficient to do anything from, say,
indicating that some long-running activity is taking place—maybe
winged packets flying across the screen from a phone to a tower—to
simply making a button's background pulse. The pulsing button example is illustrative and surprisingly easy
to implement. Examples Example 12-14 and Example 12-15 show all you need. The animation is
defined as a resource, and code applies it to the button. Example 12-14. Frame-by-frame animation (resource)<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/throbber_f0" android:duration="70" />
<item android:drawable="@drawable/throbber_f1" android:duration="70" />
<item android:drawable="@drawable/throbber_f2" android:duration="70" />
<item android:drawable="@drawable/throbber_f3" android:duration="70" />
<item android:drawable="@drawable/throbber_f4" android:duration="70" />
<item android:drawable="@drawable/throbber_f5" android:duration="70" />
<item android:drawable="@drawable/throbber_f6" android:duration="70" />
</animation-list>
|
Example 12-15. Frame-by-frame animation (code)// w is a button that will "throb"
button.setBackgroundResource(R.drawable.throbber);
//!!! This is necessary, but should not be so in Cupcake
button.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
AnimationDrawable animation
= (AnimationDrawable) v.getBackground();
if (animation.isRunning()) { animation.stop(); }
else { animation.start(); }
// button action.
} }); |
There are several gotchas here, though. First of all, as of this
writing, the animation-list example
in the Google documentation does not quite work. There is a problem
with the way it identifies the animation-list resource. To make it work,
don't define an android:id in that
resource. Instead, simply refer to the object by its filename
(R.drawable.throbber), as Example 12-15
demonstrates. The second issue is that a bug in the V1_r2 release of the
toolkit prevents a background animation from being started in the
Activity.onCreate method. If your
application's background should be animated whenever it is visible,
you'll have to use trickery to start it. The example implementation
uses an onClick handler. There are
suggestions on the Web that the animation can also be started
successfully from a thread that pauses briefly before calling AnimationDrawable.start. The Android
development team has a fix for this problem, so the constraint should
be relaxed with the release of Cupcake. Finally, if you have worked with other UI frameworks, especially
Mobile UI frameworks, you may be accustomed to painting the view
background in the first couple of lines of the onDraw method (or equivalent). If you do
that in Android, however, you will paint over your animation. It is,
in general, a good idea to get into the habit of using setBackground to control the View background, whether it is a solid
color, a gradient, an image, or an animation. Specifying an AnimationDrawable by resource is very
flexible. You can specify a list of drawable resources—any images you
like—that comprise the animation. If your animation needs to be
dynamic, AnimationDrawable is a straightforward
recipe for creating a dynamic drawable that can be animated in the
background of a View. 12.2.2.3. Surface view animationFull-on animation requires a SurfaceView. The SurfaceView provides a node in the view tree
(and, therefore, space on the display) on which any process at all can
draw. The SurfaceView node is laid
out, sized, and receives clicks and updates, just like any other
widget. Instead of drawing, however, it simply reserves space on the
screen, preventing other widgets from affecting any of the pixels
within its frame. Drawing on a SurfaceView
requires implementing the SurfaceHolder.Callback interface. The two methods surfaceCreated and surfaceDestroyed inform the implementor that
the drawing surface is available for drawing and that it has become
unavailable, respectively. The argument to both of the calls is an
instance of yet a third class, SurfaceHolder. In the interval
between these two calls, a drawing routine can call the
SurfaceView methods lockCanvas and unlockCanvasAndPost to edit the pixels
there. If this seems complex, even alongside some of the elaborate
animation discussed previously…well, it is. As usual, concurrency
increases the likelihood of nasty, hard-to-find bugs. The client of a
SurfaceView must be sure that access to any state
shared across threads is properly synchronized, and also that it never
touches the SurfaceView, Surface,
or Canvas except in the
interval between the calls to surfaceCreated and surfaceDestroyed. The toolkit could
clearly benefit from a more complete framework support for
SurfaceView animation. If you are considering SurfaceView animation, you are probably also
considering OpenGL graphics. As we'll see, there is an extension
available for OpenGL animation on a SurfaceView. It will turn up in a somewhat
out-of-the-way place, though. 12.2.3. OpenGL GraphicsThe Android platform supports OpenGL graphics in roughly the
same way that a silk hat supports rabbits. Although this is certainly
among the most exciting technologies in Android, it is definitely at the
edge of the map. It also appears that just before the final beta
release, the interface underwent major changes. Much of the code and
many of the suggestions found on the Web are obsolete and no longer
work. The API V1_r2 release is an implementation of OpenGL ES 1.0 and
much of ES 1.1. It is, essentially, a domain-specific language embedded
in Java. Someone who has been doing gaming UIs for a while is likely to
be much more comfortable developing Android OpenGL programs than a Java
programmer, even a programmer who is a Java UI expert. Before discussing the OpenGL graphics library itself, we should
take a minute to consider exactly how pixels drawn with OpenGL appear on
the display. The rest of this chapter has discussed the intricate
View framework that Android uses to
organize and represent objects on the screen. OpenGL is a language in
which an application describes an entire scene that will be rendered by
an engine that is not only outside the JVM, but possibly running on
another processor altogether (the Graphics Processing Unit, or GPU).
Coordinating the two processors' views of the screen is tricky. The SurfaceView, discussed
earlier, is nearly the right thing. Its purpose is to create a surface
on which a thread other than the UI graphics thread can draw. The tool
we'd like is an extension of SurfaceView that has a bit more support for
concurrency, combined with support for OpenGL. It turns out that there is exactly such a tool. All of the demo
applications in the Android SDK distribution that do OpenGL animation
depend on the utility class GLSurfaceView. Since the demo applications
written by the creators of Android use this class, considering it for
other applications seems advisable. GLSurfaceView defines an
interface, GLSurfaceView.Renderer,
which dramatically simplifies the otherwise overwhelming complexity of
using OpenGL and GLSurfaceView.
GLSurfaceView calls the renderer
method getConfigSpec to get its
OpenGL configuration information. Two other methods, sizeChanged and surfaceCreated, are called by the GLSurfaceView to inform the renderer that its
size has changed or that it should prepare to draw, respectively.
Finally, drawFrame, the heart of the
interface, is called to render a new OpenGL frame. Example 12-16 shows the
important methods from the implementation of an OpenGL renderer. Example 12-16. Frame-by-frame animation with OpenGL// ... some state set up in the constructor
@Override
public void surfaceCreated(GL10 gl) {
// set up the surface
gl.glDisable(GL10.GL_DITHER);
gl.glHint(
GL10.GL_PERSPECTIVE_CORRECTION_HINT,
GL10.GL_FASTEST);
gl.glClearColor(0.4f, 0.2f, 0.2f, 0.5f);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glEnable(GL10.GL_DEPTH_TEST);
// fetch the checker-board
initImage(gl);
}
@Override
public void drawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// apply the checker-board to the shape
gl.glActiveTexture(GL10.GL_TEXTURE0);
gl.glTexEnvx(
GL10.GL_TEXTURE_ENV,
GL10.GL_TEXTURE_ENV_MODE,
GL10.GL_MODULATE);
gl.glTexParameterx(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_S,
GL10.GL_REPEAT);
gl.glTexParameterx(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_T,
GL10.GL_REPEAT);
// animation
int t = (int) (SystemClock.uptimeMillis() % (10 * 1000L));
gl.glTranslatef(6.0f - (0.0013f * t), 0, 0);
// draw
gl.glFrontFace(GL10.GL_CCW);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuf);
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuf);
gl.glDrawElements(
GL10.GL_TRIANGLE_STRIP,
5,
GL10.GL_UNSIGNED_SHORT, indexBuf);
}
private void initImage(GL10 gl) {
int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
gl.glTexParameterf(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER,
GL10.GL_NEAREST);
gl.glTexParameterf(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterf(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(
GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexEnvf(
GL10.GL_TEXTURE_ENV,
GL10.GL_TEXTURE_ENV_MODE,
GL10.GL_REPLACE);
InputStream in
= context.getResources().openRawResource(R.drawable.cb);
Bitmap image;
try { image = BitmapFactory.decodeStream(in); }
finally {
try { in.close(); } catch(IOException e) { }
}
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, image, 0);
image.recycle();
} |
The surfaceCreated method
prepares the scene. It sets several OpenGL attributes that need to be
initialized only when the widget gets a new drawing surface. In
addition, it calls initImage, which
reads in a bitmap resource and stores it as a 2D texture. Finally, when
drawFrame is called, everything is
ready for drawing. The texture is applied to a plane whose vertices were
set up in vertexBuf by the
constructor, the animation phase is chosen, and the scene is
redrawn.
|