Introducing Kyrie - An Alternative to Animated Vector Drawables

Posted

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 VectorDrawables are great because they provide density independence—they can be scaled arbitrarily on any device without loss of quality. AnimatedVectorDrawables make them even more awesome, allowing us to animate specific properties of a VectorDrawable in a variety of ways.

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.

   Figure 1 - Creating a seekable animation from an existing AnimatedVectorDrawable resource (source code). Click to play.

We can also create KyrieDrawables dynamically at runtime using the builder pattern. KyrieDrawables are similar to SVGs and VectorDrawables in that they are tree-like structures built of Nodes. As we build the tree, we can optionally assign Animations 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 Animations can also be constructed using Keyframes, just as we would do so with a PropertyValuesHolder.

   Figure 2 - Creating a path morphing animation using keyframes (source code). Click to play.

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.

   Figure 3 - Recreating the polygon animations from Nick Butcher's Playing with Paths blog post (source code). Click to play.

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. :)

+1 this blog!

Android Design Patterns is a website for developers who wish to better understand the Android application framework. The tutorials here emphasize proper code design and project maintainability.

Find a typo?

Submit a pull request! The code powering this site is open-source and available on GitHub. Corrections are appreciated and encouraged! Click here for instructions.

Apps by me

Shape Shifter simplifies the creation of AnimatedVectorDrawable path morphing animations. View on GitHub.
2048++ is hands down the cleanest, sleekest, most responsive 2048 app for Android!