Creating custom Android views – Part 3: Animating your custom views, smoothly

In part 2 of this series we started doing a line chart view. This post assumes you’ve read part 2 so if you haven’t I’d recommend reading that first. Now, let’s continue working on the line chart view.

What I have in mind is three buttons on top of the graph where the user can toggle the line chart to display data for different categories, in this case different training categories: walking, running and cycling. When the user presses a button the line chart we change the datapoints for the line chart view to the new datapoints.

If we implement the buttons and set new datapoints and run the application right away nothing will happen to the line chart view. The setChartData() method is called the new datapoints are set but the view doesn’t draw the new point. Why? Well, what we’ve forgotten is to tell the view that we want it to redraw. This is done by calling invalidate(). This tells the view hierarchy that the view is invalid and needs to be redrawn. Adding a call to invalidate() in setChartData() solves our problem, the line chart is updated with the new values. However, wouldn’t it be nice if the line chart animated the change rather than just change?

If we want to animate the line chart there is two things that we need to work out how to solve. First, we need the line chart data values change, step by step, from the old values to the new values. Second, we need some mechanism to update the view for each step.

Let’s start with changing the values first. There are many ways we could change the values. The simplest maybe being a simple linear interpolation and then there are more advanced interpolation. What we are going to do here is a bit different though.

Dynamics

To help with this we will use a class I’ve called Dynamics. A Dynamics object is basically a (one dimensional) point. It has a position and a velocity. It also has a target position, where it wants to be, and an update() method that will update both the position and velocity of the point. It looks like this:

The first thing we do in this function is to calculate the time step which is basically the time elapsed since the last update. In this case I’ve also made sure that the maximum time step is 50 ms. The reason for this is mostly to avoid large glitches in the animation even if, for some reason, the time between two updates grow very large.

Then we update the velocity which depends on how far away we are from our target position. This is multiplied by the “springiness” which is a constant that determines how “springy” the point is. The updated velocity is then decreased a bit using a damping factor (which should be more than 0 and less than 1). If we don’t do this part the spring will continue to move for ever. You can think of this part as a shock absorber on a car that damps the spring in the suspension.

Then we use the velocity to update the position using simple Euler integration and store the time of this update so we can calculate the time step next time.

Using this, the the point will behave more or less like if it was attached to a spring. With a high value of springiness the point will accelerate to the target position faster and oscillate around it more. If we increase the damping factor, then we will slow down the acceleration and if the damping is high enough the point will not oscillate at all.

Animating this way is a bit different than interpolation. If we would have interpolated the values we would have needed a duration during which to interpolate. Using this, it will take the time it needs. But we need to know when to stop the animation. For this we add the following method:

This returns true if the point is at the the target position and if the velocity is 0. Since it’s not a good idea to compare floats for equality, we check instead if it’s close enough to the wanted value. In this case TOLERANCE is 0.01f which is good enough for us in this case.

Using dynamics

Updating our LineChartView code to use the new dynamics objects is quite easy. The part that creates the path for the chart looked like this when we left it last time:

After we’ve changed the datapoints from regular floats to Dynamic objects the same snippet will look like this instead

As you can see, the only difference is that we use the function getPosition() on the dynamics to get the value.

Running the animation

What we need now is a way to repeatedly call update on the datapoints and trigger redraws of the screen. To do this we are going to use a Runnable. A runnable is a small interface that represents a command, something to execute. It’s often used to run things on another thread but here we will use it on the UI-thread.

The runnable that we are going to use looks like this.

In its only method, run() we loop through all the datapoints, which are now Dynamics objects, and call update() on them. If any of them is not at rest, we will need to schedule a new frame. This is done by posting the same runnable again with a bit of delay. Finally, we call invalidate to trigger a new draw.

Now, what if the animator runnable is executed again before the next draw? After all, it might be more than 15ms to the next draw, we’re not in control of that. The nice thing is that the runnable will be wrapped in a message and added to the MessageQueue of the Looper of the UI-thread, and the same thing will happen to the invalidate call. The looper will then dispatch the messages in order and thus the next draw is guaranteed to happen before the next execution of the runnable.

The combination of dynamics and runnables is a very convenient way to handle animations. It’s easy to setup and it’s easy to add animation to a previously non animated property. I routinely use this pattern with almost all animations within a View or ViewGroup. Most of the time I start with getting the drawing and all interaction done and when that is in place and works like it should, I change the code to use a dynamic for the property and animate it with a runnable.

Let’s take a look at the setChartData() method that uses the updated Dynamics array of datapoints instead of regular floats

There is two different cases we need to handle. The first is if we don’t have any data before (or if the new data is not the same length as before). If we don’t we create a new array of Dynamics objects and initialize them. We set the position to the y value and the velocity 0. Then we set the target position to the same as the position and invalidate the view to trigger a redraw.

