Wednesday, January 4, 2012

Curved Motion in Android

The animation support added in Android 3.0 and enhanced since then is useful, allowing a flexible system of property animation to animate literally anything you want. Like a store going out of business: if it ain't nailed down, it's up for grabs.

But that doesn't mean we're done yet; there are many things that we'd like to do to keep improving the animation system and making Android animations easier to write and more powerful. One of those things is curved motion.

Right now, if you animate something between a start and end point, it will move in a straight line between those values. For example, a translation animation, where the object moves from one (x, y) location to another, will move in a straight line beween those points. You can (and should) use non-linear timing for these animations, and the animation framework uses ease-in/ease-out timing by default. But the actual spacial movement is linear; there is no way for you to specify a curved path for an object to take between the end values of an animation.

Even if you use a multi-point keyframe animation, where you specify several intermediate points for the animation to pass through along the way, you are still going to move linearly between each of those points.

One goal with animations is to make them feel as natural as possible. And just as motion in the real world (you know, the one we have to use as we move between the machines of our lives) is not linear, animations in our GUIs should not be limited to linear. For example, f a view moves from corner of the screen to the opposite corner, wouldn't it be nice to have the option to give it a little curve in and out at both ends, instead of being locked into a straight-line movement?

We'd like to add this kind of capability to the animation system in a future release; it needs to be easy to create such animations, we just have to provide the APIs and functionality to let you do it.

As I was looking into the problem, I created some prototype code using the existing animation APIs and realized that there's nothing stopping you from having this capability already. The flexibility of the Animator APIs allow you to do exactly the kinds of operations you need to get curved motion. You just need a little more code to do it.

I thought it would help to post some of my prototype code to show you how. In particular, I thought this sample was interesting because it shows:
  • How to move things along curves and complex paths
  • How to use the Animator APIs to do more than what the base library supports. In particular, how to use TypeEvaluator to animate custom types.

Some notes and caveats before we begin:
  • This is a prototype only, and does not necessarily represent the way it would appear in any final API. It's just a sample program, and a pretty simple one at that.
  • Simply computing the location based on the fraction time elapsed in a curve interval is probably not the motion you want. It will give the mathematically correct motion along that path, but the time spent traveling along any particular length of that curve is dependent on the structure of the curve. Basically, you'll end up with slower and faster portions of the curve. This problem is admirably described on this blog. A more complete solution flattens the curve and ensures uniform speed. But again, it's just a simple demo, so I'll leave correct path-time-distance navigation as an exercise for the reader (and for the writer, since this would be a part of any future implementation in the SDK).
  • The timing of the animation along a multiple-point path such as the one in the demo app is not as flexible as I'd like it to be. Basically, you end up with something that gives equal time in the overall animation to each individual interval. In addition, any "move" operations in the middle of the path cause the animation to wait at that location for that interval's equal slice of the duration. It should be possible, in a more complete implementation, to define the time associated with any particular interval.
  • This description assumes a foreknowledge of Bézier curves; if you have no idea what I'm talking about, you might want to go look them up (such as on the blog linked above or various other places on the web, such as Wikipedia). Or you can just read along, refer to the mathematically imprecise sketch to the right, and hopefully not get too lost.
  • The code as written requires Android 4.0 to build. Actually, it's mostly compatible back to 3.0, but the PathEvaluator class uses a generic specification for TypeEvaluator that was introduced in 4.0 (not necessary, just convenient when I wrote the code).

On with the code.

The activity code is in PathAnimationActivity.onCreate(). First, we set up the path itself:
AnimatorPath path = new AnimatorPath();
    path.moveTo(0, 0);
    path.lineTo(0, 300);
    path.curveTo(100, 0, 300, 900, 400, 500);
