Но можно ли сделать это настолько эффективно, чтобы без лагов перерисовывать размытый bitmap при любом изменении контента, как это реализовано в iOS?
Итак, что хотелось бы сделать:
- ViewGroup, которая сможет размывать контент, который находится под ней и отображать в качестве своего фона. Назовем ее BlurView
- Возможность добавлять в нее детей (любые View)
- Не зависеть от какой-либо конкретной реализации родительской ViewGroup
- Перерисовывать размытый фон, если контент под нашей View меняется
- Делать полный цикл размытия и отрисовки менее чем за 16ms
- Иметь возможность изменять стратегию размытия
В статье я постараюсь обратить внимание только на ключевые моменты, за деталями прошу сюда.
Как получить 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 и переиспользовать его, если размер входного изображения не изменился.
@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.
Идеи, замечания, баг-репорты и пулл реквесты приветствуются.
Комментарии (19)
Temtaime
03.06.2016 23:50-7А теперь возьмите китайфон с двуядерным цпу и дохлой графикой и побудьте пользователем своего приложения.
Нравится?Dimezis
04.06.2016 00:10+5Возьмите, отпишите о производительности. Может взять еще телефон на 1.6 Андроиде? :)
На Galaxy Nexus, который тоже довольно старый и не шибко мощный, все работает быстро.
Претензия странная.
ishkulov
05.06.2016 21:56Можно на старых устройствах просто не активировать этот эффект, как это реализовано на iOS (до iPhone 4S и iPad 4 blur заменяется transparency).
KumoKairo
04.06.2016 00:00+2Я помню на iOS очень быстрый блюр получается если через скомпиленный GLSL шейдер картинку скармливать, GPU такие задачи решает в разы быстрее, чем CPU. Может на Android подобная возможность есть? Не пробовали через шейдеры делать?
Dimezis
04.06.2016 00:12+3Собственно, блюр с помощью RenderScript как раз выполняется на GPU, и пока что является самым быстрым вариантом.
Может можно придумать действительно что-то еще быстрее на шейдерах, но я пока не пробовал.
sompylasar
04.06.2016 22:27Чем отличается от https://github.com/500px/500px-android-blur ?
Dimezis
05.06.2016 11:08+1Самое основное отличие в том, что в 500px нужно самому решать когда перерисовывать блюр. Дело даже не в том, что это неудобно, а в том, что это в принципе не всегда возможно.
Еще я попробовал на своем демо-проекте заюзать их либу и вижу, что их BlurringView почему-то не нарисовала блюр в правой части таба. Просто пустое место. А если в качестве blurredView ей указать root layout окна, она вообще крашится со StackOverflowError.
В целом, мне кажется, что она менее гибкая и менее удобная. Единственный плюс (имхо), это XML атрибуты для настройки. Надо взять на вооружение.sompylasar
05.06.2016 13:17Спасибо, замечательно! В таком случае можете предложить свой модуль здесь: https://github.com/react-native-fellowship/react-native-blur/issues/21 (опишите эти преимущества, там пока сошлись на варианте от 500px).
basnopisets
07.06.2016 14:06Статью не читай, сразу пиши комментарии) Вопрос снимается — прочитал статью невнимательно
adombrovsky
Спасибо за статью. Очень интересно, но в GitHub проекте не очень информативный скриншот. Блюр сливается с фоном изображения.