An Alternative to Animated Vector Drawables | Android Design Patterns
Today I am open sourcing the first alpha release of an animation library I’ve been writing named Kyrie. Think of it as a superset of Android’s VectorDrawable
and AnimatedVectorDrawable
classes: it can do everything they can do and more.
Motivation
Let me start by explaining why I began writing this library in the first place.
If you read my blog post on icon animations, you know that VectorDrawable
s are great because they provide density independence—they can be scaled arbitrarily on any device without loss of quality. AnimatedVectorDrawable
s make them even more awesome, allowing us to animate specific properties of a VectorDrawable
However, these two classes also have several limitations:
- They can’t be dynamically created at runtime (they must be inflated from a drawable resource).
- They can’t be paused, resumed, or seeked.
- They only support a small subset of features that SVGs provide on the web.
I started writing Kyrie in an attempt to address these problems.
Examples
Let’s walk through a few examples from the sample app that accompanies the library.
The first snippet of code below shows how we can use Kyrie to transform an existing AnimatedVectorDrawable
resource into a KyrieDrawable
that can be scrubbed with a SeekBar
:
KyrieDrawable drawable = KyrieDrawable.create(context, R.drawable.avd_heartbreak);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
long totalDuration = drawable.getTotalDuration();
drawable.setCurrentPlayTime((long) (progress / 100f * totalDuration));
}
/* ... */
});
The video in Figure 1 shows the resulting animation. We can pause/resume the animation by calling pause()
and resume()
respectively, and can also listen to animation events using a KyrieDrawable.Listener
. In the future, I plan to add a couple more features as well, such as the ability to customize the playback speed and/or play the animation in reverse.
We can also create KyrieDrawable
s dynamically at runtime using the builder pattern. KyrieDrawable
s are similar to SVGs and VectorDrawable
s in that they are tree-like structures built of Node
s. As we build the tree, we can optionally assign Animation
s to the properties of each Node
to create a more elaborate animation. The code below shows how we can create a path morphing animation this way:
// Fill colors.
int hippoFillColor = ContextCompat.getColor(context, R.color.hippo);
int elephantFillColor = ContextCompat.getColor(context, R.color.elephant);
int buffaloFillColor = ContextCompat.getColor(context, R.color.buffalo);
// SVG path data objects.
PathData hippoPathData = PathData.parse(getString(R.string.hippo));
PathData elephantPathData = PathData.parse(getString(R.string.elephant));
PathData buffaloPathData = PathData.parse(getString(R.string.buffalo));
KyrieDrawable drawable =
KyrieDrawable.builder()
.viewport(409, 280)
.child(
PathNode.builder()
.strokeColor(Color.BLACK)
.strokeWidth(1f)
.fillColor(
Animation.ofArgb(hippoFillColor, elephantFillColor).duration(300),
Animation.ofArgb(buffaloFillColor).startDelay(600).duration(300),
Animation.ofArgb(hippoFillColor).startDelay(1200).duration(300))
.pathData(
Animation.ofPathMorph(
Keyframe.of(0, hippoPathData),
Keyframe.of(0.2f, elephantPathData),
Keyframe.of(0.4f, elephantPathData),
Keyframe.of(0.6f, buffaloPathData),
Keyframe.of(0.8f, buffaloPathData),
Keyframe.of(1, hippoPathData))
.duration(1500)))
.build();
Figure 2 shows the resulting animation. Note that Animation
s can also be constructed using Keyframe
s, just as we would do so with a PropertyValuesHolder
.
Kyrie also supports animating along a path using the Animation#ofPathMotion
method. Say, for example, we wanted to recreate the polygon animations from Nick Butcher’s Playing with Paths blog post (the full source code is available in the sample app):
KyrieDrawable.Builder builder = KyrieDrawable.builder().viewport(1080, 1080);
// Draw each polygon using a PathNode with a custom stroke color.
for (Polygon polygon : polygons) {
builder.child(
PathNode.builder()
.pathData(PathData.parse(polygon.pathData))
.strokeWidth(4f)
.strokeColor(polygon.color));
}
// Animate a black dot along each polygon's perimeter.
for (Polygon polygon : polygons) {
PathData pathData =
PathData.parse(TextUtils.join(" ", Collections.nCopies(polygon.laps, polygon.pathData)));
Animation<PointF, PointF> pathMotion =
Animation.ofPathMotion(PathData.toPath(pathData)).duration(7500);
builder.child(
CircleNode.builder()
.fillColor(Color.BLACK)
.radius(8)
.centerX(pathMotion.transform(p -> p.x))
.centerY(pathMotion.transform(p -> p.y)));
}
The left half of Figure 3 shows the resulting animation. Note that Animation#ofPathMotion
returns an Animation
that computes PointF
objects, where each point represents a location along the specified path as the animation progresses. In order to animate each black circle’s location along this path, we use the Animation#transform
method to transform the points into streams of x/y coordinates that can be consumed by the CircleNode
’s centerX
and centerY
properties.
Future work
I have a lot of ideas on how to further improve this library, but right now I am interested in what you think. Make sure you file any feature requests you might have on GitHub! And like I said, the library is still in alpha, so make sure you report bugs too. :)