Here, we are constructing an AnimatorPath (which is part of the demo project that we'll see below) and supplying it with operations that will become points in the path, along with the operations to navigate the intervals up to those points. The first operation defines where the path starts, (0, 0). Then we move in a straight line to (0, 300). Finally, we move along a curve (a cubic Bézier curve, to be precise) to the point (400, 500), using control points (100, 0) and (300, 900) along the way.

Next, we set up an ObjectAnimator to animate this path:
    final ObjectAnimator anim = ObjectAnimator.ofObject(this, "buttonLoc", 
            new PathEvaluator(), path.getPoints().toArray());
This animator uses a new PathEvaluator object (introduced below). It also queries the AnimatorPath object to get an array of PathPoint (covered below) objects; these will become the points in the animation that define the intervals that we animate between. The animator will send the animated values to the this object, which is the activity instance itself. We implement the setter below to receive those values and pass them along to the actual Button object that we want to move on the screen:
    public void setButtonLoc(PathPoint newLoc) {
        mButton.setTranslationX(newLoc.mX);
        mButton.setTranslationY(newLoc.mY);
    }

The AnimatorPath class referred to above stores information about the overall path. Its API consists of everything seen above:
public void moveTo(float x, float y);
    public void lineTo(float x, float y);
    public void curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y);
    public Collection getPoints();

Internally, AnimatorPath uses PathPoint to store the information at each point along the path. The PathPoint class is a simple data structure that just holds an x/y location, optional control point information (for curves), and the operation that tells the path containing that path point how to nagivate the interval leading up to that point. There are three factory methods that AnimatorPath uses to construct PathPoints as its API is called:
    public static PathPoint moveTo(float x, float y);
    public static PathPoint lineTo(float x, float y);
    public static PathPoint curveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y);

All of the logic of actually animating between points along the path (besides that in the Android Animator engine itself) is in the class PathEvaluator. This class implements the single method in the TypeEvaluator interface, evaluate():
    public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) {...}
The return value of evaluator() depends on the operation described by the endValue PathPoint object.

For curves, we calculate the x/y values given the anchor points (the location at startValue and endValue) and control points (both control points are stored in the endValue structure).
    if (endValue.mOperation == PathPoint.CURVE) {
        float oneMinusT = 1 - t;
        x = oneMinusT * oneMinusT * oneMinusT * startValue.mX +
                3 * oneMinusT * oneMinusT * t * endValue.mControl0X +
                3 * oneMinusT * t * t * endValue.mControl1X +
                t * t * t * endValue.mX;
        y = oneMinusT * oneMinusT * oneMinusT * startValue.mY +
                3 * oneMinusT * oneMinusT * t * endValue.mControl0Y +
                3 * oneMinusT * t * t * endValue.mControl1Y +
                t * t * t * endValue.mY;
    }
For lines, we perform a simple linear interpolation between the start and end points:
    else if (endValue.mOperation == PathPoint.LINE) {
        x = startValue.mX + t * (endValue.mX - startValue.mX);
        y = startValue.mY + t * (endValue.mY - startValue.mY);
    }
For moves, we simply jump to the location defined by the end value:
    else {
        x = endValue.mX;
        y = endValue.mY;
    }
Finally, we create a new PathPoint with this calculated xy information, which will be passed to the setButtonLoc() method seen above:
    return PathPoint.moveTo(x, y);

... and that's it. There's really not much to it, which was the whole point of posting this code. If you want to add curved motion to your animations, you can wait for the framework to do it for you... or you can take what I've done here and modify it to suit your needs. Yay, TypeEvaluator!

I've posted the code on code.google.com; check it out for yourself. Or you can download the Eclipse project (including the pre-built apk) here.

22 comments:

hazam said...

Hello Chet,
thank you again for sharing your code!

What do you think about reusing android.graphics.Path and android.graphics.PathMeasure objects?

I used those in some educational content that I'm composing.
Thank you!

-Emanuele

Chet Haase said...

@hazam: Reusing the existing Path API is definitely preferable, and is something I'd try to do in any version of this feature that got integrated into the SDK. The API was a bit complex for the simple things I was trying to do in my demo/prototype, so I skipped it in this particular case. But I wouldn't want to introduce a new kind of 'path' object just for the case of animation when there's a full-featured path in the library already.

Jens Zalzala said...

Are there any thoughts about creating some tools in eclipse (or elsewhere) to work with curves and prototype animations? It'd be nice to be able to move around handles and see results instantly.

Chet Haase said...

