Android Recipe #2, fun with shaders

In this second recipe I will show you how to implement something that won’t be commonly used but fun nonetheless. It will however teach you how to implement advanced visual effects using Android’s animation engine and a very simple drawing trick. The picture below shows the effect: an animated spotlight slowly reveals an arbitrarily complex set of views (in this example, an image and two labels.) This effect can be used for splash screens (even though I dislike those on mobile) or highlight particular sections of your apps; for a tutorial for instance.

To follow along you can download the source code and a binary to run on your device. Note that I have only tested this demo on Nexus 7 and Nexus 10. You can replay the animation by changing the orientation of your device. You will be able to alternate between two different photos and see how the code adapts to both orientations.

Spotlight animation

If you cannot (or don’t want to) run the demo application you can see what the effect looks like in motion in this video. I captured this video in the Android emulator and the frame rate of my capture program was capped at 30fps so it doesn’t look as nice as it does on a device:

This effect is implemented in the same way as the previous recipe, by using a BitmapShader The difference is that instead of using a vector shape (a rounded rectangle), I am using here a bitmap mask. Bitmaps are the only primitive with which you cannot use shaders unless the bitmap’s configuration is Bitmap.Config.ALPHA_8. An alpha bitmap contains only an alpha channel and can therefore be used as a shape or mask. The shader set on the paint is used to fill the inside of the mask.

The first step is to load your bitmap as an alpha mask. In our case, the source bitmap is a blurry black circle. Here is how it’s done:

private static Bitmap convertToAlphaMask(Bitmap b) {
    Bitmap a = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ALPHA_8);
    Canvas c = new Canvas(a);
    c.drawBitmap(b, 0.0f, 0.0f, null);
    return a;
}

Bitmap mask;
mask = convertToAlphaMask(BitmapFactory.decodeResource(getResources(), R.drawable.spot_mask));

You can of course draw anything you want in the ALPHA_8 bitmap. It does not have to be another bitmap, but it’s usually unnecessary to go through this intermediate step with other shapes.

Once your mask is loaded you must prepare a paint with a shader. The code should look familiar if you’ve followed the previous recipe:

private static Shader createShader(Bitmap b) {
    return new BitmapShader(b, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
}

Shader targetShader = createShader(mTargetBitmap);
paint.setShader(targetShader);

And then all you need to do is draw the mask with the shader:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mMask, 0.0f, 0.0f, mPaint);
}

Note that alpha bitmaps only work with shaders when invoking the “simple” version of drawBitmap(). The variants that take a Matrix or a source and destination rectangle are not supported with hardware acceleration at the moment.

The code in the demo is more complicated since it knows how to handle a scale factor for the mask. If you don’t need to change the size of the mask you can automatically get the spotlight effect by moving the mask on screen. BitmapShaders have this special property that their origin is not tied to the shape you are drawing but to the canvas itself. Thus by moving a shape over the canvas you can reveal different parts of the bitmap.

I highly recommend you read the entire source code of the demo to see how I used ObjectAnimator to animate the spotlight and how I used ViewTreeObserver to wait until the activity is laid out to capture a bitmap of the views under the spotlight layer.

This effect can be implemented in a different way. The SpotlightView could draw a black rectangle covering the whole screen and draw the bitmap mask using the PorterDuff.Mode.CLEAR blending mode. This would work only by drawing into an intermediate ARGB_8888 bitmap though and the overdraw cost would be much higher. The implementation above is slightly more complicated because we need to compute a local matrix for the shader but it’s very efficient.

12 Responses to “Android Recipe #2, fun with shaders”

  1. Very cool. I guess, with Jake Wharton’s NineOldAndroids, this can work all the way down to 2.1 at least.

    https://github.com/JakeWharton/NineOldAndroids

  2. Luke Sleeman says:

    Heh, perhaps I’m getting old, but I remember when you did something similar way back in the day, with swing.

    Ahh, after a bit of searching through the archives, I think its this: http://www.curious-creature.org/2005/02/17/search-with-style-in-swing/
    Pity all the images are broken.

  3. Romain Guy says:

    The Swing version was implemented in a very different way though :)

  4. Levi Notik says:

    @zsolt not until Jake Wharton’s ports the withLayer() stuff added in API 16

  5. Romain Guy says:

    Levi, you can just call withLayer() only when API level is >= 16.

  6. Peter says:

    Is possible create this effect over surfaceview? I wanna do this when movie starts playing, I know I can use your solution, but I will need to extract first frame of movie into bitmap and set this frame to mTargetBitmap in SpotlightView, but is there any ViewShader API?

  7. Miguel says:

    The example runs with NineOldAndroids in, at least, Android 2.3.6 (I haven’t tested previous versions). Add nineoldandroids-2.4.0.jar to the project libs, and change one line of SpotlightActivitythis:

    spotlight.animate().alpha(1.0f).withLayer().withEndAction(new Runnable() { ……

    by this

    animate(spotlight).alpha(1.0f);
    this.runOnUiThread(new Runnable() { …..

    Change the imports to use the NineOldAndroids ones, and it works!

    import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
    import com.nineoldandroids.animation.Animator;
    import com.nineoldandroids.animation.Animator.AnimatorListener;
    import com.nineoldandroids.animation.AnimatorSet;
    import com.nineoldandroids.animation.ObjectAnimator;

  8. Max says:

    Does the convertToAlphaMask method above, provide any benefits or differences from bitmap.extractAlpha()?

  9. ishtek says:

    Hi,

    Thank you so much for this knowledge shared!!

  10. duanjin says:

    Very nice effect, thank you for knowledge sharing. Looking forward to more shares :)