If, on the other hand, we already have the set the data points before, the only thing we do is that we change the target position to the new value and then we start the animation. To start the animation we call post() and post our animator runnable. However, the animation might already be running, so before we post it we remove any instance of it. I would recommend always removing a runnable before posting it. If you end up with the same runnable posted multiple time you can get very hard-to-debug problems. Better safe than sorry. We don’t need to call invalidate here. The only thing that actually change is the target values, the current position is not changed until the update method is called on the datapoints in the animator runnable, and the animator runnable will call invalidate for us.

Smoothing things out

Just one thing left. The graph looks a bit jaggy, don’t you think. Let’s change the part that constructs the path from using the simple lineTo() method to use cubicTo() instead. This will add a cubic bezier to the next point. To use this we also need to to calculate the coordinates for the two control points.

A good way of picking them is to place the first control point on a line that goes through the starting point (point i) and has the same slope as the line between point i-1 and point i+1. The second control point should be on the line that goes through the end point (point i+1) and has the same slope as the line between point i and point i+2. This will produce a path that is continuous and whose derivative is also continuous. In other words, it will look smooth.

You can see the result below.

The first part tries out the animations of the graph between the three states. The second part of the video tries to stress-test the animations. A problem of many types of animation, including the standard animations on Android, is that when changing the animation target during an ongoing animation. Let me explain by an example.

Say you have a simple application that contains two things. An image and a button. The image is hidden when starting the application. Pressing the button toggles the image visibility by fading it up and down using an AlphaAnimation. If we only press the button when the animation is finished, it all looks good. But if we press the button to fade in the image, and then quickly presses it again to fade out, the image will suddenly jump from a half transparent state to a fully opaque state and then fade out. These types of animations bugs can be seen in quite a lot of places and they often look a bit like flickering and can be quite jarring.

As you can see from the video, the animation here does not have that problem. Changing the target position of the animation does not change the current position or the velocity directly. The animation will change direction over time and smoothly animate to the new target position

Another thing you might notice is that I’ve added text, or rather, numbers, on the vertical axis. I’ll return to this in another part, but for now, it’s just there to make it a bit more complete.

Stay tuned for the next part!

You can find the sources for this part here.

13 Comments

  1. evelyne24

    Hi Anders,

    Is it possible to update the tutorial with the sources you showed at Droidcon UK? That’d be very helpful. Thanks!

  2. MH

    How can i display 2 lines on one graph?

    • Anders Ericsson

      You’ll need to change some things but it’s not much work. First of all make the datapoints member in LineChartView an array of array of Dynamics instead. Then let the methods drawLineChart and createSmoothPath take an array of Dynamics as an argument. Then in onDraw, loop through the arrays of Dynamics in datapoints. You’ll of course also need to change the setDatapoints method and the runnable and maybe some other things as well. Good luck!

  3. Enke

    Hi,

    Thank you for a wonderful tutorial!

    I need the graph line to be animated point by point. Like the graph is being drawn. I’m assuming that I’ll have to work in setChartData to animate from point to point rather from data set to data set.

    Any tips?

    Enke

    • Anders Ericsson

      Glad you liked the tutorial!

      One way of doing it could be to call setChartData several times with a delay in between. Let’s say you have the data [5, 4, 2, 7, 4, 7] and first you call setChartData([5, 0, 0, 0, 0, 0]), wait a few hundred milliseconds, then call setChartData([5, 4, 0, 0, 0, 0]), wait a few hundred milliseconds and so on util you finally call setChartData() with all the correct values. That might work directly, without any changes.

      Another solution would be to have one Runnable for each point, and in setChartData you post them all but the higher index of the datapoint, the longer delay.

      I guess it depends a bit on how (specifically) you want it to behave/look.

      Good luck with the changes!

      • Enke

        Thank you so much for your prompt reply!

        I’m going to try that as soon as I can get my broken work station up and running. But that makes some sense. I’m sure I can get it to work.

        I think I was heading it the latter direction, but your first suggestion sounds so much easier.

        Will report back!

  4. Dillon

    I really loved this series of tutorials. I was having trouble understanding exactly how to implement a custom View and this cleared a lot up for me.

    When I look at the animation it looks to be very smooth which is great. I was curious what the GPU profiler that is built into my phone would show. Because the animation looked so smooth I was expecting that all of the bars in the GPU profiling tool would fall below the 15ms mark, however I was surprised to find that almost all of the bars were far above the 15ms mark. The animation looks so smooth, but the profiler indicates that the view is taking anywhere from 30ms – 40ms to fully render. How can this be?

Trackbacks for this post

  1. New Adventures in Software » Thoughts on Droidcon London 2012
  2. Dynamics Animations using Custom Views – Sources – Jayway
  3. Creating custom Android views – Part 4: Measuring and how to force a view to be square – Jayway
  4. How to animate a graph being drawn in Android | BlogoSfera
  5. How to animate a graph being drawn in Android | BlogoSfera

Leave a Reply