In the Xaminar about Android animations that we published earlier this year, we discussed how recent versions of Google’s mobile platform introduce lots of powerful features for animating user interfaces. In that talk, I mentioned the property animation API, which lets you animate any object property—including non-graphical properties.
The corresponding class to that API is ValueAnimator
—and more precisely, in our case, its subclass ObjectAnimator
. Both let you construct animations over a range of normal scalar types like float or int for any property. Their true power, however, lies in the fact that they can animate properties of any object type with the right configuration (more on that in the next section).
In addition, an often-overlooked fact is that animation timing behavior is not set in stone. Indeed, each of the animation APIs on Android can optionally take an ITimeInterpolator
which defines the “time shape” of the animations. For instance, the following are the respective time shapes of two of the default interpolators used in the framework, LinearInterpolator
and AccelerateDecelerateInterpolator
:
Creating your own time interpolator is also very easy. You simply have to inplement the ITimeInterpolator
interface which has only one method: GetInterpolation(float)
. The argument passed to that method is the current step of the animation, which is a value between 0 (the start of your animation) and 1.0 (its end). You are generally supposed to return a value in that range too, but adjacent values are also supported and are being translated to an overshoot or an undershoot effect.
Additionaly, the formula you are using to compute your final value can be as complex as you want as long as it doesn’t impede the rendering loop (remember, for animations to be buttery smooth you want to aim for 60 FPS). For instance, imagining I needed a time interpolator to represent the fall of an object (a typical quadratic equation), I could do it like this:
class QuadraticTimeInterpolator : Java.Lang.Object, ITimeInterpolator { public float GetInterpolation (float input) { return input * input; // or (float)Math.Pow (input, 2) } }
Which corresponds to the following shape:
So why is this useful?
Although you can generally use the default interpolator (AccelerateDecelerateInterpolator
), if your animations are supposed to model a real-life phenomenon (like my example of an object falling) then you should strive to find the equation that faithfully reproduces the physical behavior your users expect.
Practical example
Let’s now see a practical example of the two features I mentioned about property animation: customized time interpolator and animating over non-scalar values. For our experiment we are going to use Google Maps v2 API and try to add a bit of spice to Marker
objects—which, by default, can’t be animated with the existing API.
The idea is to create a dropping effect for the pin:
This effect can be used to inform the user of the result of his location search, for instance. The iOS Map application uses a similar effect for similar reasons. Since the only property available to animate the pin is its latitude/longitude coordinates on the map, we will have to determine a pair of geo-points respectively for the starting and ending positions of a vertical movement.
Since the ending position is assumed to be known (i.e. we have the coordinates where the pin should be), we simply have to compute the initial offset of the pin when dropped. With the Google Maps API, you can do these kinds of display/map conversions using the Projection
class available on your Map
object. For instance, computing the starting geocoordinates for our pin will be done like this:
// finalLatLng is known var proj = mapFragment.Map.Projection; var location = proj.ToScreenLocation (finalLatLng); // We will start 35dp above the final position location.Offset (0, -(35.ToPixels ())); var startLatLng = proj.FromScreenLocation (location);
We now have two geocoordinates between which we want to animate the pin using its SetPosition(LatLng)
method. The problem is that, by default, the animation subsystem has no idea what a LatLng
type is and how to create the intermediate values between our starting and ending points. Fortunately, as I explained in the beginning, we can extend the animation framework to accept any type with a bit of help. This is achieved with a subclass of the ITypeEvaluator
interface.
The implementation of this interface is very similar in spirit to ITimeInterpolator
, with a single method to implement which gives you three parameters: the desired intermediate value (represented by a fraction value between 0.0f and 1.0f), the start object instance, and the end object instance. Your job as an implementer is to provide a linear interpolation based on the fraction’s value between those two object states (the axiom being that fraction = 0.0f ⇔ result = startObject and fraction = 1.0f ⇔ result = endObject).
Here is the straightforward implementation of your evaluator for a LatLng
object:
class LatLngEvaluator : Java.Lang.Object, ITypeEvaluator { public Java.Lang.Object Evaluate (float fraction, Java.Lang.Object startValue, Java.Lang.Object endValue) { var start = (LatLng)startValue; var end = (LatLng)endValue; return new LatLng (start.Latitude + fraction * (end.Latitude - start.Latitude), start.Longitude + fraction * (end.Longitude - start.Longitude)); } }
The last piece is that we want the pin drop to feel as natural as possible. Instinctively, when you think about an object drop you can picture it accelerating until it reaches the ground and then gently bouncing until all its initial inertia is gone. Fortunately, we don’t have to model that behavior ourselves because it’s already available in Android through the well-named BounceInterpolator
class.
You can take a look at the formula implementation if you are interested, but for the sake of the explanation I think the shape is a better illustration of the result:
Putting those two pieces together with our ObjectAnimator
friend, we can now implement the solution very concisely like this:
var opts = new MarkerOptions () { Position = startLatLng, Icon = BitmapDescriptorFactory.DefaultMarker (BitmapDescriptorFactory.HueViolet) }; var marker = mapFragment.Map.AddMarker (opts); var evaluator = new LatLngEvaluator (); ObjectAnimator.OfObject (marker, "position", evaluator, startLatLng, finalLatLng) .SetDuration (1000) .SetInterpolator (new Android.Views.Animations.BounceInterpolator ()) .Start ();