Animation / Bitmap / Canvas / RenderScript

Blurring Images – Part 6

In the previous article we looked at frame rates, and explored how we can measure them by adding some simple benchmarking logging. In this article we’ll look at implementing our dynamic blurring so that we can animate things, and optimising things to improve our framerate.

The first thing that we’ll do is to simply add our RenderScript blur method to our onPreDraw() method to see how our existing implementation performs:

private OnPreDrawListener mPreDrawListener = 
	new OnPreDrawListener() {
	@Override
	public boolean onPreDraw() {
		Drawable drawable = mImage.getDrawable();
		if (drawable != null && 
			drawable instanceof BitmapDrawable) {
			Bitmap bitmap = 
				((BitmapDrawable) drawable)
				.getBitmap();
			if (bitmap != null) {
				blur(bitmap, mText, 25);
			}
		}
		mFrameCount++;
		return true;
	}
};

This gets called every time we draw. So how does it look?

That’s not looking great, so what about the frame rate?

Elapsed time 6.5 sec, local frames 224, frames 32, 4.9 fps

Less than 5 frames per second is pretty poor, and explains why it looks so bad. Hang on though – if we look at the local frames compared to the frames value, there’s a big discrepancy. Our custom TextView is only being drawn 32 times in 6.5 seconds to achieve that poor frame rate, yet our onPreDraw() method is being called 224 times. It is in our onPreDraw() method that we are actually performing the blur.

Hopefully it should be pretty obvious from this that it would be far more efficient to perform the blur operation in the onDraw() method of our custom control rather than our onPreDraw() method.

While we’re moving this code across, let’s also consider some other optimisations. We saw previously that setting up and tearing down our RenderScript context was a little heavy, so why not re-use the context for the duration of the animation? Also, the size of the Allocation that we created to hold the bitmap being blurred within the RenderScript memory space will not change unless the dimensions of the control change, so we can reuse this to prevent unnecessary object creation.

So our custom TextView now looks like this:

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

	private Bitmap mBackground = null;
	private Bitmap mOverlay = null;
	private Canvas mOverlayCanvas = null;
	private RenderScript mRs = null;
	private Allocation mOverlayAlloc = null;
	private ScriptIntrinsicBlur mBlur = null;

	private float mRadius = 25;

	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 (mRs != null) {
			mFrameCount++;
			mOverlayCanvas.drawBitmap(mBackground, 
				-getLeft() - getTranslationX(), 
				-getTop() - getTranslationY(), null);
			mOverlayAlloc.copyFrom(mOverlay);
			mBlur.setInput(mOverlayAlloc);
			mBlur.forEach(mOverlayAlloc);
			mOverlayAlloc.copyTo(mOverlay);
			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) {
		if (bgd != null && bgd.getDrawable() != null) {
			Drawable drawable = bgd.getDrawable();
			if (drawable != null && 
				drawable instanceof BitmapDrawable) {
				mBackground = ((BitmapDrawable) drawable)
					.getBitmap();
				if (mBackground.isRecycled()) {
					mBackground = null;
				}
			}
		}
		mRs = RenderScript.create(getContext());
		mRadius = radius;
		resize();
	}

	private void resize() {
		if (mRs != null) {
			mOverlay = Bitmap.createBitmap(getMeasuredWidth(), 
				getMeasuredHeight(), Bitmap.Config.ARGB_8888);
			mOverlayCanvas = new Canvas(mOverlay);
			if(mOverlayAlloc != null) {
				mOverlayAlloc.destroy();
			}
			mOverlayAlloc = Allocation.createFromBitmap(mRs, mOverlay);
			if(mBlur != null) {
				mBlur.destroy();
			}
			mBlur = ScriptIntrinsicBlur.create(mRs, mOverlayAlloc.getElement());
			mBlur.setRadius(mRadius);
		}
	}

	public void cleanupBlur() {
		mBackground = null;
		mOverlay = null;
		mOverlayCanvas = null;
		mRs.destroy();
		mRs = null;
	}

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

We must initialise the blur operation by calling initBlur() and passing in a reference to the ImageView holding the background image that we wish to blur. This obtains a reference to the background image, sets up the RenderScript context and calls resize() to initialise Bitmap, Allocation, and ScriptIntrinsicBlur objects required to perform the actual blur. Any time the size of the custom view changes, resize will be called again to re-create these objects to the appropriate size.

The onDraw() method performs the blur operation, and simply draws the resultant bitmap before the rest of the view is drawn.

When we are finished animating we should call cleanupBlur() to tidy up, and deallocate the objects used for animating the individual frames.

Our MainActivity code just needs to change to call initBlur() when we start animating, invalidate() (to force onDraw()) in onPreDraw(), and cleanupBlur() when we stop animating:

private OnPreDrawListener mPreDrawListener = 
	new OnPreDrawListener() {
	@Override
	public boolean onPreDraw() {
		Drawable drawable = mImage.getDrawable();
		if (mAnimator != null && 
			drawable != null && 
			drawable instanceof BitmapDrawable) {
			Bitmap bitmap = ((BitmapDrawable) drawable)
				.getBitmap();
			if (bitmap != null) {
				mText.invalidate();
			}
		}
		mFrameCount++;
		return true;
	}
};

private void startAnimation() {
	mStart = System.currentTimeMillis();
	mText.setFrameCount(0);
	mText.initBlur(mImage, 25);
	mFrameCount = 0;
	mAnimator = ObjectAnimator.ofFloat(mText, 
		"translationY", 0, -200);
	mAnimator.setDuration(1000);
	mAnimator.setRepeatMode(ValueAnimator.REVERSE);
	mAnimator.setRepeatCount(ValueAnimator.INFINITE);
	mAnimator.start();
}

private void stopAnimation() {
	mText.cleanupBlur();
	mAnimator.cancel();
	float elapsed = (float) 
		(System.currentTimeMillis() - mStart) / 1000.0f;
	long framecount = mText.getFrameCount();
	float fps = framecount / elapsed;
	Log.d(TAG, getString(R.string.framerate, elapsed, 
		mFrameCount, framecount, fps));
	mAnimator = null;
}

If we run this the animation seems to be much smoother than before:

But what about the all important frame rate:

Elapsed time 6.2 sec, local frames 966, frames 322, 51.9 fps

Over 50 fps is more than ten times better than we had before, and is a perfectly respectable frame rate.

Before we get carried away with this, there are a couple of quite major caveats:

  1. This is running on very high specification hardware (at the time of writing!). We will definitely not achieve this kind of frame rate across all devices.
  2. The custom TextView is relatively small, and therefore the number of pixels in the bitmap that we are blurring in each frame is also relatively small. As the size of the TextView increases, number of pixels that must be blurred will increase exponentially, which will reduce our frame rate.

At this point it would appear that we have very much achieved what we set out to in blurring a part of an image, and managing to achieve a highly respectable frame rate when performing this blur on a frame by frame basis as part of an animation. However, there is an alternative approach that we can use to achieve this high frame rate, and in the concluding article in this series we’ll take a look at it.

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.