Animation / Bitmap / Canvas / ImageView / RenderScript

Blurring Images – Part 7

In the previous article we performed some simple optimisations which enormously improved the frame rate of our animations. However, we mentioned that increasing the size of the area that we wish to blur will slow the frame rate because the complexity of the blur operation will increase exponentially. In this article we’ll look at an alternative approach which should permit us to animate larger areas.

The basic approach that we’re going to take is very similar the general pattern that we’ve been using throughout this series. Perform the heavy processing operations outside of the critical section (i.e. onDraw()). In the previous article we moved all of the object allocation out of onDraw() and saw a fairly impressive improvement in terms of the frame rate that we were able to achieve. However, we can improve this further if we are able to move the blur operation itself (and the marshalling to and from the RenderScript memory space). But how can we do that when the way that Android animation framework adapts the frame rate to the hardware speed makes it all but impossible to predict the what the individual frames of the animation will be? The solution that we’ll try is to blur the entire image once, and for each frame we’ll perform our cookie cutter (i.e. copying only the area which corresponds to the bounds of our custom TextView) for each frame.

It is worth mentioning that this will improve performance, but will also be much more costly in terms of memory usage because we’ll hold two copies of the entire background image rather than the one that we held previously. Also, the initial blur operation will be much more expensive processing-wise because it is a much larger image that we are performing the blur on, and marshalling backwards and forwards.

So, let’s take a look at the code for our new version of BlurredTextView:

public class BlurredTextView extends TextView {
	private long mFrameCount = 0;

	private Bitmap mOverlay = null;
	private Canvas mOverlayCanvas = null;
	private Bitmap mBlurredBackground = null;

	public BlurredTextView(Context context) {
		this(context, null, -1);
	}

	public BlurredTextView(Context context, 
		AttributeSet attrs) {
		super(context, attrs, -1);
	}

	public BlurredTextView(Context context, 
		AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		if (mBlurredBackground != null) {
			mFrameCount++;
			mOverlayCanvas.drawBitmap(mBlurredBackground, 
				-getLeft() - getTranslationX(), 
				-getTop() - getTranslationY(), null);
			canvas.drawBitmap(mOverlay, 0, 0, null);
		}
		super.onDraw(canvas);
	}

	public long getFrameCount() {
		return mFrameCount;
	}

	public void setFrameCount(long frameCount) {
		this.mFrameCount = frameCount;
	}

	public void initBlur(ImageView bgd, float radius) {
		Bitmap background = null;
		if (bgd != null && bgd.getDrawable() != null) {
			Drawable drawable = bgd.getDrawable();
			if (drawable != null && 
				drawable instanceof BitmapDrawable) {
				background = 
					((BitmapDrawable) drawable).getBitmap();
				if (background.isRecycled()) {
					background = null;
				}
			}
		}
		if(background != null) {
			TimingLogger tl = new TimingLogger(
				MainActivity.TAG, "Full Blur");
			RenderScript rs = RenderScript.create(
				getContext());
			tl.addSplit("RenderScript.create()");
			mBlurredBackground = Bitmap.createBitmap(
				background.getWidth(), background.getHeight(), 
				background.getConfig());
			tl.addSplit("Bitmap.createBitmap()");
			Canvas canvas = new Canvas(mBlurredBackground);
			tl.addSplit("new Canvas()");
			canvas.drawBitmap(background, 0, 0, null);
			tl.addSplit("canvas.drawBitmap()");
			Allocation alloc = Allocation.createFromBitmap(
				rs, mBlurredBackground);
			tl.addSplit("Allocation.createFromBitmap()");
			ScriptIntrinsicBlur blur = 
				ScriptIntrinsicBlur.create(
					rs, alloc.getElement());
			tl.addSplit("ScriptIntrinsicBlur.create()");
			blur.setInput(alloc);
			tl.addSplit("blur.setInput()");
			blur.setRadius(radius);
			tl.addSplit("blur.setRadius()");
			blur.forEach(alloc);
			tl.addSplit("blur.forEach()");
			alloc.copyTo(mBlurredBackground);
			tl.addSplit("alloc.copyTo");
			rs.destroy();
			tl.addSplit("rs.destroy()");
			tl.dumpToLog();
		}
		resize();
	}

	private void resize() {
		if (mBlurredBackground != null) {
			mOverlay = Bitmap.createBitmap(getMeasuredWidth(), 
				getMeasuredHeight(), Bitmap.Config.ARGB_8888);
			mOverlayCanvas = new Canvas(mOverlay);
		}
	}

	public void cleanupBlur() {
	}

	@Override
	protected void onSizeChanged(int w, int h, 
		int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		if (w != oldw || h != oldh) {
			resize();
		}
	}
}

The actual blur is being performed within the initBlur() method, and our onDraw() is now much simpler. Our cleanupBlur() method is no longer needed, but we’ll leave an empty method body to avoid any changes to MainActivity.

If we run this, the animation is pretty smooth, but there is a noticeable delay before the animation begins:

If we look at the TimingLogger output from the blur operation we can see that it is taking almost half a second to perform:

Full Blur: begin
Full Blur:      27 ms, RenderScript.create()
Full Blur:      0 ms, Bitmap.createBitmap()
Full Blur:      0 ms, new Canvas()
Full Blur:      26 ms, canvas.drawBitmap()
Full Blur:      52 ms, Allocation.createFromBitmap()
Full Blur:      1 ms, ScriptIntrinsicBlur.create()
Full Blur:      0 ms, blur.setInput()
Full Blur:      0 ms, blur.setRadius()
Full Blur:      24 ms, blur.forEach()
Full Blur:      303 ms, alloc.copyTo
Full Blur:      9 ms, rs.destroy()
Full Blur: end, 443 ms

The blur operation itself is actually really fast, but it is the marshalling of the much larger bitmap which is really taking the time. This only goes to reinforce the earlier assertion that things would slow down considerably as the size of the bitmap increased. However, this is now being performed once, and the operation that we now have to perform for each frame being drawn is much lighter.

Checking the frame rate, we can see that we now get almost 60fps:

Elapsed time 6.1 sec, local frames 1086, frames 362, 59.5 fps

While the frame rate will still be affected of the size of the TextView increases, this approach will still perform much better because the operation that we are doing is one of the steps required when we do the blurring on a frame-by-frame basis – we’re simply moving the blur out of that sequence.

It is also worth noting that in our example, the blur operation is performed when we start the animation, so we experience a delay. We could improve this by instead performing the blur when we detect a change to the background image, and we would then have the blurred image ready when we start the animation.

That concludes this series on blurring images. We have looked at a number of different techniques but, possibly more importantly, we have also covered some techniques for identifying and resolving bottlenecks to optimise performance particularly when performing animations.

The source code for this article is available here.

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.