Simple frame by frame spinner animation

Patrick Elmquist
4 min readJul 12, 2017

--

This tutorial shows how to implement a simple frame by frame spinner animation using a custom view and an animator.

The final product.

Introduction

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
  • ValueAnimator
  • 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 insideonDraw() but generally speaking, everything that can be done before it’s invoked, should be. This is why, in this tutorial, the Paint and 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 Paint class.

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.

Stroke width

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.

Paint padding

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.

Bounds

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.

onAttachedToWindow

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 AnimatorUpdateListener is 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, invalidate is called to trigger a call to onDraw().

onDetachedFromWindow

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 Activity receiving 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 DecelerateInterpolator.

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 android:layout_width/height

Wrapping up

Well that’s all there is to it. The example project used for this tutorial can be found here.

--

--

Patrick Elmquist

Software Engineer@IKEA. Caffeinated unicorn of excellence, with an interest in UX and design