[Из песочницы] Программное создание NinePatchDrawable

В новом Android Lollipop появился такой интересный компонент как VectorDrawable. Если использовать его с умом, можно значительно снизить объем приложения, сэкономив на графических ресурсах, плюс, использование векторной графики освобождает нас от муторного процесса создания изображений под разные плотности экрана. Первая мысль, которая меня посетила, когда я увидел VectorDrawable, была: «Ух ты! А его можно тянуть как NinePatch?». Оказалось нельзя. Тут можно было бы немного огорчиться и довольствоваться тем, что хотя бы иконки можно в векторе держать. Однако, я на этом решил не останавливаться. В итоге получилась универсальная утилита, которая из любого Drawable способна сделать NinePatchDrawable.535be65930be4d71a565d79bac0a46ee.png

В изображении, которое вы видите, используется вектор, но он растянут по центру. И это удивительно! Возможность растягивать вектор только в определенных областях предоставляет, по истине, колоссальные возможности. А если учесть, что есть проекты позволяющий использовать вектор на более ранних версиях андроида, векторные изображения начинают показывать себя во всем своем великолепии.Перед тем, как начать чудесное превращение, давайте немного углубимся в детали и выясним, как же нам прийти к желаемому результату. Для начала попытаемся понять, что из себя представляет 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.

419c113db92c4a38b2006770fb6afa1d.png

Здесь, растяжимая область по оси 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 я внёс небольшие изменения в предыдущее изображение давайте посмотрим на него, ещё раз.

050e6f9ce64f470d9035ab0e9ba96f70.png

Как видно из рисунка, изображение можно поделить на 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

Преобразовать его в NinePatch не составит большого труда. NinePatchBuilder ninePatchBuilder = new NinePatchBuilder (resources) .addStretchSegmentX (0.49f, 0.51f) .addStretchSegmentY (0.49f, 0.51f) .setDrawable (R.drawable.android, (int) resources.getDimension (R.dimen.android_width), (int) resources.getDimension (R.dimen.android_height)); Дополнительно нужно указать размеры в пикселях, так как не все Drawable имеют фиксированные размеры. В момент вызова build, Drawable нарисуется на Bitmap и уже эта Bitmap используется для создания NinePacthDrawable.Особые случаи Естественно, слепо использовать нарисованный Drawable, не всегда удачное решение, так как есть, например, DrawableContainer и его наследники. Для поддержки таких сложный объектов пришлось пойти на определенную хитрость. // Код из NinePatchBuilder. if (drawable instanceof DrawableContainer) { final XmlPullParser parser = mResources.getXml (drawableId); final AttributeSet attrs = Xml.asAttributeSet (parser); int type = XmlPullParser.START_DOCUMENT; try { while ((type=parser.next ()) != XmlPullParser.START_TAG && type!= XmlPullParser.END_DOCUMENT) { // Empty loop } } catch (XmlPullParserException | IOException e) { e.printStackTrace (); } if (type == XmlPullParser.START_TAG) { Drawable result = null; try { result = drawable.getClass ().newInstance (); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace (); }

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 можно описать следующим образом: Файл должен находиться в папке «xml». Теперь, код, занимающийся созданием такого Drawable, можно сократить до одной строки. Drawable drawable = NinePatchInflater.inflate (resources, R.xml.vector_drawable_nine_patch); Кроме вынесения большей части кода в отдельный файл у inflater-a есть ещё один большой плюс — это кеширование по id ресурса. Дело в том, что создание Drawable может оказаться весьма дорогой операцией, особенно в нашем случае, когда для получения одного Drawable, приходится создавать кучу не нужных, в будущем, объектов. К счастью большая часть необходимой работы уже выполнена в классе ConstantState, нам лишь необходимо сохранять ConstantState созданных Drawable в кеше и, при необходимости, создавать новые Drawable при помощи метода ConstantState.newDrawable (). Не буду углубляться в подробности, статья и так получилась развернутой, к тому же я не придумал ни чего нового, именно таким способом происходит кеширование в классе Resources.Заключение Получилось неплохо, однако, создать полноценную обёртку над ресурсами, что бы можно было вставлять ссылки на эти файлы прямо в XML разметке, не прибегая к написанию программного кода, так и нет получилось. Как оказалось, при создании View, местами, используются методы с модификатором доступа «по умолчанию», а иногда, на прямую вызываются статические методы класса Drawable. Несмотря на это, считаю, что желаемый результат был достигнут, хоть и не в полной мере.Проект на GitHub: NinePatchBuildUtils

Как подключить к своему проекту Есть 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Спасибо за внимание!

© Habrahabr.ru