В этой заметке пойдет речь о неочевидных особенностях работы 9-patch структур, использующихся в Андроиде для создания «растягивающихся» элементов графического оформления.
Я не буду останавливаться на том, что это такое, благо на эту тему уже есть статьи. Ключевым элементом для создания растягивающихся изображений является так называемый chunk, структура типа byte[]. Этот chunk используется в конструкторе NinePatch, но документация о том, что же это такое, не слишком многословна:
Откуда берется chunk?
9-patch ресурсы могут пребывать в одном из двух видов. Первый — та самая картинка с черными однопиксельными линиями-маркерами, задающими области растяжения. Второй вид — скомпилированный. Внешне скомпилированная картинка выглядит точно так же, как и исходная, но линии-маркеры не представлены визуально, а сконвертированы в бинарный формат и зашиты внутрь под кодовым названием «npTc». Преобразование из первого вида во второй выполняет утилита aapt, на стадии подготовки ресурсов.
Для наглядности возьмем картинку, состоящую из одного пикселя красного цвета и сделаем её 9-patch ресурсом с помощью соответствующей утилиты. Сравним её в hex-редакторе со скомпилированной версией:
9 пикселей исходного изображения (1 + 8 пикселей маркеров)
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/fab/8f8/b86/fab8f8b866b3a2e6d9cbec29ad383439.png)
1 пиксель + бинарные данные после метки npTc
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/49b/5c1/dcd/49b5c1dcd95f17920fd5e88e8f8204b4.png)
Каждая из этих форм бытия имеет плюсы и минусы: с одной стороны, скомпилированная форма оптимизирована для использования в рантайме, но вы не сможете изменять её в графическом редакторе — chunk неизбежно будет затерт при пересохранении.
Что же конкретно из себя представляет эта структура и что в ней содержится, можно прочитать в исходниках (см. структуру Res_png_9patch)
Зачем нам это знать?
Документация к 9-patch ресурсам гласит: «It must be saved with the extension .9.png, and saved into the res/drawable/ directory of your project». Значит, это единственное место, ресурсы откуда компилирует aapt. Но что делать если мы хотим брать картинки из другого места, например из папки assets или вообще скачивать их в рантайме? Попробуем так:
Попытка создать NinePatch в лоб из «сырых» ресурсов таким образом приводит к нативному крэшу где-то в дебрях виртуальной машины, конкретно — на строке raw.getNinePatchChunk(), что вполне логично — вместо ожидаемого npTc там оказывается мешанина из исходной картинки и линий-маркеров.
Выход в том, чтобы подсовывать системе уже скомпилированные ресурсы. Самый простой способ сделать это следующий: вы берете пустой android-проект, складываете ваши ресурсы в res/drawable, компилируете, экспортируете apk, открываете apk как архив, вытаскиваете из того же места те же ресурсы, но уже скомпилированные. Копируете их в assets вашего приложения (ну или еще куда-то) и с этого момента вы можете конструировать валидные объекты NinePath из битмапов, используя вышеприведенный код.
Такие дела.
Я не буду останавливаться на том, что это такое, благо на эту тему уже есть статьи. Ключевым элементом для создания растягивающихся изображений является так называемый chunk, структура типа byte[]. Этот chunk используется в конструкторе NinePatch, но документация о том, что же это такое, не слишком многословна:
public NinePatch (Bitmap bitmap, byte[] chunk, String srcName)
chunk The 9-patch data chunk describing how the underlying bitmap is split apart and drawn.
Bitmap.getNinePatchChunk()
Returns an optional array of private data, used by the UI system for some bitmaps. Not intended to be called by applications.
Откуда берется chunk?
9-patch ресурсы могут пребывать в одном из двух видов. Первый — та самая картинка с черными однопиксельными линиями-маркерами, задающими области растяжения. Второй вид — скомпилированный. Внешне скомпилированная картинка выглядит точно так же, как и исходная, но линии-маркеры не представлены визуально, а сконвертированы в бинарный формат и зашиты внутрь под кодовым названием «npTc». Преобразование из первого вида во второй выполняет утилита aapt, на стадии подготовки ресурсов.
Для наглядности возьмем картинку, состоящую из одного пикселя красного цвета и сделаем её 9-patch ресурсом с помощью соответствующей утилиты. Сравним её в hex-редакторе со скомпилированной версией:
9 пикселей исходного изображения (1 + 8 пикселей маркеров)
![image](https://habrastorage.org/getpro/habr/post_images/fab/8f8/b86/fab8f8b866b3a2e6d9cbec29ad383439.png)
1 пиксель + бинарные данные после метки npTc
![image](https://habrastorage.org/getpro/habr/post_images/49b/5c1/dcd/49b5c1dcd95f17920fd5e88e8f8204b4.png)
Каждая из этих форм бытия имеет плюсы и минусы: с одной стороны, скомпилированная форма оптимизирована для использования в рантайме, но вы не сможете изменять её в графическом редакторе — chunk неизбежно будет затерт при пересохранении.
Что же конкретно из себя представляет эта структура и что в ней содержится, можно прочитать в исходниках (см. структуру Res_png_9patch)
Зачем нам это знать?
Документация к 9-patch ресурсам гласит: «It must be saved with the extension .9.png, and saved into the res/drawable/ directory of your project». Значит, это единственное место, ресурсы откуда компилирует aapt. Но что делать если мы хотим брать картинки из другого места, например из папки assets или вообще скачивать их в рантайме? Попробуем так:
public NinePatchDrawable create9Patch() {
Bitmap raw = BitmapFactory.decodeStream(...);
byte[] chunk = raw.getNinePatchChunk();
NinePatch patch = new NinePatch(raw, chunk, null);
return new NinePatchDrawable(patch);
}
* This source code was highlighted with Source Code Highlighter.
Попытка создать NinePatch в лоб из «сырых» ресурсов таким образом приводит к нативному крэшу где-то в дебрях виртуальной машины, конкретно — на строке raw.getNinePatchChunk(), что вполне логично — вместо ожидаемого npTc там оказывается мешанина из исходной картинки и линий-маркеров.
I/DEBUG ( 4694): signal 11 (SIGSEGV), fault addr 00000008
I/DEBUG ( 4694): r0 0000bd00 r1 00000007 r2 ad03db59 r3 00000000
I/DEBUG ( 4694): r4 0000bd00 r5 00000000 r6 ad3431b1 r7 41049cf8
I/DEBUG ( 4694): r8 beb484b8 r9 41049cf0 10 41049ce0 fp 00000000
I/DEBUG ( 4694): ip ad3431b1 sp beb48498 lr ad046f05 pc ad03db6c cpsr 00000030
I/DEBUG ( 4694): #00 pc 0003db6c /system/lib/libdvm.so
I/DEBUG ( 4694): #01 pc 000431c6 /system/lib/libandroid_runtime.so
I/DEBUG ( 4694): #02 pc 0000e434 /system/lib/libdvm.so
I/DEBUG ( 4694): #03 pc 00040b0a /system/lib/libdvm.so
I/DEBUG ( 4694): #04 pc 00013198 /system/lib/libdvm.so
I/DEBUG ( 4694): #05 pc 00017b9c /system/lib/libdvm.so
...
Выход в том, чтобы подсовывать системе уже скомпилированные ресурсы. Самый простой способ сделать это следующий: вы берете пустой android-проект, складываете ваши ресурсы в res/drawable, компилируете, экспортируете apk, открываете apk как архив, вытаскиваете из того же места те же ресурсы, но уже скомпилированные. Копируете их в assets вашего приложения (ну или еще куда-то) и с этого момента вы можете конструировать валидные объекты NinePath из битмапов, используя вышеприведенный код.
Такие дела.