Dynamic blurred app background

My team at GG Network added a very nice effect to Open FM – an on-line radio player that we rebuilt from the ground in the second half of 2014. We used a blurred version of a content image as the application background. I want to share with you the steps we’ve taken to implement this.

It seemed at first like quite a task. Android doesn’t use blur anywhere in the system UI (at least not since Froyo), neither do any of the AOSP or Google apps, so it seemed there will be no example code to rely on. In the end it wasn’t that hard – there may not be a blur() function in any of the framework classes, but what’s available is almost as straightforward.

Blurring an image seems like the most important part of the job. However, there are already quite a few writeups on how to do that on Android. I’ll gloss over this part and instead describe the whole process to use this technique in practice to achieve the full effect. If you are only interested in blurring then you can learn all about it by following these links:

I will warn you that we use Picasso for image loading and caching in our applications. You will see it simplifies implementing this effect. If you don’t use Picasso then I would advise you to start using it. Otherwise, you’ll have to figure out how to adapt our code, but I’m pretty sure you’ll be able to.

Step 1: Change the application background with Picasso.

We started by setting the application background with Picasso. In our BaseActivity code we added a method looking something like this:

1
2
3
4
5
private void updateWindowBackground() {
    String url = getUrlToTheImage();
    Picasso.with(this).load(url)
            .error(R.drawable.background_default).into(this);
}

For this to work we needed to implement Target interface in our BaseActivity class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onBitmapFailed(Drawable drawable) {
    getWindow().setBackgroundDrawable(drawable);
}

@Override
public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
    getWindow().setBackgroundDrawable(
            new BitmapDrawable(getResources(), bitmap));
}

@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
    // just prepare mentally
}

Step 2: Plug in a custom Transformation

Picasso is great even if you want only the base use case. But it’s a powerful tool for custom effects too. It lets you apply Transformations while loading images, so we can do something like that:

1
2
Picasso.with(this).load(url).transform(new BlurTransformation())
        .error(R.drawable.background_default).into(this);

(Actually, this is an oversimplified example. You should reuse an instance of Transformation object, not create a new one every time you load a new image. You can see that in the final demo code.)

Picasso doesn’t come with a BlurTransformation class out of the box, so we needed to create one. Transformation is an interface with two methods you have to implement. So we started with something along those lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BlurTransformation implements Transformation {
    private Context mContext;
    private float mRadius;

    public BlurTransformation(Context context, float radius) {
        mContext = context;
        mRadius = radius;
    }

    @Override
    public Bitmap transform(Bitmap bitmap) {
        return null; // TODO: actually do some transforming
    }

    @Override
    public String key() {
        return "blur(" + String.valueOf(mRadius) + ")";
    }
}

It’s pretty straightforward. In the transform() method you get passed a Bitmap and you have to return a Bitmap. There are two important things to remember:

  • If you return a newly created Bitmap (as opposed to a mutated input Bitmap) from the transform() method then you have to call recycle() on the input Bitmap. You’ll see an example of that in the next step.
  • In the key() method you should take into account all the variables that influence how the resulting Bitmap looks. In this example we added the blur radius to the key. This way if you blur the same source Bitmap with different radii, Picasso can cache both of them and correctly return each one when appropriate.

Step 3: Blur that Bitmahp already!

If you look at the writeups and gists I’ve linked at the beginning then you’ll find code that is pretty much ready to just drop into the transform() method and this is what we just did:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Bitmap transform(Bitmap bitmap) {
    Bitmap blurredBitmap = Bitmap.createBitmap(bitmap.getWidth(),
            bitmap.getHeight(), Bitmap.Config.ARGB_8888);
    // Initialize RenderScript and the script to be used
    RenderScript renderScript = RenderScript.create(mContext);
    ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(renderScript,
    Element.U8_4(renderScript));
    // Allocate memory for Renderscript to work with
    Allocation input = Allocation.createFromBitmap(renderScript, bitmap);
    Allocation output = Allocation
            .createFromBitmap(renderScript, blurredBitmap);

    script.setInput(input);
    script.setRadius(mRadius);
    script.forEach(output);
    output.copyTo(blurredBitmap);

    renderScript.destroy();
    bitmap.recycle();
    return blurredBitmap;
}

That looks quite complete. But there are still some details and problems to iron out. First of them is that ScriptIntrinsicBlur accepts a maximum radius of 25. Try to pass it a bigger radius and it’ll plainly crash. However, if you decide you need a bigger blur there are some workarounds. We could think of two:

  • You could run ScriptIntrinsicBlur multiple times.
  • You could blur a downscaled version of a bitmap and then stretch it to the original size again. This would also stretch the final blur radius, but you would have to obviously watch for the quality loss.

But let’s move on to some more important issues.

Step 4: Make it run pre API 17

The above code works only starting from API 17 and that might be too high for most apps. Renderscript was available earlier, but ScriptIntrinsicBlur was only added in API 17. Fortunately there’s a support lib for that. Here’s an official blog post about it

