Simple frame by frame spinner animation
This tutorial shows how to implement a simple frame by frame spinner animation using a custom view and an animator.
I find the easiest way to learn frame by frame animations is to start of by a simple example and just add stuff to it and see what happens. I hope this tutorial can function as a base to build upon.
We’ll be covering:
- Custom view implementation
- Drawing to canvas
The demo project can be found at my Github account: Loader Demo
Ohh and before we start, don’t forget the first rule of creating a custom view:
Never perform heavy computations or allocate new objects inside the onDraw() implementation.
There are a few cases where it’s justified to allocate new objects inside
onDraw() but generally speaking, everything that can be done before it’s invoked, should be. This is why, in this tutorial, the
RectF objects are allocated once and reused, and the
onDraw() implementation is basically a one-liner.
Let’s get started
Lets start of by creating our new class and extending the View class. We will be doing custom canvas drawing so lets also add and initialize an instance of the
So far, so good. The only thing to notice here is that AntiAlias is being enabled. This is usually a good idea to get smooth edges when drawing circles or other non-rectangular shapes.
Determining the size
Handling the size of a custom view can be done in several ways. For some cases it’s a good idea to set a desired/minimum size but in this case we are doing a spinner and we want to let it scale to whatever size is given to it by its parent. This also means that if the
layout_width/height is set to
wrap_content the view will most likely not appear. Instead just if fixed values like
64dp or something.
We define the stroke width as 5% of the smallest side of the view. Why the smallest? If the view is not square, we want to base the stroke width on the smallest side to be sure it will fit in the view. Making the stroke width depend on the view size makes it scale automatically if the view size is changed.
When drawing a stroked line at an X,Y position it’s always the center of the brush that’s positioned there. This means that if the position is set the one of view edges, half the stroke width will be drawn outside of the view. To counter this, add half the stroke width as a padding.
onSizeChanged() is only called when the view bounds has changed, which means that it’s better to define sizes here, compared to calculating them in each call to
onDraw(). The bounds are calculated once and stored as a
RectF field and reused in every call to
onDraw(). When setting the bounds it’s important to take padding into account, since it won’t be done automatically. The padding in this case is both the one defined in XML by
android:padding="" and the paint padding mentioned above.
Making it move
Let’s add the animation code. We’ll be using a
ValueAnimator with a range from
0.0f -> 1.0f and an
LinearInterpolator as a base. I find it easier to use a
LinearInterpolator for the animator but more on that below.
Defining the animator here makes it start when the view is added to it’s parent, making the animation to be running when the view appears to the user. There are three parts to note in this implementation:
setRepeatCount(ValueAnimator.INFINITE)is used to make the animator loop when it’s reached the end
setRepeatMode(ValueAnimator.RESTART)is used so that the animation is played from the beginning when looping, instead of being reversed.
addUpdateListener(...)is where the magic happens. The
AnimatorUpdateListeneris called every time the animation needs to refresh (approximately every 16.7ms) and it’s its job to make sure to progress the state of the frame by frame animation. In this tutorial it’s responsible for calculating the starting position of the arc (making the spinner rotate) and the arc size (making it grow/shrink) based on how far along the animator has progressed. When the state is up to date,
invalidateis called to trigger a call to
When this is called, the view is no longer visible and it’s safe to tear down the animation. Worth noting is that this is not the same as the
onPause, so if the animation should be paused when the app is put in the background, methods for starting and stopping the animation should be made public so that the Activity can control it. In this tutorial however that’s left as an exercise for the reader (homework…yay…).
Calculating size and start position
The following methods define how the animation will behave.
Here it becomes clear why a
LinearInterpolator was used as a base for the
Animator. When calculating the size and position we want to apply different types of interpolations and a linear interpolation is easy to transform. During one loop we want the spinner size to both expand and shrink, so the animator progress is passed to a sine function.
progress = 0.0f
Math.sin(progress * π) -> 0f
progress = 0.5f
Math.sin(progress * π) -> 1f
progress = 1.0f
Math.sin(progress * π) -> 0f
We also want the spinner movement to be decelerated, so again we transform the progress using a
Drawing the frame
The last piece of the puzzle is to draw the current state to the canvas. Since all values needed are prepared, we’re left with a nice one-liner.
Now all that’s left is to add the view in the app. Since it scales, make sure to define the size using
Well that’s all there is to it. The example project used for this tutorial can be found here.