[Из песочницы] Программное создание NinePatchDrawable
В новом Android Lollipop появился такой интересный компонент как VectorDrawable. Если использовать его с умом, можно значительно снизить объем приложения, сэкономив на графических ресурсах, плюс, использование векторной графики освобождает нас от муторного процесса создания изображений под разные плотности экрана. Первая мысль, которая меня посетила, когда я увидел VectorDrawable, была: «Ух ты! А его можно тянуть как NinePatch?». Оказалось нельзя. Тут можно было бы немного огорчиться и довольствоваться тем, что хотя бы иконки можно в векторе держать. Однако, я на этом решил не останавливаться. В итоге получилась универсальная утилита, которая из любого Drawable способна сделать NinePatchDrawable.
В изображении, которое вы видите, используется вектор, но он растянут по центру. И это удивительно! Возможность растягивать вектор только в определенных областях предоставляет, по истине, колоссальные возможности. А если учесть, что есть проекты позволяющий использовать вектор на более ранних версиях андроида, векторные изображения начинают показывать себя во всем своем великолепии.Перед тем, как начать чудесное превращение, давайте немного углубимся в детали и выясним, как же нам прийти к желаемому результату. Для начала попытаемся понять, что из себя представляет NinePatchDrawable. А он состоит из двух основных частей, первая из которых — это Bitmap, а вторая массив байт, называемый «chunk», содержащий в себе информацию о том как эту Bitmap растягивать. Из этого следует, что имея на руках любую Bitmap и соответствующий ей «chunk», возможно создать NinePatchDrawable в обход стандартной модели: создать 9.png, положить его в проект, скомпилировать.
Однако, на практике не все так просто. Дело в том, что, фактически, не реально найти документацию про то как формируется «chunk». Его генерацией, ещё на стадии компиляции, занимается утилита «aapt», а в API андроида нет ни одного класса помогающего в генерации «chunk». Мне удалось выяснить, что из себя представляет большая часть массива. Скажу честно, я не до конца понял какой байт за что отвечает, однако этого оказалось достаточно.
Что такое «chunk»? Разберем этот массив по байтам. Байты, назначение которых мне не известно, я решил назвать «магическими», в силу их таинственности и загадочности. Итак:[0] — флаг wasDeserialized; [1] — указывает количество точек в массиве XDivs; [2] — указывает количество точек в массиве YDivs; [3] — указывает количество точек в массиве Colors; [4–11] — 8 «магических» байт; [12–27] — 16 байт для Padding; [28–31] — 4 «магических» байта; [32- ~] — далее идет перечисление массивов XDivs, YDivs и Colors. Нулевой байт хранит в себе булевое значение «wasDeserialized». Во всех, найденных мной, примерах говорится, что он должен быть 0×01, но если указать вместо этого любое другое значение ни чего страшного не произойдет, он автоматически установится в true при конвертировании массива «chunk» в нативный объект.Данные о Padding, XDivs, YDivs и Colors хранятся в int (4 байта). Кстати, именно поэтому Padding занимает не 4 байта, по количеству сторон, а 16 байт.
XDivs и YDivs содержат области для растягивания по осям X и Y. Счет ведется с нуля. Первое число указывает на начало первой области, следующее на её конец и так далее. Затем, аналогичным образом описывается следующий массив.
В качестве примера давайте разберем NinePatch изображение 6 на 6.
Здесь, растяжимая область по оси X проходит от 2 до 3 пикселя, а по оси Y от 2 до 4. Значит XDivs будет состоять из [0×02, 0×00, 0×00, 0×00, 0×03, 0×00, 0×00, 0×00], а YDivs из [0×02, 0×00, 0×00, 0×00, 0×04, 0×00, 0×00, 0×00]. В качестве первого (второго по порядку) и второго (третий по порядку) байтов массива «chunk», определяющих размеры XDivs и YDivs, следует указать 0×02, что значит 2 точки по 4 байта каждая.
Для, лучшего понимания того, что должно храниться в массиве Colors я внёс небольшие изменения в предыдущее изображение давайте посмотрим на него, ещё раз.
Как видно из рисунка, изображение можно поделить на 9 регионов. Так вот, Colors определяет как эти регионы рисовывать. Есть 2 варианта:
0×00000000 (TRASPARENT) регион будет прозрачным. 0×00000001 (NO_COLOR) регион будет видимым. Как могло показаться из названия, Colors отвечает за некий цвет, однако, в данном случае, его назначение на много тривиальней — указывать видимость региона.Для приведенного изображения, если мы хотим оставить все регионы видимыми, нужно 9 раз указать [0×01, 0×00, 0×00, 0×00], а для 3 байта (4 по порядку) массива «chunk» выставить значение равное 0×09.Любопытное замечание: по всей видимости, нативному коду, занимающемуся рисованием, все равно на размеры массива Colors, он сам знает сколько регионов у изображения, и получит их не зависимо от того какой размер мы задали для Colors. В результате чего произойдет обращение к области памяти за пределами нашего массива, как итог, получим то пропадающие то появляющиеся сектора, от одной прорисовки к другой.
По началу, я сомневался, хватит ли мне этого для создания полноценного NinePatch. Оказалось, хватит. Дело в том, что NinePatch, перед использованием «chunk», преобразует его при мощи метода validateNinePatchChunk в нативный объект Res_png_9patch. Если заглянуть в исходники, по коду можно увидеть, непонятные для нас байты не используются, а значит, можно заполнить их любыми значениями, например нулями.
NinePatchBuilder Теперь, зная как с генерировать «chunk», не составит ни какого труда создать NinePatch из любого изображения, в том числе и Drawable, если, предварительно, нарисовать его на Bitmap. Для упрощения этих действий я решил создать класс NinePatchBuilder.Следующий код показывает как использовать его в случае с обычным Bitmap.
NinePatchBuilder ninePatchBuilder = new NinePatchBuilder (getResources ()) .addStretchSegmentX (0.49f, 0.51f) .addStretchSegmentY (0.49f, 0.51f) .setBitmap (bitmap);
Drawable drawable = ninePatchBuilder.build (); Методами addStretchSegment указываются области для растягивания. Поскольку, при создании NinePatch, могут использоваться изображения, размер которых заранее не известен, было решено использовать относительные размеры в диапазоне [0, 1]. При вызове build, в зависимости от установленных параметров и размера Bitmap, сформируется массив «chunk» и создастся NinePatchDrawable.Вот что происходит внутри NinePatchBuilder-а:
// Код из NinePatchBuilder. private Drawable buildFromBitmap (Bitmap bitmap) { return new NinePatchDrawable (mResources, bitmap, getChunkByteArray (bitmap), getPaddingRect (bitmap.getWidth (), bitmap.getHeight ()), mSrcName); } Код метода getChunkByteArray приводить не буду, так как большая часть его реализации вытекает из описанного ранее алгоритма генерации «chunk».Аналогично происходит для Drawable. В качестве Drawable тут может выступать все что угодно, в том числе и VectorDrawable. В итоге, имея лишь одно векторное изображение, мы получаем полный набор NinePatchDrawable, для всех плотностей экрана! Допустим, у нас есть векторное изображение.
android.xml
if (result!= null) { try { result.inflate (new ResourceWrapper (mResources), parser, attrs); return result; } catch (XmlPullParserException | IOException e) { e.printStackTrace (); } } } } Это часть кода из NinePatchBuilder, он создает новый Drawable того же класса, что и исходный, используя Class.newInstance (), а затем наполняет его при помощи метода inflate. Все это похоже на то, что происходит внутри LayoutInflater, за исключением ResourceWrapper. В нем и таится вся суть. Если заглянуть в работу метода inflate, то мы увидим, дочерние Drawable получаются методом getDrawable из переданных, в качестве параметра, ресурсов. Для получения желаемого результата достаточно переопределить этот метод. // Код из NinePatchBuilder. private class ResourceWrapper extends Resources {
public ResourceWrapper (Resources resources) { super (resources.getAssets (), resources.getDisplayMetrics (), resources.getConfiguration ()); }
@Override public Drawable getDrawable (int id) throws NotFoundException { return buildFromDrawable (id, mDrawableWidth, mDrawableHeight); }
@Override
public Drawable getDrawable (int id, Theme theme) throws NotFoundException {
return buildFromDrawable (id, mDrawableWidth, mDrawableHeight);
}
}
Благодаря такому «финту ушами» мы реализовали полную поддержку всех наследников DrawableContainer с любым уровнем вложенности, и если вы преобразуете StateListDrawable, то на выходе получится StateListDrawable состоящая из NinePatchDrawable.XML и кеширование
Одного билдера мне оказалось мало, я решил пойти дальше и сделать класс NinePatchInflater, собирающий NinePatch из XML файла. В итоге, наш Drawable можно описать следующим образом:
Как подключить к своему проекту Есть 2 варианта.1 вариант (в лоб):2 вариант (элегантный): Скачать проект с гита в отдельную папку, например «NinePatchBuildUtils» В файл settings.gradle добавить: include ': ninepatchbuildutils' project (': ninepatchbuildutils').projectDir = new File ('<Путь к папке с проектами>/NinePatchBuildUtils/ninepatchbuildutils/') Также можно использовать относительный путь: project (': ninepatchbuildutils').projectDir = new File (settingsDir, '…/NinePatchBuildUtils/ninepatchbuildutils/') В фаил build.gradle модуля приложения добавить зависимость: compile project (': ninepatchbuildutils') Пересобрать проект Ссылки для особо упоротых как я любопытных: Исходники NinePatch: NinePatch.java и NinePatch.cppМесто где можно почитать про Res_png_9patch: ResourceTypes.h и ResourceTypes.cppСпасибо за внимание!