Wait what? Eclipse? What about Android Studio and Gradle? Not a problem, fortunately there’s DSL for that! Just add this to your build.gradle:

1
2
3
4
5
defaultConfig {
    ...
    renderscriptTargetApi 21
    renderscriptSupportModeEnabled true
}

Now you can go back as far as API 8 if you want (or need) to! (If you didn’t read the linked blog post, for which I don’t blame you since it’s about Ehclipse, a little hint: you also need to change the imports.)

Step 5: Fight with the artifacts

That looks like we’re done (we aaaalmost are). It looks great on a test device, at least on a couple of test images. But when we ran it on a wide variety of images or on specific devices (like Samsung Galaxy S3), we started to see some nasty artifacts. It took us some precious time to find a solution to it, so I’m just going to save you this time and point you right to Roman Nurik’s G+ post that is a great help.

Thanks Roman! OK, so let’s just crop some pixels if our width is not a multiple of four:

1
2
3
4
5
int bitmapWidth = bitmap.getWidth();
if (bitmapWidth % 4 != 0) {
    bitmap = BitmapUtils.resize(bitmap, bitmapWidth - (bitmapWidth % 4),
            bitmap.getHeight(), ScaleType.CENTER);
}

For the actual resize I am using here our internal BitmapUtils class which does the heavy lifting (including recycling the original bitmap as it returns a cropped copy). To keep things simple I will not go into how exactly we do this. You probably already have a similar util in your code. If not there are probably many great resources on how to do that (AOSP code for the Gallery app could be one place to start).

Step 6: Release to prod, see it crash

OK, now we really were done. At least we thought so. We couldn’t find any more issues with this code in our internal testing. So we shipped it. And prod come back at us with crashes like it always does.

Fortunately we use Crashlytics and it helped us quickly find the problems. On some devices (judging by their names they were quite exotic ones) the input Bitmap had a config of Config.RGB_565. If you look back on the code from Step 3, you’ll see we created the output bitmap with Config.ARGB_8888. So when Renderscript tried to copy the blurred pixels to the output Bitmap it crashed because of this incompatibility.

So the obvious solution would be to create the output Bitmap with the same config as input one. However, if you tried it (and if you managed to find a device that produces these RGB_565 bitmaps) or if you just researched it, you would find that renderscript doesn’t play nice with this config. Unless you’re into results like in this SO thread.

We thought it’d be a great idea for a hallucinogens simulation app, but it didn’t fit into our music player. What we decided to do is to try to convert the input into ARGB_8888and if that fails we throw an exception inside the transformation. Picasso catches it and handles by calling the error callback. We’ve decided we much rather like to display some static default background, than those trippy images, so that worked for us.

Here’s the code we use for conversion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Bitmap convertBitmap(Bitmap bitmap, Config config) {
    if (bitmap.getConfig() == config) {
        return bitmap;
    } else {
        Bitmap argbBitmap;
        argbBitmap = bitmap.copy(config, false);
        bitmap.recycle();
        if (argbBitmap == null) {
            throw new UnsupportedOperationException(
                    "Couldn't convert bitmap from config " + bitmap
                            .getConfig() + " to " + config);
        }
        return argbBitmap;
    }
}

Which we call like that before finally passing it to the Renderscript:

1
Bitmap argbBitmap = convertBitmap(croppedBitmap, Config.ARGB_8888);

Bonus: Change the background smoothly

One last thing was added later by my fabulous team mates. It actually had nothing to do with blurring, but it was the +10% awesomeness touch, that made the 90% of the effect. There’s just one problem with gorgeous blurred images that cover user’s screen from edge to edge and are dynamically switched while the app is in use – changing one to another in an instant just doesn’t cut it. It just makes for a really jarring experience when your whole screen flashes from one background to another (and even worse on some devices that sometimes show a solid black background for a few frames).

The solution is obvious, we should smoothly animate from one background to another. TransitionDrawable comes to the rescue. There’s also the problem that Window doesn’t have a getter for current background drawable, but you can work around it by reaching deeper – into the DecorView. Here’s our code:

1
2
3
4
5
6
7
8
private void changeBackground(Drawable drawable) {
    View decorView = getWindow().getDecorView();
    Drawable oldBackgroundDrawable = decorView.getBackground();
    TransitionDrawable transitionDrawable = new TransitionDrawable(
            new Drawable[]{oldBackgroundDrawable, drawable});
    decorView.setBackground(transitionDrawable);
    transitionDrawable.startTransition(1000);
}

Check out the complete code of a working demo app.

Below a preview of the effect. And of course the best live demo you can see on your own phone is the Open FM app in the Play Store. Give it a try - download it and play some songs.

Dynamic blurred app background demo

Images for this preview and the demo code were taken from Marie Schweiz’s Lorem Ipsum Illustration. Thanks, Marie!

Comments