Tags
android, animation, bezier, bezier curve, canvas, curve, custom view, custom-ui, draw, font, motion, motion animation, mozilla, number, optimize, paint, path, segment, textview, tween, tweening
Timely is a beautiful Android app. Waking up to see the numbers animate is like a morning bliss. I was intrigued by how the numbers are animated in that app. It’s not a simple fade-out-fade-in effect. It has some kind of folding in it — but not the same as the airport status boards. UiAutomator says that the entire block is one custom view. By profiling the GPU, I found that something is drawn to the screen consistently — even when there is a pause between animations. What could it be? That’s when Christoper Nolan came in my dreams and asked, “Are you watching closely?”.
The numbers are not directly coming from a font drawn as a TextView, instead constructed as multiple segments. The transition from one number to another happens by shape tweening the segments. Interesting! But how does a line change to a curve and back to a line? That’s where bezier curves help us. A bezier curve has two end points, say A and B, and two control points, say C1 and C2. The curve is drawn by.. alright, no math here! 😛 And the interesting fact is, if the C1 = A and C2 = B, we get a straight line from A to B. So a cubic bezier can behave as a curve and a line — that’s all we need.
The first part of the problem is to identify the control points. Numbers like 1 and 7 needs only two control points. However, numbers like 0, 3, 8 needs 5 control points. If we have to map 4 segments between numbers, then all the 10 digits should use five control points. The image above doesn’t show all the points in digit 1. You could assume that points 3, 4 and 5 are shared. The digit 7 doesn’t need 5 control points. But they are added so that the animation from 7 to 8 looks nice. The next part is to draw the curves with these control points. If you are using an application like Adobe Illustrator, trying this out is easy. The following image shows how the curves are created for number 3. But wait! The choice of font also matters. They choose Futura Light, which has nice curves and straight lines. If you were to try this with Roboto — good luck! 😉
Now that we’ve identified the end points and the control points for each segment, lets mash up some code.
public class NumberView extends View { private final Interpolator mInterpolator; private final Paint mPaint; private final Path mPath; // Numbers currently shown. private int mCurrent = 0; private int mNext = 1; // Frame of transition between current and next frames. private int mFrame = 0; // The 5 end points. (Note: The last end point is the first end point of the next segment. private final float[][][] mPoints = { {{44.5f, 100}, {100, 18}, {156, 100}, {100, 180}, {44.5f, 100}}, // 0 ... and for the other numbers. }; // The set of the "first" control points of each segment. private final float[][][] mControlPoint1 = { ... }; // The set of the "second" control points of each segment. private final float[][][] mControlPoint2 = { ... }; public NumberView(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); mInterpolator = new AccelerateDecelerateInterpolator(); // A new paint with the style as stroke. mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.BLACK); mPaint.setStrokeWidth(5.0f); mPaint.setStyle(Paint.Style.STROKE); mPath = new Path(); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); // Frames 0, 1 is the first pause. // Frames 9, 10 is the last pause. // Constrain current frame to be between 0 and 6. final int currentFrame; if (mFrame < 2) { currentFrame = 0; } else if (mFrame > 8) { currentFrame = 6; } else { currentFrame = mFrame - 2; } // A factor of the difference between current // and next frame based on interpolation. // Only 6 frames are used between the transition. final float factor = mInterpolator.getInterpolation(currentFrame / 6.0f); // Reset the path. mPath.reset(); final float[][] current = mPoints[mCurrent]; final float[][] next = mPoints[mNext]; final float[][] curr1 = mControlPoint1[mCurrent]; final float[][] next1 = mControlPoint1[mNext]; final float[][] curr2 = mControlPoint2[mCurrent]; final float[][] next2 = mControlPoint2[mNext]; // First point. mPath.moveTo(current[0][0] + ((next[0][0] - current[0][0]) * factor), current[0][1] + ((next[0][1] - current[0][1]) * factor)); // Rest of the points connected as bezier curve. for (int i = 1; i < 5; i++) { mPath.cubicTo(curr1[i-1][0] + ((next1[i-1][0] - curr1[i-1][0]) * factor), curr1[i-1][1] + ((next1[i-1][1] - curr1[i-1][1]) * factor), curr2[i-1][0] + ((next2[i-1][0] - curr2[i-1][0]) * factor), curr2[i-1][1] + ((next2[i-1][1] - curr2[i-1][1]) * factor), current[i][0] + ((next[i][0] - current[i][0]) * factor), current[i][1] + ((next[i][1] - current[i][1]) * factor)); } // Draw the path. canvas.drawPath(mPath, mPaint); // Next frame. mFrame++; // Each number change has 10 frames. Reset. if (mFrame == 10) { // Reset to zarro. mFrame = 0; mCurrent = mNext; mNext++; // Reset to zarro. if (mNext == 10) { mNext = 0; } } // Callback for the next frame. postInvalidateDelayed(100); } }
Given a set of end points and control points, the idea is to drawn 4 cubic bezier curves. We create a path and move to the first end point. From there, we draw a bezier curve to the second end point using the given control points. This end point will be the “first” end point for the next segment, and so on. This logic would drawn the numbers without any animation. To kick in some animation bits, each transition is defined by 10 frames, called at 100ms interval. The first two and the last two frames are static — that how the number stops for a heartbeat. The in-between 6 frames maps the end points and control points for the segments between the numbers, draws a part of the transition based on the interpolated value. To put this in simpler terms, let’s take the “first” end points to be mapped to be (100, 100) and (124, 124). The first of the six frames would draw (100, 100). The second would draw ((124 – 100)/6, (124 – 100)6), that is (104, 104). The third of the six frames would draw (108, 108) and the last would finally end at (124, 124). Now extend this argument for other end and control points. This would change the curves from one to another — with in-between curves looking twisted and turned 😉
As you can see in the code, all 6 points are calculated in each draw frame. This might not be a good. Since we know the interpolation, these values could be pre-calculated and held as a giant array of points. It would also be nice to have all the points to be a float value between 0 and 1, as that would allow scaling the numbers to any sized canvas easily. If you were to deal with different width for each digit, and have other texts showing up, which could also animate, and calculate where each digit show go in a single View — that’s when I bow and respect the Timely app! They’ve done more work that what this blog post says. This post is just to give an idea of how such things can be done. Their work is a piece of art! To see how this animation works, try a HTML version here.
My awe of the Timely app just increased tenfold after learning of the effort they must have put into it.
I want to translate this to iOS now and use it!
Here it is on iOS guys, https://github.com/MattFoley/MFLFoldingClock
Pingback: iOS Library For Creating Slick Number Transitions Like Android’s Timely Clock App
Hi, you wrote “Since we know the interpolation, these values could be pre-calculated and held as a giant array of points” … can you please expand this a little bit more? I’m developing a library based on this (planning to upload it on GitHub once I have it working :)) and I’m finding a little bit hard to make it more performant.
Thanks!
Let’s say we use a LinearInterpolator to go from 0 to 100 in 10 frames. So, the pre-calculated interpolated values will be 0, 10, 20, 30, … , 90, 100. Similarly, based on the interpolator used, and the start and end points, the in-between points that are needed can be pre-calculated and stored.
In other words, the value of expression “curr1[i-1][0] + ((next1[i-1][0] – curr1[i-1][0]) * factor)” is always going to give the same result between one frame to another. These values can be pre-calculated and stored. So the mPath.cubicTo() will not do calculation, but used to calculated values from a giant array.
Why are you doing a saveLayer() call? It’s expensive and as far as I can tell completely unnecessary here. Actually you don’t even need a save/restore.
Oops. I copied the structure from another piece of code where I was doing some PorterDuff cliping. So the save() and restore() stayed. Will remove it. Thanks 🙂
Can you go into more detail on the mControlPoint1 and mControlPoint2 arrays? As I understand it those are coordinates to generate the bezier curves, as in the image of the “3”. Why are there two distinct arrays of points for that though? If it’s at all possible to maybe take a screenshot and show how the coordinates in those arrays align to for example the “3” that would be great. Just trying to wrap my head around this as I’d like to do something similar for Roboto Light.
The idea of keeping two array for the control points is for the sake of clarity. Each bezier curve has 2 control points — the first one is in first array and the second in the second array. I had to change the coordinates for 3 to make the transition smooth. Here’s an image for the final “3” I used: http://cl.ly/image/0H3v0D1T2j2b
In the image, the solid points are mentioned as 1,2,3.. The control points are named as C1,2 etc. C1,2 means “first” control point of the second curve. C2,3 stands for “second” control point of third curve.
Here’s how my array looks:
[63.25, 54], [99.5, 18], [99.5, 96], [100, 180], [56.5, 143] <– solid points.
[63, 27], [156, 18], [158, 96], [54, 180] <– first set of control points.
[86, 18], [146, 96], [150, 180], [56, 150] <– second set of control points.
Note that all these points are in a 200×200 grid.
Doing for Roboto would take more than 5 points. In fact, even for Futura, Timely used 7 points to get a smooth transition.
Thank you, for sharing this. but i would like to know how to use this view in an application.I mean i copied this code to the package and Added the it in the layout… what else should i do to show the animation in the layout?
The code is not full. As you can see, the float values for three arrays are missing. Few people went ahead and created libraries. May be that would help.
Can u tell how show the values from 1-20, because i am able to show only single digit but need to show in two digits?
Thank you, How can I get the coordinates is there any software that can help me get it?
Thanks for the post. It’s very helpful. However, if for a totally different font, how can I get all the end points and control points for each character?
Thanks!
You would have to create it using a vector tool like illustrator to find the points.
Pingback: Timely number animation | Armijn Vink - Android developer
Hi Sriram, I’ve tried to translate the Roboto font based on your project.
http://armijnvink.wordpress.com/2014/06/07/timely-number-animation/
This kind of path morphing is much easier with the new animated vector drawables, see example here: https://github.com/chiuki/animated-vector-drawable
general question , as these are numbers , control points are known beforehand . But what if i give u a new symbol . how are you going to calculate control points of that symbol ?
We cannot do that. If you represent them as a SVG or something, then we can do that calculation. Otherwise it will be just magic numbers everywhere.
Hi thanks a lot for the insight, it was really great. Actually I want to implement with double digits (Here we have only single digit i.e. from 0-9) i.e. from 0 to 99. Can you help me as to how can i achieve this?