Pull to refresh

Визуальное редактирование разметки внутри Android-приложения

Reading time6 min
Views13K
Преамбула

Eclipse и Idea имеют собственные средства визуального редактирования разметки Android-приложения. NetBeans лишен этого счастья. Желание сотворить нечто подобное для привычной NetBeans сравнительно простыми средствами привело к идее перенести процесс визуального редактирования в само приложение. Резонов здесь несколько:
  • естественный Preview разметки средствами самого Android;
  • возможность автономной работы без desktop-ой IDE (может быть интересно, в первую очередь, дизайнерам);
  • «Богу – богово, а кесарю — кесарево». При реализации удобно использовать уже имеющиеся структуры данных View-объектов, константы классов и т.п…


Как это устроено?

Зеркальное дерево разметки. Основная идея – параллельно дереву объектов разметки (View), которое генерирует Android при загрузке Layout, программа создает зеркальное дерево собственных объектов (одноименные классы с префиксом Z – Zview, ZButton,…). Каждый из них имеет ссылку на объект – оригинал. Классы имеют наследование, аналогичное View, и поддерживают весь функционал, связанный с обходом дерева, например, при генерации xml-файлов разметки, создании списков параметров View. Кроме того, зеркальное дерево является основной структурой данных для описания открытого Layout: каждый объект содержит два важных параметра – вектор параметров разметки для View и зеркальный объект-описатель для LayoutParams.

Параметры View. Исходное многообразие параметров View поддерживается группой классов – наследников ZParam, каждый из них реализует всю специфику внешнего и внутреннего представления параметров данного типа, конвертирования, вызова диалога его редактирования, определения соответствия типам значений в скомпилированном файле. На данный момент реализованы типы – int, hex (16-ричный int), boolean, float, string, charSequence, dimen (размерности), id (идентификатор ресурса), image, color, enumInt (список именованных целых констант), enumString, enumClass (список именованных целых классов). В последнем случае для каждого enum-класса создается свой производный класс.

Вектор параметров для каждого View десериализуется из соответствующего xml-файла, размещенного в assets. Чтобы упростить процедуру создания таких файлов, в приложении имеется компонента, которая для всех View ищет геттеры/сеттеры и по ним создает описание предполагаемых параметров. Затем уже эти файлы редактируются вручную по документации.

Параметры разметки LayoutParams. Для параметров разметки аналогичных файлов описания не существует. Вместо этого для каждого View читается LayoutParams (его тип зависит от типа предка), затем с помощью рефлексии для него генерируется одноименный класс описателя – ZLP и его производные, каждый класс создает вектор параметров и записывает в него значения, считанные из соответствующего LayoutParams.

Скомпилированный xml-файл Layout-а. В файле Android-приложения (apk) xml-файлы описания Layout содержатся в скомпилированном виде. Подробное описание формата имеется на justanapplication.wordpress.com/category/android/android-binary-xml. Несомненные достоинства формата – ограниченное количество внутренних типов данных для параметров, достаточная простота, чтобы пропарсить его доморощенными средствами (например, для просмотра дампа или извлечения параметров в зеркальное дерево разметки). Кроме того, внутренний парсер Android — класс android.content. res.XmlBlock способен создавать View из байтного массива, содержащего данные в аналогичном формате.

Замечание. В самом apk-файле скомпилированные файлы содержатся в сжатом виде. Однако в нашем случае это не важно, т.к. парсер XmlBlock получает их уже разархивированными.

Как это работает?

При старте приложение средствами рефлексии опрашивает Android-классы на предмет сбора необходимых констант и формирует таблицы в виде пар <имя-значение>:
  • идентификаторы ресурсов из R.java (отдельно по разным типам — подклассам);
  • константы классов View;
  • android.R.attr – внутренние константы имен для всех параметров;
  • android.R.styleable – символьные обозначения констант, используемых в параметрах.

Для всех типов View из xml-файлов, размещенных в assets, десериализуются векторы объектов-описателей параметров (классы ZParam).

Редактирование в общих чертах происходит таком образом. Исходный нередактированный Layout загружается через id ресурса стандартным LayoutInflater. По загруженному View создается зеркальное дерево объектов, для каждого View копируется исходный вектор параметров. Затем собственный CXmlResourceParser, построенный на основе стандартного android.content.res.XmlResourceParser, читает параметры всех View и записывает их в объекты ZParam. Объекты-параметры, значения которых были прочитаны из файла разметки, отмечаются, так что в общем списке параметров они всегда будут выделены. Обработчики событий всех View настраиваются на программный код диалога редактирования.

