Stuart Kent
(Android Developer)

An Intro To PathInterpolatorCompat

Version 22.1 of the v4 support library added several new Interpolator classes to help developers infuse their applications with Authentic Motion. Today, we’ll explore the highly-flexible PathInterpolatorCompat class.

As the name suggests, PathInterpolatorCompat is a utility for creating Path-based interpolators. The goals for this post are as follows:

Paths

If it’s been a while since you worked with a Path, here’s the snappy definition from the docs:

The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves.

In plainer English:

A Path is a collection of (not-necessarily-connected) straight lines, curves and shapes.

Paths are most commonly used to draw complex shapes on a Canvas via the Canvas.drawPath method. We build them by calling a sequence of methods to add new components. Key methods include but are not limited to:

Here’s a super-simple example Path defined inside a custom view:

final Path path = new Path();

path.moveTo(0, getHeight());

path.lineTo(getWidth(), 0);

This Path consists of a single straight line that starts at the bottom left corner of the view and ends at the top right corner of the view:

Here’s a more complex Path that consists of multiple components:

final Path path = new Path();

path.addCircle(
      getWidth() / 2,
      getHeight() / 2,
      2 * getWidth() / 7,
      Direction.CW
);

path.addCircle(
      2 * getWidth() / 5,
      3 * getHeight() / 7,
      3 * pathPaint.getStrokeWidth(),
      Direction.CW
);

path.addCircle(
      3 * getWidth() / 5,
      3 * getHeight() / 7,
      3 * pathPaint.getStrokeWidth(),
      Direction.CW
);

path.addArc(
      new RectF(
              getWidth() / 2 - 2 * getWidth() / 7,
              getHeight() / 2 - 3 * getWidth() / 7,
              getWidth() / 2 + 2 * getWidth() / 7,
              getHeight() / 2 + getWidth() / 7
      ),
      45,
      90
);

and the corresponding output:

Hopefully it’s clear that we can make super-general graphics using Path. However, this also means that we shouldn’t expect to be able to convert every Path into a valid interpolator. To figure out the appropriate constraints, let’s take a peek at some Android source code.

Interpolators ↔ Functions

The (abbreviated) source code for the TimeInterpolator interface below explains that an Android interpolator is nothing more than a function mapping the closed interval $[0,1]$ to the real numbers $\mathbb{R}$:

public interface TimeInterpolator {
    /**
     * [...]
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0
     *        represents the end
     * @return The interpolation value. This value can be more than 1.0
     *         for interpolators which overshoot their targets, or less
     *         than 0 for interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

Armed with this context, the wordy restrictions placed on Paths passed as arguments to the PathInterpolator(Path path) and PathInterpolatorCompat(Path path) constructors become a little less mysterious - we just have to make sure our Path corresponds to the graph of some function $f$ satisfying $f(0) = 0$ and $f(1) = 1$.1

Function Representations

In general, a function $g(x)$ can be represented in one of three ways:

\[g(x) = x^2\]
$x$ $g(x)$
$0$ $0$
$\frac{1}{4}$ $\frac{1}{16}$
$\frac{1}{2}$ $\frac{1}{4}$
$\frac{3}{4}$ $\frac{9}{16}$
$1$ $1$

Prior to the release of Lollipop, all stock and most custom interpolators were defined algebraically. Framework examples include the ubiqituous AccelerateInterpolator:

public float getInterpolation(float input) {
    if (mFactor == 1.0f) {
        return input * input;
    } else {
        return (float)Math.pow(input, mDoubleFactor);
    }
}

and the quirky OvershootInterpolator2:

public float getInterpolation(float t) {
   t -= 1.0f;
   return t * t * ((mTension + 1) * t + mTension) + 1.0f;
}

There are a couple of notable advantages to algebraic representations of interpolators:

However, there are also some significant drawbacks:

PathInterpolatorCompat addresses the limitations above by allowing us first to design our interpolator graphically (an intuitive method, since most interpolators are used to generate animations), and then to represent this interpolator in code using the “natural language” methods provided by the Path class.

Using PathInterpolatorCompat

Great; we’ve figured out why PathInterpolatorCompat exists and which Paths we can convert into interpolators. Let’s give it a spin.

Our aim will be to construct a zig-zag interpolator whose interpolated value bounces between 0 and 1 $n$ times (where $n$ is odd). Here’s a graph that represents this zig-zag interpolator with $n=5$:

I’m not saying this is the most useful interpolator ever; I designed it to convince you that there exist interpolators that are more naturally represented by composite paths than by a single algebraic expression.

Here’s a Path-based representation of the class of interpolators described above:

final Path path = new Path();
final double n = 5;

for (int i = 1; i <= n; i++) {
    path.lineTo(i / n, i % 2);
}

final TimeInterpolator result = new PathInterpolatorCompat(path);

I like this. It’s short and fairly readable.

Imagine trying to create this same interpolator by explicitly implementing getInterpolation for a general odd $n$. I’d wager that (a) computing the appropriate expression(s) would take you a while, and (b) the resulting algebraic representation could either be compact, or readable, but not both.

What Next?

Go forth and explore Path-based interpolators. Hopefully this introduction has given you some inspiration. There are definitely many areas to investigate still, including:

Further Reading

The Android framework has some interesting internal interpolators. For a more complex algebraic interpolator based on fluid physics, check out ViscousFluidInterpolator, an inner class of android.widget.Scroller used to animate flings.

  1. Path-based interpolators are therefore incapable of generating repeating animations. 

  2. The constant name mTension suggests that this motion may be related to the oscillations of an underdamped spring.