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

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 "Matrix transformations" on page 231, 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: AccelerateInterpolator for the first half of the animation and DecelerateInter polator 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.

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 Animation Drawable. Unfortunately, it will supply animation services only to the one Drawable object that is installed as its background with one of the two methods setBackground Drawable 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 12-14 and 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">

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

</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.isRunningQ) { animation.stop(); } else { animation.start(); } // button action. } });

There are several gotchas here, though. First of all, as of this writing, the animationlist 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.

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 pre-viously...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.

+1 0

Post a comment