Custom Drawables and Animations
There are many opportunities where the usage of a custom drawable will lead to nice, clean, and reusable results. If you need to display changing text inside an icon or introduce a custom progress indicator like in Modifying the resource image of Progress Bar on Stack Overflow, you can easily do so with custom drawables.
With this guide I want to point out what’s necessary to set up, draw, and animate drawables with little effort. This is a basic sample, so please take the code and modify it to your needs! A compilable project with the full source code is available at GitHub.
So this is what we are trying to accomplish within the next few steps: a simple star that spins when clicked. The sample sets the menu icon in a
Toolbar. This is to point out another good use case for custom drawables, where you could instead display an unread count or coin balance in your menu!
Why custom drawables?
Power. With custom drawables you get a canvas and can let your creativity flow. By understanding the basics of how to draw text and shapes on your canvas you can achieve almost anything. There are alternatives to using custom drawables that require less effort like
animation-list drawables, but you will need a separate image for each frame greatly increasing your app size and you can not interpolate in-between the single pictures.
By drawing circles and arcs yourself you can use Interpolators to improve readability and reusability as they provide a simple interface to apply minor tweaks to your animation. e.g. Interpolators will smooth out the acceleration, leading to a more natural look and feel. In the image above an
AnticipateOvershootInterpolator was used, with which the animation will start and end in the opposite direction.
Basics: Coordinates & Bounds
So far it seems most people struggle with basic drawing. Take some pen and paper and start scratching out what you are trying to accomplish. Possible paddings, margins, and other things you need to consider will be much easier to grasp. Adding this knowledge to the comments will also help to greatly improve the readability of your code, since drawing operations are always hard to follow.
First things first: the top left is at [0,0]. Although, this may not be where you are expected to draw. Every drawable has bounds which specify its position and dimension on the canvas.
getBounds() tells you where to position and draw your drawable, without unwanted side effects.
Using bounds will make things such as adding an additional padding or offset easy since you can just apply transformations to your bounds. With this in mind, the top left of your drawable then changes to
[bounds.left, bounds.top] which may seem somewhat irritating at first, but will greatly simplify things with more complex drawing. These bounds also provide you
height() which you will need fairly often.
Don’t forget to make a copy of your bounds before modifying them. e.g. Applying an inset or offset to your bounds will lead to different results for each drawing operation.
Above all, keep in mind that, even if there are no lint checks (yet?), running animations will call your
draw() method every 16ms. This means: Do as little as possible while actually drawing.
If you have paths or complex arrangements that you can compute beforehand you should do so. By overriding
setBounds(int, int, int, int) and calling an
init() method you have a great hook to update your values whenever the bounds change.
This init method will also be used in the sample below to keep the drawing method clean and simple.
Measuring your drawables
getBounds() will return your intended size. There should not be too many situations where you need to think about setting your height and width yourself and you should not draw with a fixed pixel size. But in case that you absolutely need to, overriding
getIntrinsicWidth() will account for just that. Here you can specify the height and width you wish to be, but doing so might still lead to different values for your bounds.
The aforementioned is also needed if you want to set your custom drawable as an icon for a
MenuItem. Reading the android source code, it seems that your drawable will always receive
min(intrinsincHeight, menuIconMaxHeight), which will by default set your bounds to
(width:-1, height:-1) if you don’t override
This is a bug, since there is no further possibility to get the actual size and position for your drawable. The canvas will have arbitrary bounds and your drawing will be offset and cut off. To fix this you can make use of the
min clause and just return a large number for your intrinsic height. You then will get correct bounds.
The only problem with this solution is that it reduces the reusability of your code. By requesting a fixed height using your drawable as a background or in an
ImageView will squeeze or stretch your drawing instead of letting you account for the different sizes yourself.
Example: Calculate a path for a hexagram
As mentioned before, the calculation for your path should always be the same and hence should be performed only once, and if possible outside of the
draw method. The following sample uses some basic trigonometry to calculate the positions of a triangle within a circle and basically just sets those 2 triangles on top of each other, leading to a hexagram or star like figure.
How to calculate paths and or use trigonometry is not part of this article. I just will mention again, take some pen and paper to sketch out the bounds and alignments of your objects, as this will make things easier to grasp and implement.
Given that we already set everything up in
init(), the drawing operation itself should be simple enough. If you have multiple objects you would add modifications to your paint object (e.g. color, alpha) and just draw your objects one after the other.
##Animating the star
In this sample I want to show how to spin the star, but so far it is just statically drawn on the canvas. To support animations the drawable has to implement
Animatable. For reasons of simplicity this sample drawable will also implement
Runnable which you could supply as a separate object.
Animations always have to do with time, so this is where we start:
FRAME_DELAY is a constant used to supply 60 fps or 16ms and is mostly used for readability.
mRunning will keep track of the drawables’ state, while
mStartTime is used to calculate the animation progress.
mDuration determines the length of the complete animation in ms. This will usually be a value between 250 and 1000.
Start, stop, and run
Start and stop schedule a
Runnable to be run. They also keep track of the state, so that only one animations runs at once. Start will set
mStartTime and start scheduling the runnable, possibly stopping an already running animation.
invalidateSelf() will cause a request for the drawable to be redrawn. This will lead to a call to
draw() on the drawing pass.
The main work lies within the
Runnable implementation: Check if the animation is finished or schedule the drawable again. By not rescheduling itself you remove the need of calling
unscheduleSelf(). To support indeterminate drawable animation you could simply call
start() when the animation finished to just start all over again.
Animating the drawable
The real animation of course takes place in
draw() where we need to account for the progress and rotate the star accordingly.
In this sample we will just rotate the canvas, since this eliminated the need to recalculate the path every 16ms. We can reuse the same path and even stretch, shrink, or move the star by just manipulating the canvas.
The progress should be within [0,1] and to do so we just take
elapsed / total. The interpolator helps to smooth out the look of the animation and make it feel more natural. For values between 0 and 1 the output of the interpolator again is within [0,1]. You can read a bit more on interpolators in the next section.
In the above sample we already used an
Interpolator. They help to apply some further enhancements to a linear interval, letting us easily tweak some simple animations. e.g. Using an
AccelerateDecelerateInterpolator would gradually speed up at the beginning of the animation and slow down at the end.
Using interpolators like the
AnticipateInterpolator will start with the star spinning into the opposite direction before accelerating in the right direction. Using these interpolators greatly simplifies more natural looking animations and helps to improve reusability and to support little modifications.
The source code is available at GitHub within a runnable project.
Thanks to Nick Liu for proof reading.