There is plenty of information on how to quickly blur an image on Android.
But is it possible to do this so effectively that without lags redraw a blurred bitmap for any content change, as it is implemented in iOS?
So, what I would like to do:
- ViewGroup, which can blur the content that is under it and display as its background. Let's call it BlurView
- Ability to add children to it (any View)
- Do not depend on any particular implementation of the parent ViewGroup.
- Redraw blurred background if the content under our View changes
- Do a full cycle of blurring and rendering in less than 16ms
- Have the ability to change the blur strategy
In the article I will try to pay attention only to the key points, for details I ask
here .
gif with an example (8mb) How to get a bitmap with content under BlurView?
Create a Canvas, a Bitmap for it, and let the entire View hierarchy be drawn on this canvas.
internalBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); internalCanvas = new Canvas(internalBitmap); ... rootView.draw(internalCanvas);
Now in the internalBitmap all views are drawn, which are visible on the screen.
')
Problems:If your rootView does not have a specified background, you must first draw a background Activity on the canvas, otherwise it will be transparent on the bitmap.
final View decorView = getWindow().getDecorView();
I would like to draw only that part of the hierarchy that we need. That is, under our BlurView, with the appropriate size.
In addition, it would be nice to reduce the Bitmap on which the drawing will take place. This will speed up the rendering of the View hierarchy, blur itself, as well as reduce memory consumption.
Add a scaleFactor field, it is desirable that this coefficient be a power of two.
float scaleFactor = 8;
Reduce the bitmap by 8 times and adjust the canvas matrix so that it draws correctly on it, taking into account position, size and scale factor:
private void setupInternalCanvasMatrix() { float scaledLeftPosition = -blurView.getLeft() / scaleFactor; float scaledTopPosition = -blurView.getTop() / scaleFactor; float scaledTranslationX = blurView.getTranslationX() / scaleFactor; float scaledTranslationY = blurView.getTranslationY() / scaleFactor; internalCanvas.translate(scaledLeftPosition - scaledTranslationX, scaledTopPosition - scaledTranslationY); float scaleX = blurView.getScaleX() / scaleFactor; float scaleY = blurView.getScaleY() / scaleFactor; internalCanvas.scale(scaleX, scaleY); }
Now, when calling rootView.draw (internalCanvas), we will have a smaller copy of the entire View hierarchy on the internalBitmap. It will look scary, but it does not really bother me, because I will continue to blur it anyway.
Problems:rootView will tell all its children to draw on the canvas, including BlurView. I would like to avoid this; BlurView should not blur itself. To do this, you can override the draw (Canvas canvas) method in BlurView and make a check on what canvas it is asked to draw.
If this is a system canvas, we allow rendering, if it is internalCanvas, then disable it.
Actually, blur
I made the interface BlurAlgorithm and the ability to customize the algorithm.
Added 2 implementations, StackBlur (by Mario Klingemann) and RenderScriptBlur. The RenderScript option runs on the GPU.
To save memory, I write the result in the same bitmap that I came to the input. This is optional.
You can also try to cache outAllocation and reuse it if the size of the input image has not changed.
RenderScriptBlur @Override public final Bitmap blur(Bitmap bitmap, float blurRadius) { Allocation inAllocation = Allocation.createFromBitmap(renderScript, bitmap); Bitmap outputBitmap; if (canModifyBitmap) { outputBitmap = bitmap; } else { outputBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); }
When to redraw blur?
There are several options:
- Manually make an update when you know for sure that the content has changed
- Join scrolling events if BlurView is above a scrolling container.
- Play draw () calls via ViewTreeObserver
I chose option 3 using OnPreDrawListener. It is worth noting that his onPreDraw method twitches every time any View in the hierarchy draws itself. This, of course, is not ideal, because BlurView will have to be redrawn for every sneeze, but this does not require any control by the programmer.
Problems:onPreDraw will also be called when the entire hierarchy is drawn on the internalCanvas, as well as when the BlurView is drawn.
To avoid this problem, I started the isMeDrawingNow flag.
It seems everything is obvious, except for one moment. You need to set it to false via handler, because The draw dispatch occurs asynchronously through the UI thread of the thread. Therefore, you also need to add this task to the event queue so that it runs exactly at the end of the drawing.
drawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (!isMeDrawingNow) { updateBlur(); } return true; } }; rootView.getViewTreeObserver().addOnPreDrawListener(drawListener);
Performance
I haven't collected a lot of statistics, but on Nexus 4, Nexus 5, the whole drawing cycle takes 1-4ms on the screens from the demo project.
Galaxy nexus - about 10ms.
Sony Xperia Z3 compact for some reason copes worse - 8-16ms.
Nevertheless, I am satisfied with the performance, the FPS is kept at a good level.
Conclusion
All code is on githab (library and demo project) -
BlurView .
Ideas, comments, bug reports and pull requests are welcome.