Android Recipe #4, path tracing

I left the Android team a couple of months ago but there are still many visual effects and demos I would like to write for that platform. Since I am currently on vacation I was able to find some spare time to write a new demo that I hope will teach you a couple of new development tricks.

This new demo is called Road Trip USA and demonstrates how to implement path tracing. The video below shows what this effect looks like:

Path tracing is used in several places in the application: to draw the map of the USA, to draw the outline of each state, to draw the name of the states and to draw the drag indicator when loading is complete. The progress indicator briefly shown a the beginning (with moving chevrons) is implemented using a technique very similar to path tracing.

I recommend you download the source code (GitHub) and debug APK for the demo to better understand the explanations below.

Path effects

The path tracing effect relies on a little known Android API called PathEffect. A PathEffect can be applied to a Canvas.drawPath() command by setting it on a Paint. There are four main types of path effects:

  • CornerPathEffect can be used to round sharp corners (turns a series of connected lines into a curve for instance)
  • DashPathEffect strokes the path using a series of dashes
  • DiscretePathEffect divides the path into a series of randomly positioned segments
  • PathDashPathEffect strokes the path using another path as a stamp

These effects can be combined by using either a ComposePathEffect or a SumPathEffect. For instance, a series of connected lines can be transformed into a dashed curve by using a ComposeEffect containing a CornerPathEffect and a DashPathEffect:

Path effects

The demo shown earlier uses only two of these effects: DashPathEffect and PathDashPathEffect.

Phase out

The progress indicator is implemented using a PathDashPathEffect and a concave arrow as the stamp. Since the PathEffect API does all the work for us, the hardest part is to create the paths:

private Path makeConvexArrow(float length, float height) {
    Path p = new Path();
    p.moveTo(0.0f, -height / 2.0f);
    p.lineTo(length - height / 4.0f, -height / 2.0f);
    p.lineTo(length, 0.0f);
    p.lineTo(length - height / 4.0f, height / 2.0f);
    p.lineTo(0.0f, height / 2.0f);
    p.lineTo(0.0f + height / 4.0f, 0.0f);
    p.close();
    return p;
}

// Create a straight line
Path path = new Path();
path.moveTo(32, 32);
path.lineTo(232, 32);

// Stamp a concave arrow along the line
PathEffect effect = new PathDashPathEffect(
    makeConvexArrow(24.0f, 14.0f),    // "stamp"
    36.0f,                            // advance, or distance between two stamps
    0.0f,                             // phase, or offset before the first stamp
    PathDashPathEffect.Style.ROTATE); // how to transform each stamp

// Apply the effect and draw the path
paint.setPathEffect(effect);
canvas.drawPath(path, paint);

The parameter we are going to use to achieve the progress animation is the phase parameter. This value can be used to define the offset before the first stamp. By animating this value we can effectively move our arrows along the path. The picture below shows the same path drawn with different phases:

Phase out

Path tracing

The tracing animations are implemented using the phase parameter of DashPathEffect instead of PathDashPathEffect. The DashPathEffect constructor lets you specify the length of the dashes as well as the amount of space between the dashes. The trick is to make the dashes and spaces as long as the path itself.

Computing the length of a line is very easy but to measure the length of an arbitrary path is a little more complicated since you need to measure the length of quadratic and cubic curves. Android thankfully provides an easy API to do this for us:

Path path = makePath();

// Measure the path
PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();

// Apply the dash effect
PathEffect effect = new DashPathEffect(new float[] { length, length }, 0.0f);
paint.setPathEffect(effect);

canvas.drawPath(path, paint);

If you run the example above you should see the path drawn exactly as when no PathEffect is applied. This happens because a DashPathEffect always draws a dash first. To create the tracing effect we need to start with a phase equal to the length of the path:

PathEffect effect = new DashPathEffect(new float[] { length, length }, length);

The full effect can now be realized by animating the phase value between 0.0f and length. You can find the full implementation of this animation in the file StateView.java.

Tracing an arrow

The drag indicator, drawn as an arrow, is a little trickier to implement. Instead of using one path effect we need to use two: a DashPathEffect to fill the arrow trail and a PathDashPathEffect to draw the tip. The advance on the PathDashPathEffect is set to the length of the path to move the tip to the end of the path.

The full implementation can be found in IntroView.java.

Using SVG paths

I used SVG files to store the paths in this demo. While convenient, this format proved to be slow to load, at least with the library I am using, and I would probably switch to a custom binary format if I were to use this effect in a real application. Considering that the effect only needs the path geometry and no styling information, such a format would be simple and fast to parse.

The library I picked, androidsvg, is easy to use but does not give access to the paths contained in the SVG document. I worked around this issue by creating a custom Canvas that intercepts drawPath() calls:

Canvas canvas = new Canvas() {
    private final Matrix mMatrix = new Matrix();

    @Override
    public void drawPath(Path path, Paint paint) {
        Path dst = new Path();

        // Get the current transform matrix
        getMatrix(mMatrix);
        // Apply the matrix to the path
        path.transform(mMatrix, dst);
        // Store the transformed path
        mPaths.add(new SvgPath(dst, new Paint(mSourcePaint)));
    }
};

// Load an SVG document
SVG svg = SVG.getFromResource(context, R.raw.map_usa);
// Capture the paths
svg.renderToCanvas(canvas);

The paths are stored pre-transformed so we can measure their length at their final on-screen size. A slightly more advanced version of this code can be found in SvgHelper.java. The implementation used in the demo applies scaling to ensure the paths fill their containers properly.

Other visual effects

