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 1 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.
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.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.
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 3 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.
Example 3. 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 3 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 4 shows the rest of the
implementation of the animation.
Example 4. 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 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.