Advanced blurring techniques
Today we will try to dig a bit deeper into blurring techniques available for Android developers. I read couple of articles and SO posts describing different ways to do this, so I want to summarize what I learned.
Why?
More and more developers now try to add different kinds of blurry backgrounds for their custom views. Take a look at awesome Muzei app by +RomanNurik or Yahoo Weather app. I really like what they did with the design there.
I was inspired to write this article by set of blog posts from here (by Mark Allison). So the first part of this post will be really similar to Mark's post. But I will try to go even further.
Basically what we will try to accomplish today is the following:
Prerequisites
Let me describe what I will be working with. I have 1 activity which hosts different fragments in a
Here is what my
ViewPager
. Every fragment represent 1 blurring technique.Here is what my
layout_main.xml
looks like:<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.paveldudka.MainActivity" />
And here is my
fragment_layout.xml
:<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/picture"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/text"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="My super text"
android:textColor="@android:color/white"
android:layout_gravity="center_vertical"
android:textStyle="bold"
android:textSize="48sp" />
<LinearLayout
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#7f000000"
android:orientation="vertical"
android:layout_gravity="bottom"/>
</FrameLayout>
As you can see this is just an
ImageView
with TextView
centered and some debug layout (@+id/controls
) I will use to display performance measurements and add some more tweaks.
The general blurring technique looks like:
- Cut that part of background which is behind my
TextView
- Blur it
- Set this blurred part as background to my
TextView
Renderscript
The most popular answer to questions like "how do I implement blur in Android" is - Renderscript. This is very powerful and optimized "engine" to work with graphics. I will not try to explain how it works under the hood (since I don't know either :) and this is definitely out of scope for this post).
public class RSBlurFragment extends Fragment {
private ImageView image;
private TextView text;
private TextView statusText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
image = (ImageView) view.findViewById(R.id.picture);
text = (TextView) view.findViewById(R.id.text);
statusText = addStatusText((ViewGroup) view.findViewById(R.id.controls));
applyBlur();
return view;
}
private void applyBlur() {
image.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
image.getViewTreeObserver().removeOnPreDrawListener(this);
image.buildDrawingCache();
Bitmap bmp = image.getDrawingCache();
blur(bmp, text);
return true;
}
});
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float radius = 20;
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()),
(int) (view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft(), -view.getTop());
canvas.drawBitmap(bkg, 0, 0, null);
RenderScript rs = RenderScript.create(getActivity());
Allocation overlayAlloc = Allocation.createFromBitmap(
rs, overlay);
ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(
rs, overlayAlloc.getElement());
blur.setInput(overlayAlloc);
blur.setRadius(radius);
blur.forEach(overlayAlloc);
overlayAlloc.copyTo(overlay);
view.setBackground(new BitmapDrawable(
getResources(), overlay));
rs.destroy();
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
@Override
public String toString() {
return "RenderScript";
}
private TextView addStatusText(ViewGroup container) {
TextView result = new TextView(getActivity());
result.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
result.setTextColor(0xFFFFFFFF);
container.addView(result);
return result;
}
}
- When fragment gets created - I inflate my layout, add
TextView
to my debug panel (I will use it to display blurring performance) and apply blur to the image - Inside
applyBlur()
I registeronPreDrawListener()
. I need this because at the moment myapplyBlur()
is called nothing is laid out yet, so there is nothing to blur. I need to wait until my layout is measured, laid out and is ready to be displayed. - In
onPreDraw()
callback first thing I usually do is change generatedfalse
return value totrue
. It is really important to understand that if you returnfalse
- the frame which is about to be drawn will be skipped. I am actually interested in the first frame, so I returntrue
. - Then I remove my callback because I don't want to listen to pre-draw events anymore.
- Now I want to get
Bitmap
out of myImageView
. I build drawing cache and retrieve it by callinggetDrawingCache()
- And eventually blur. Let's discuss this step more precisely.
I want to say here that I realize that my code doesn't cover couple of very important moments:
- It doesn't re-blur when layout changes. For this you need to register
onGlobalLayoutListener
and repeat blurring whenever layout changes - It does blurring in the main thread. Obviously is not the way you do it in production, but for the sake of simplicity, I will do that for now :)
So, let's go back to my
blur()
:- At first I create an empty bitmap to copy part of my background into. This bitmap I will blur later and set as a background to my
TextView
- Create
Canvas
backed up by this bitmap - Translate canvas to the position of my
TextView
within parent layout - Draw part of my
ImageView
to bitmap - At this point I have a bitmap equals to my
TextView
size and containing that part of myImageView
which is behind theTextView
- Create Renderscript instance
- Copy my bitmap to Renderscript-friendly piece of data
- Create Renderscript blur instance
- Set input, radius and apply blur
- Copy result back to my bitmap
- Great! Now we have blurred bitmap. Let's set it as a background to my
TextView
Here is what I got:
As we can see, result is pretty good and it took 57ms. Since one frame in Android should render no more than ~16ms (60fps) we can see that doing that on UI thread will drop our frame rate down to 17fps for the period of blurring. Obviously is not acceptable, so we need to offload this to
AsyncTask
or something similar.
Also it worth mentioning that
ScriptIntrinsicBlur
is available from API 17 only, but you can use renderscript support lib to lower required API a bit.
But still, a lot of us still have to support older APIs which don't have this fancy renderscript support. Let's find out what we can do here.
FastBlur
Since blur process is nothing more than just pixel manipulation, obvious solution would be to try do blurring manually. Luckily, there are plenty examples of Java implementation of blur. The only thing we need to do is to find relatively quick implementation.
Thanks to this post on SO, I picked fast blur implementation. Let's see what does it look like.
I will describe only blur function since the rest of the code is the same:
I will describe only blur function since the rest of the code is the same:
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float radius = 20;
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()),
(int) (view.getMeasuredHeight()), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft(), -view.getTop());
canvas.drawBitmap(bkg, 0, 0, null);
overlay = FastBlur.doBlur(overlay, (int)radius, true);
view.setBackground(new BitmapDrawable(getResources(), overlay));
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
And here is result:
As we can see, quality of blur is pretty much the same.
So, the benefit of using
But damn! It takes hell a lot of time! We spent 147ms doing blur! And this is far not the slowest SW blurring algorithm. I don't event want to try Gaussian blur...
So, the benefit of using
FastBlur
is that we eliminated renderscript dependency (and removed min API constraint).But damn! It takes hell a lot of time! We spent 147ms doing blur! And this is far not the slowest SW blurring algorithm. I don't event want to try Gaussian blur...
Going beyond
Now let's think what can we do better. Blurring process itself is all about "losing" pixels. You know what else is all about losing pixels? Right! Downscaling!
What if we try to downscale our bitmap first, do blur and then upscale it again. I tried to implement this technique and here is what I got:
What if we try to downscale our bitmap first, do blur and then upscale it again. I tried to implement this technique and here is what I got:
Well, look at that! 13ms for renderscript and 2ms for FastBlur. Not bad at all!
Let's look at the code. I describe only fastblur approach since it the same for renderscript. Full code you can check in my GitHub repo.
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
float scaleFactor = 1;
float radius = 20;
if (downScale.isChecked()) {
scaleFactor = 8;
radius = 2;
}
Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()/scaleFactor),
(int) (view.getMeasuredHeight()/scaleFactor), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
canvas.translate(-view.getLeft()/scaleFactor, -view.getTop()/scaleFactor);
canvas.scale(1 / scaleFactor, 1 / scaleFactor);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(bkg, 0, 0, paint);
overlay = FastBlur.doBlur(overlay, (int)radius, true);
view.setBackground(new BitmapDrawable(getResources(), overlay));
statusText.setText(System.currentTimeMillis() - startMs + "ms");
}
Let's go through the code:
scaleFactor
tells what level of downscale we want to apply. In my case I will downscale my bitmap to 1/8 of its original size. Also since my bitmap will be blurred by downscaling/upscaling process, I don't need that big radius for my blurring algo. I decided to go with2
.- Now I need to create bitmap. This bitmap will be 8 times smaller than I finally need for my background.
- Also please note that I provided
Paint
withFILTER_BITMAP_FLAG
. In this way I will get bilinear filtering applied to my bitmap during scaling. It will give me even smoother blurring. - As before, apply blur. In this case image is smaller and radius is lower, so blur is really fast.
- Set blurred image as a background. This will automatically upscale it back again.
It is interesting that fastblur did blurring even faster than renderscript. That's because we don't waste time copying our bitmap to
Allocation
and back.
With these simple manipulations I managed to get relatively fast blurring mechanism w/o renderscript dependency.
WARNING! Please note that FastBlur uses hell a lot of additional memory (it is copying entire bitmap into temp buffers), so even if it works perfect for small bitmaps, I would not recommend using it for blurring entire screen since you can easily get OutOfMemoryException on low-end devices. Use your best judjement
Source code for this article is available on GitHub
No comments:
Post a Comment