Информации о том как быстро размыть картинку на Android существует предостаточно.
Но можно ли сделать это настолько эффективно, чтобы без лагов перерисовывать размытый bitmap при любом изменении контента, как это реализовано в iOS?
Итак, что хотелось бы сделать:
В статье я постараюсь обратить внимание только на ключевые моменты, за деталями прошу сюда.
Создадим Canvas, Bitmap для него, и скажем всей View-иерархии нарисоваться на этом канвасе.
Теперь в internalBitmap нарисованы все View, которые видны на экране.
Проблемы:
Если у вашего rootView нет заданного фона, нужно предварительно нарисовать на канвасе фон Activity, иначе он будет прозрачным на битмапе.
Хотелось бы рисовать только ту часть иерархии, которая нам нужна. То есть под нашей BlurView, с соответствующим размером.
Кроме того, было бы неплохо уменьшить Bitmap, на котором будет происходить отрисовка. Это ускорит отрисовку View иерархии, сам blur, а так же уменьшит потребление памяти.
Добавляем поле scaleFactor, желательно, чтобы этот коэффициент был степенью двойки.
Уменьшаем bitmap в 8 раз и настраиваем матрицу канваса так, чтобы корректно на нем рисовать, учитывая позицию, размер и scale factor:
Теперь при вызове rootView.draw(internalCanvas), мы будем иметь на internalBitmap уменьшенную копию всей View-иерархии. Выглядеть она будет страшно, но это меня не очень волнует, т.к. дальше я все равно буду ее размывать.
Проблемы:
rootView скажет всем своим детям нарисоваться на канвасе, в том числе и BlurView. Этого хотелось бы избежать, BlurView не должна размывать сама себя. Для этого можно переопределить метод draw(Canvas canvas) в BlurView и делать проверку на каком канвасе ее просят отрисоваться.
Если это системный канвас — разрешаем отрисовку, если это internalCanvas — запрещаем.
Я сделал интерфейс BlurAlgorithm и возможность настройки алгоритма.
Добавил 2 реализации, StackBlur (by Mario Klingemann) и RenderScriptBlur. Вариант с RenderScript выполняется на GPU.
Чтобы сэкономить память, я записываю результат в тот же bitmap, который мне пришел на вход. Это опционально.
Так же можно попробовать кэшировать outAllocation и переиспользовать его, если размер входного изображения не изменился.
Есть несколько вариантов:
Я выбрал 3 вариант, используя OnPreDrawListener. Стоит заметить, что его метод onPreDraw дергается каждый раз, когда любая View в иерархии рисует себя. Это, конечно, не идеал, т.к. придется перерисовывать BlurView на каждый чих, но зато это не требует никакого контроля со стороны программиста.
Проблемы:
onPreDraw будет так же вызван, когда будет происходить отрисовка всей иерархии на internalCanvas, а так же при отрисовке BlurView.
Чтобы избежать этой проблемы, я завел флаг isMeDrawingNow.
Вроде все очевидно, кроме одного момента. Ставить его в false нужно через handler, т.к. диспатч отрисовки происходит асинхронно через очередь событий UI потока. Поэтому нужно так же добавить этот таск в очередь событий, чтобы он выполнился именно в конце отрисовки.
Много статистики я не насобирал, но на Nexus 4, Nexus 5 весь цикл отрисовки занимает 1-4мс на экранах из демо-проекта.
Galaxy nexus — около 10мс.
Sony Xperia Z3 compact почему-то справляется хуже — 8-16мс.
Тем не менее, производительностью я удовлетворен, FPS держится на хорошем уровне.
Весь код есть на гитхабе (библиотека и демо-проект) — BlurView.
Идеи, замечания, баг-репорты и пулл реквесты приветствуются.
Но можно ли сделать это настолько эффективно, чтобы без лагов перерисовывать размытый bitmap при любом изменении контента, как это реализовано в iOS?
Итак, что хотелось бы сделать:
- ViewGroup, которая сможет размывать контент, который находится под ней и отображать в качестве своего фона. Назовем ее BlurView
- Возможность добавлять в нее детей (любые View)
- Не зависеть от какой-либо конкретной реализации родительской ViewGroup
- Перерисовывать размытый фон, если контент под нашей View меняется
- Делать полный цикл размытия и отрисовки менее чем за 16ms
- Иметь возможность изменять стратегию размытия
В статье я постараюсь обратить внимание только на ключевые моменты, за деталями прошу сюда.
gif с примером (8mb)