@Jens: I can't say what the future holds, but a graphical/animation tool would be quite different from the tools we offer so far, and would be a huge undertaking. It wouldn't be hard to write a simple one-off tool that just allowed you to draw a path and animate an object along it, but generalizing it to the larger case of animations overall wouldn't be trivial.

chiranjeevi said...

Hi Chet,
Thank you sharing your code. i have downloaded the total code .All files are working fine,but PathEvaluator.java file showing error,

public class PathEvaluator implements TypeEvaluator<\PathPoint>

changed to

public class PathEvaluator implements TypeEvaluator

application run safely.but animation is not working.When click on button i got an exception.

ERROR/PropertyValuesHolder(311): java.lang.reflect.InvocationTargetException

please help how to prepare curve motion in 3.0

Ian said...

Awesome post, Chet. I'm wondering why you used Bezier curves, though--seems like a Hermite representation (start, end, start velocity, end velocity) would be more useful for animation.

Chet Haase said...

@Ian: Bezier splines seem to be a common concept in animation tools and with animation designers. They're closely related to Hermite curves in any case; the control points of the Bezier determine the tangents at the start and end anchor points.
Also, my understanding is that the tangents on Hermite determine the direction, but not the velocity, or at least that's not how we'd treat it in animations. It would be too difficult to control the timing of the emotion along the curve if the speed was determined by the tangent vectors (or, in the case of the Bezier curves, the control points). See the second caveat on "mathematically correct" vs. what you want. It would be easier to be able to define the curve's geometry simply as the path that the animation would follow, but leave the details about how fast it travels along that curve to the other elements of the animation engine (e.g., TimeInterpolator).

Anonymous said...

Do anyone have a solution for Android 2?

Anonymous said...

nice posting.. thanks for sharing.

JiaJing WANG said...

Really nice post Chet - I was searching for animation along a path - but couldn't find it. (and btw, great talk at google I/O :)

BTW, since someone was asking the support on Android 2.x, here's someone who's built an property animation library that replicates the animations library for Android 1.0-Android 2.x:
http://nineoldandroids.com/

Combining the two , i'm hoping to be able to create some simple curved animation that can run on all android devices.

Unknown said...

Why doesn't Android just pull the work already done in Clutter?

Or it's Google policy to continue to reinvent?

Chet Haase said...

Er... because it's Android and uses a completely different language, set of APIs, and capabilities? The functionality of the animation framework in Android is similar to other animation frameworks, because they all share the concepts of timing-based property animation. Specifics of how they do that, and how the APIs work, is dependent on platform specifics.

Anurag Kulkarni said...

Hello,

I found your post really helping in getting knowledge about animation, but sadly I'm totally new to this field and got a bit confused. I came across this post while searching for a way to animate an object in a circular path with certain radius(user-input), from your code am sure it is possible but I'm not sure how, would you please help me modify the part of your code so that the button animates in a circular motion with repeat on? I would really appreciate your help. Thanks

Unknown said...

Thanks this is great. When trying to convert a iOS app I come across a lot of these animation related things that are simply much easier to implement in iOS. This is understandable as iOS has always been more about flashiness than substance, but it would be really nice if Android would add some of those functions, like easily flipping an image from one to the other (yrotation and xrotation are close but not quite it) and animating something over an arbitrary path (using the great Path class).

آرش said...

Thanks for your great tutorial , but Just a little question.
Is there anyway to change this code for drawing something?

Chet Haase said...

Not sure what you're trying to do, but there are various ways to draw curves already, including Canvas.drawPath(). If you're trying to animate the drawing of a path, it would be a simple matter to draw a line to each new point of the curve during the animation.

آرش said...

@chet Haase: thanks,i want to animate writing of for example letter "A" with this method,what do you suggest?

Rajkumar said...
This comment has been removed by the author.
Rajkumar said...

This post seems to be very old. Are things changed for curved animation now? or this is still one of the recomonded way of doing curved animation in Android?
Thanks.

Chet Haase said...

@Rajkumar: Yes, this is still a valid approach.

pradyumna said...

Hi chet ,
Interesting article man , I need to do some thing like this
http://www.coderanch.com/t/631354/Android/Mobile/Moving-Image-Bitmap-path#2890979
in my app , please give me some hints how to achieve that

Pokigayo said...
This comment has been removed by the author.