Previous section   Next section

Mobile Phone Handheld Hardware Hardware Rick Rogers John Lombardo O'Reilly Media, Inc. O'Reilly Media Android Application Development, 1st Edition

12.2. Bling

The 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 widget

public 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 application

private 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.

Figure 12-5. Graphics effects


12.2.1. Shadows, Gradients, and Filters

PathEffect, 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. Animation

The 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.AnimationDraw⁠able, 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 animation

A 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 composition

public 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: Accel⁠era⁠teInter⁠polator for the first half of the animation and DecelerateInterpola⁠tor 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. Decelera⁠teInterpolator has exactly the opposite effect. Android also provides a CycleInterpo⁠lator 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 TRansla⁠teA⁠nima⁠tion. 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 animation

Frame-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 AnimationDraw⁠able. Unfortunately, it will supply animation services only to the one Drawable object that is installed as its background with one of the two methods setBackgroundDrawa⁠ble 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 animation

Full-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 Graphics

The 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.

          
      Previous section   Next section