Cdacians

Cdacians
Cdacians

Thursday, 7 September 2017

Advanced blurring techniques

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:

final result

Prerequisites

Let me describe what I will be working with. I have 1 activity which hosts different fragments in a 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 register onPreDrawListener(). I need this because at the moment my applyBlur() 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 generated false return value to true. It is really important to understand that if you return false - the frame which is about to be drawn will be skipped. I am actually interested in the first frame, so I return true.
  • 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 my ImageView. I build drawing cache and retrieve it by calling getDrawingCache()
  • 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 my ImageView which is behind the TextView
  • 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:
Renderscript-blurred
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:
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:
Fast blur As we can see, quality of blur is pretty much the same.
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:
downscaling
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 with 2.
  • 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 with FILTER_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