Как получить bitmap с контентом под BlurView?
Создадим Canvas, Bitmap для него, и скажем всей View-иерархии нарисоваться на этом канвасе.
internalBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
internalCanvas = new Canvas(internalBitmap);
...
rootView.draw(internalCanvas);
Теперь в internalBitmap нарисованы все View, которые видны на экране.
Проблемы:
Если у вашего rootView нет заданного фона, нужно предварительно нарисовать на канвасе фон Activity, иначе он будет прозрачным на битмапе.
final View decorView = getWindow().getDecorView();
//Activity's root View. Can also be root View of your layout
final View rootView = decorView.findViewById(android.R.id.content);
final Drawable windowBackground = decorView.getBackground();
...
windowBackground.draw(internalCanvas);
rootView.draw(internalCanvas);
Хотелось бы рисовать только ту часть иерархии, которая нам нужна. То есть под нашей BlurView, с соответствующим размером.
Кроме того, было бы неплохо уменьшить Bitmap, на котором будет происходить отрисовка. Это ускорит отрисовку View иерархии, сам blur, а так же уменьшит потребление памяти.
Добавляем поле scaleFactor, желательно, чтобы этот коэффициент был степенью двойки.
float scaleFactor = 8;
Уменьшаем bitmap в 8 раз и настраиваем матрицу канваса так, чтобы корректно на нем рисовать, учитывая позицию, размер и 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);
}
Теперь при вызове rootView.draw(internalCanvas), мы будем иметь на internalBitmap уменьшенную копию всей View-иерархии. Выглядеть она будет страшно, но это меня не очень волнует, т.к. дальше я все равно буду ее размывать.
Проблемы:
rootView скажет всем своим детям нарисоваться на канвасе, в том числе и BlurView. Этого хотелось бы избежать, BlurView не должна размывать сама себя. Для этого можно переопределить метод draw(Canvas canvas) в BlurView и делать проверку на каком канвасе ее просят отрисоваться.
Если это системный канвас — разрешаем отрисовку, если это internalCanvas — запрещаем.
Собственно, blur
Я сделал интерфейс BlurAlgorithm и возможность настройки алгоритма.
Добавил 2 реализации, StackBlur (by Mario Klingemann) и RenderScriptBlur. Вариант с RenderScript выполняется на GPU.
Чтобы сэкономить память, я записываю результат в тот же bitmap, который мне пришел на вход. Это опционально.
Так же можно попробовать кэшировать outAllocation и переиспользовать его, если размер входного изображения не изменился.
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());
}
//do not use inAllocation in forEach. it will cause visual artifacts on blurred Bitmap
Allocation outAllocation = Allocation.createTyped(renderScript, inAllocation.getType());
blurScript.setRadius(blurRadius);
blurScript.setInput(inAllocation);
blurScript.forEach(outAllocation);
outAllocation.copyTo(outputBitmap);
inAllocation.destroy();
outAllocation.destroy();
return outputBitmap;
}
Когда перерисовывать blur?
Есть несколько вариантов:
- Вручную делать апдейт, когда вы точно знаете, что контент изменился
- Завязаться на события скролла, если BlurView находится над скроллящимся контейнером
- Слушать вызовы draw() через ViewTreeObserver
Я выбрал 3 вариант, используя OnPreDrawListener. Стоит заметить, что его метод onPreDraw дергается каждый раз, когда любая View в иерархии рисует себя. Это, конечно, не идеал, т.к. придется перерисовывать BlurView на каждый чих, но зато это не требует никакого контроля со стороны программиста.
Проблемы:
onPreDraw будет так же вызван, когда будет происходить отрисовка всей иерархии на internalCanvas, а так же при отрисовке BlurView.
Чтобы избежать этой проблемы, я завел флаг isMeDrawingNow.
Вроде все очевидно, кроме одного момента. Ставить его в false нужно через handler, т.к. диспатч отрисовки происходит асинхронно через очередь событий UI потока. Поэтому нужно так же добавить этот таск в очередь событий, чтобы он выполнился именно в конце отрисовки.
drawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (!isMeDrawingNow) {
updateBlur();
}
return true;
}
};
rootView.getViewTreeObserver().addOnPreDrawListener(drawListener);
Производительность
Много статистики я не насобирал, но на Nexus 4, Nexus 5 весь цикл отрисовки занимает 1-4мс на экранах из демо-проекта.
Galaxy nexus — около 10мс.
Sony Xperia Z3 compact почему-то справляется хуже — 8-16мс.
Тем не менее, производительностью я удовлетворен, FPS держится на хорошем уровне.
Заключение
Весь код есть на гитхабе (библиотека и демо-проект) — BlurView.
Идеи, замечания, баг-репорты и пулл реквесты приветствуются.