If you look closely at the demo you’ll notice several other visual effects:

  • Black & white to color conversion, used when scrolling a row of image to the left (the first image starts in B&W and turns into colors)
  • Pinned scrolling, used to create a “stacked cards” effect when scrolling up and down the list
  • Parallax scrolling, used to scroll the various maps at a different speed than other items
  • Animated action bar opacity, as seen in Google Music

The implementation of all these effects can be found in MainActivity.java.

24 Responses to “Android Recipe #4, path tracing”

  1. Jordan says:

    Awesome demo and post. Thanks for writing this.

  2. Christian says:

    Are there any tutorials on how to create a similar scrolling view like you did, Romain?

  3. Romain Guy says:

    It’s only ScrollViews and HorizontalScrollViews.

  4. liuyi says:

    Calling this method “mSvg.renderToCanvas(canvas);” will cause the error “libc Fatal signal 11(SIGSEGV) at 0X00000000 (code=1)”
    I can’t figure out what is causing it.
    Any help will be much appreciated!
    Android Version:4.0.3

  5. liuyi says:

    I use “mSvg.renderToCanvas(canvas, viewBox);” instead of “mSvg.renderToCanvas(canvas)” to solve the problem. Thank you!

  6. Tomek says:

    A little bug:
    10412:E/AndroidRuntime(29182): java.lang.RuntimeException: Unable to start activity ComponentInfo{org.curiouscreature.android.roadtrip/org.curiouscreature.android.roadtrip.MainActivity}: java.lang.ClassCastException: android.widget.HorizontalScrollView$SavedState cannot be cast to android.widget.ScrollView$SavedState

  7. Romain Guy says:

    It’s easy to fix, I gave two Views the same id. No big deal :)

  8. heatork says:

    native crash :
    Fatal signal 11(SIGSEGV) at 0×00000000 (code=1),thread 1715(SVG Loader)

  9. heatork says:

    i have figured out that the native crash occurs because of you did create a new canvas in a non-ui thread:
    SvgHelper#getPathsForViewport

    and called mSvg.renderToCanvas(canvas) later, the crash occurs while the svg library calls canvas.getWith() and canvas.getHeight() in the SVG#renderToCanvas(Canvas canvas, RectF viewPort) method.

    ——————–
    how to fix it:
    instead of calling the renderToCanvas(Canvas canvas), we should create a new RectF object, and call renderToCanvas(Canvas canvas, RectF rect), like this: in the
    SvgHelper#getPathsForViewport,
    RectF viewPort = new RectF(0f, 0f, width, height);
    mSvg,rebderToCanvas(canvas, viewPort);

    everything goes well now :)

  10. Romain Guy says:

    Canvases can be created on non-UI threads. The problem is not threading, the problem is that the Canvas I create is not backed by a Bitmap so internally getWidth()/getHeight() crash on a null pointer. Note that this doesn’t occur on 4.4 so obviously this issue has been fixed in the platform.

  11. 7heaven says:

    awesome job. I was wondering if it’s possible to get any character’s edge and put in into a Path as joints in android.

    I notice that when I use paint.setStyle(Paint.Style.STROKE) and canvas.drawText, it actually draw the edge of the characters,canvas did get the character’s edge and draw.so it seems possible to do that,I just don’t know how, can you help me with that?

  12. Romain Guy says:

    You can use the API called Paint.getTextPath() to get a Path that represents the outline of a series of glyphs.

  13. Chris says:

    Are you the very same Romain Guy who took the picture that comes up on my Chromecast? :)

  14. norman says:

    thanks for the demos – it’s a pleasure to learn from “the source” :)

  15. Chris says:

    Awesome! Cool pics man, also great post.

  16. Cool tricks thanks!

    I have a question. In the “Path tracing” section, you talk about animating the phase value between 0.0f and path length but we must create a new DashPathEffect instance on each phase update.

    I check in StateView.java and a new DashPathEffect instance is created on each setPhase call. Is it the good way to do that?

  17. narendra says:

    Hi romain i really enjoy ur tuts they are simply best can u pliz write some tuts on 3d effect using camera, matrix or property animator i m eagerly waiting for ur rply

  18. momo says:

    Hi Romain
    Can you show us a way to screencasting without rooting.Have you any ideas how to use the cameras to focus on the canvas/bitmap/picture/surface/surfaceholder.What about canvas.save in picture.This is my challenge to you since you know more about the api.It will be appreciate if just you write an article about this problem and explain what are the problems to achieve this.
    Merci et je souhaite voir un post bientot.

  19. Rafa says:

    Hi! The article and sample code are awesome, thanks a lot.
    I’m wondering if it would be possible to fade in the filled content of the svg, I’ve been trying for a while with no success.

  20. Andrew says:

    Hello! Great Article.

    But when run a test application to draw with PathDashPathEffect the result is too small as was expected. But everything is ok, when I set setLayerType(LAYER_TYPE_SOFTWARE, null).
    The code is almost the same as in “Phase out” part of the article.

  21. Anton says:

    Hello!
    I have a question regarding Android Recipe #2 (fun with shaders)
    http://www.curious-creature.org/2012/12/13/android-recipe-2-fun-with-shaders/

    Since the comments are disabled, I had no choice but to ask about it here.

    I am trying to create something like this: https://geeksretreat.files.wordpress.com/2012/04/html5-canvas-speedometer.png
    But I can’t understand how to make a radial gradient (I am new to graphics API in android). Is there a simple and effective way to draw an arc with gradient edge?

    Thank you in advance and sorry for unrelated comment.

  22. Elia says:

    Wow, great tutorial! I appreciate your design and your simple explanation! Thanks.

  23. shanwu says:

    Thanks for sharing this article~