При изменении любых параметров собственный CXmlParser пишет байтный массив в формате скомпилированного xml-файла, из которого парсер XmlBlock создает измененный View, меняя таким образом картинку Preview. То же самое CXmlParser делает при долговременном сохранении изменений в двоичный файл. При загрузке измененного Layout работает та же самая схема, только с другой парой парсеров: внутренний android.content. res.XmlBlock для загрузки View и собственный CXmlParser – для чтения параметров.
Для внешнего использования разметка может быть экспортирована в обычный xml-файл. Для этого используются собственные средства групп классов ZView и ZParam.

Размещение данных. Все данные размещены в каталоге GUIWizard на SD-карте. Приложение создает каталог с именем собственного пакета. Сгенерированные xml-файлы отредактированной разметки записываются туда с именами Layout-ов. Вспомогательные двоичные файлы скомпилированной разметки с расширением cxml записываются в подкаталог data. Сгенерированные файлы параметров для View записываются в каталог проекта с именами собственных классов (например, android.widget.ImageView.xml).

Замечание. Первоначально предполагалось, что при редактировании измененные параметры будут сразу же записываться сеттерами в соответствующие View, а при их отсутствии – напрямую в приватные поля View. Аналогично, для получения текущих значений полей предполагалось вызывать геттеры. Однако в дальнейшем оказалось, что имеется достаточно много исключений в однозначности «параметр-поле-сеттер-геттер». Например, одному параметру может соответствовать несколько сеттеров, один сеттер может принимать несколько параметров. Поэтому решено было полностью отказаться от взаимодействия объектов ZView – View. Вместо этого после каждого изменения параметра генерируется скомпилированный xml-файл разметки и пишется в байтный поток (т.е. речь не идет о файле в прямом смысле этого слова). Затем парсер XmlBlock создает по нему измененный View.

Рефлексия, как без нее ?

Приложение интенсивно использует рефлексию, в том числе для:
  • чтения констант имен параметров Android, ресурсов приложения и констант классов View;
  • генерации объектов групп классов Zview, Zparam и Zlayout в зависимости от имени класса текущего объекта;
  • обхода ограничений доступа в некоторых классах Android.

Интеграция с NetBeans

Модуль, встраиваемый в NetBeans, выполняет тривиальную работу. При нажатии дополнительной кнопки он через вызовы ADB просматривает содержимое каталога SDCard/GUIWizard/<имя пакета Android-приложения>. Имя пакета извлекается из файла манифеста текущего проекта, выбранного в окне проектов. Все файлы с расширением .xml копируются в /res/layout и удаляются. Старые версии переименовываются с расширением .orig. Исходники модуля — bitbucket.org/solus_rex/netbeans_guiwizard_plugin

Status quo

Исходники проекта размещены на bitbucket.org/solus_rex/android_guiwizard

Для отладки парсинга скомпилированного файла разметки можно использовать двоичные файлы, содержащиеся в самом приложении. Для этого необходимо разрархивировать apk-файл и скопировать xml-файлы из res/layout в подкаталог GUIWizard/<пакет приложения>/data (напомним, что они содержатся там в скомпилированном двоичном формате). В макете можно выполнять побайтное сравнение исходного двоичного файла разметки и файла, сгенерированного программой при совпадении их размерности. Для этого приложение при записи выходного файла сортирует таблицу имен и списки параметров в том порядке, в котором они были в исходном файле.

Целью текущего этапа было проверить основные идеи и реализовать их на макете. Макет позволяет:
  • просматривать списки констант;
  • читать собственные логи данных и стеков исключений;
  • генерировать файлы описания параметров View;
  • создавать новые пустые Layout;
  • просматривать список Layout (во всех списках короткий клик выполняет основную команду, длинный – вызывает контекстное меню команд).
Короткий клик в списке Layout вызывает его Preview, причем открывается последний сохраненный в двоичном файле, а при его отсутствии — ресурс в самом приложении.




Длинный клик поднимает меню команд, исполняемых над Layout. В Preview короткий клик по любому элементу вызывает список его параметров, длинный – сообщение со сгенерированным xml-тегом. В меню команд пункт «Список View» выводит список с именами и типами View, отформатированный по вложенности. Короткий клик в этом списке вызывает уже упомянутый список параметров, длинный – поднимает контекстное меню, в котором имеется команда добавления нового View в качестве потомка к выбранному.  




Короткий клик в списке параметров поднимает диалог, соответствующий его типу, длинный – список ресурсов, которые могут быть ему назначены.




Открытым остается вопрос добавления новых ресурсов – они должны быть зашиты в исходный проект.
Tags:
Hubs:
Total votes 8: ↑5 and ↓3+2
Comments1

Articles