Pull to refresh

Тай'Дзен: первые шаги (часть 2)

Reading time7 min
Views5.2K
Уважаемое хабрасообщество, приветствую!
Продолжим рассмотрение простого нативного приложения для ведения списка покупок. В первой части были проведены подготовительные работы: исследованы структура типового Tizen-проекта, некоторые подходы к проектированию архитектуры приложения, а также особенности работы с WYSIWYG-редактором GUI. Сегодня читателя ожидают сцены и редактор сцен, управление списками графических элементов управления (контролов), их кастомизация и обработка событий. Третья часть будет чуть позже, она посвящена работе с базой данных, однако в тексте приведена ссылка на git-репозиторий. Все коммиты снабжены комментариями, поэтому работу с данными можно исследовать уже сейчас, не дожидаясь третьей части. Для каждого этапа указаны метки на соответствующие коммиты, поэтому добро пожаловать под кат.

Часть первая

ЧАСТЬ ВТОРАЯ

Начнем с постановки задачи. Перед походом в супермаркет часто нужно составить список покупок, чтобы ничего не забыть. Мобильное приложение удобнее обычного блокнота с ручкой по следующим параметрам:
  • Для мобильного приложения не нужен очередной 100500-й блокнот (смартфон с собой по умолчанию);
  • В приложении удобно «вычеркивать» совершенные покупки, при этом список может автоматически переупорядочиваться (например, выполненные покупки переносить вниз – очень актуально для длинных списков);
  • В приложении можно организовать поиск;
  • Электронный список легко отредактировать (в магазине может и не оказаться нужной вещи/продукта — заменяем по ходу дела);
  • Наконец, электронный список можно отправить по sms/e-mail, если непосредственно процесс покупок отдаем на аутсорс.


Рис. by Leka

Git-репозиторий содержит учебный проект, в котором рассмотрены этапы создания такого приложения. Все значимые этапы отмечены тегами (см. далее по тексту). READMe.txt описывает изменения, которые произошли между двумя тегами.

Для работы приложения понадобятся как минимум две формы или панели. На первой работаем непосредственно с названиями покупок (добавить, удалить, переименовать, вычеркнуть). Список покупок, который невозможно сохранить, смысла не имеет, поэтому на второй форме работаем с названиями списков: создать новый, удалить, переименовать, дублировать, копировать в буфер обмена (чтобы потом отправить исполнителю).

В настоящий момент к ShoppingListMainForm подключены три вкладки (см. первую часть статьи). Сократим поголовье до двух, т.е. удалим третью вкладку. Удаление осуществляем сначала в UI Builder, а затем уже наводим порядок в коде.
Удаление в UI Builder:
  1. в IDL_FORM удаляем кнопку для третьей панели (HEADERITEM3) из Header-а;
  2. удаляем непосредственно саму панель IDL_PANEL3 — xml-файл с ее описанием;
  3. удаляем TabScene3 в Workflow;
  4. сохраняем изменения :)

В общем-то означенные пункты очевидны, но третий рассмотрим подробнее.

Вместе с TabScene3 удаляются и ее переходы, в том числе и переход из TabScene1 – IDSCNT_3. Поэтому необходимо добавить переход из TabScene1 в TabScene, назовем его IDSCNT_2.



По умолчанию новый переход сохраняет историю в стеке переходов. О свойствах переходов можно подробно почитать здесь. Запоминать переходы между вкладками смысла нет, поэтому выставляем свойство History в значение no:



Свойство Destroy определяет, будет ли удалена из памяти текущая панель при переходе на новую. Если выставлено значение keep, то панель будет создана один раз, а при последующих переходах – просто отображаться на экране. Если выставлено значение destroy, то каждый раз при переходе на панель она будет создаваться заново (поскольку была удалена при выходе из нее). При этом, независимо от значения данного свойства, форма будет создана только один раз, поскольку все переходы пока осуществляются в ее пределах.

Система кодогенерации добавит команду перехода в обработчик события ShoppingListMainForm::OnActionPerformed(), останется лишь переместить ее в соответствующий кейс:

void
ShoppingListMainForm::OnActionPerformed(const Tizen::Ui::Control& source, int actionId)
{
	SceneManager* pSceneManager = SceneManager::GetInstance();
	
	AppAssert(pSceneManager);

	switch(actionId)
	{
	case ID_HEADER_ITEM1:
		pSceneManager->GoForward(SceneTransitionId(IDSCNT_1));
		break;
	case ID_HEADER_ITEM2:
		pSceneManager->GoForward(SceneTransitionId(IDSCNT_2));
		break;
	default:
		break;
	}
}


Наводим порядок в коде:
  1. удаляем исходный код панели – файлы ShoppingListTab3.h и ShoppingListTab3.cpp;
  2. автоматического удаления идентификаторов из AppResourceId.h и AppResourceId.cpp при работе с UI Builder не происходит, поэтому необходимо удалить лишние строки вручную: IDL_PANEL3 (ID панели) и IDSCNT_3 (ID перехода на эту панель);
  3. удаляем упоминания о третьей панели из ShoppingListPanelFactory.


Внимание! Возможно, некоторым неофитам сейчас захочется разложить все исходники по папочкам: панели и формы отдельно, управляющий код и прочие контролеры с моделями отдельно… Так вот, адепты Тай’Дзен считают перфекционизм большой ошибкой. Поэтому любое изменение в структуре исходников GUI, к которым имеет отношение UI Builder, приводит к непредсказуемому поведению последнего. Файлы нельзя переименовывать, перемещать в каталог, отличный от корневого, и т.д. В противном случае вначале начинает сходить с ума система кодогенерации, затем построитель workflow, что сводит на нет все преимущества использования данной утилиты.

Приступим непосредственно к разработке приложения.

Приложение предназначено для управления списками – ими и займемся. Открываем c помощью UI Builder панель IDL_PANEL1.xml, затем перетаскиваем на рабочую область из окна Toolbox компонент ListView, вкладка ListView & TableView. Различия между шестью представленными на вкладке видами списков подробно описаны в документации, а сейчас нас ожидает очередная «радость» – не работает управление порядком наложения (это я так перевел термин z-order):



Кнопки перемещения GUI-компонентов по «слоям» неактивны. В настоящий момент IDC_LISTVIEW1 закрывает IDC_LABEL1 – не проблема, поскольку ListView прозрачен. Однако если будет закрыта, например, кнопка (компонент Button), то мы просто не сможем ее нажать! В общем, подчинить Z-order с помощью UI Builder пока не представляется возможным, поэтому открываем IDL_PANEL1.xml с помощью обычного текстового редактора и меняем местами строчки, отвечающие за добавление компонентов:

<ScenePanel Bversion="2.0.0.201311071821" Dversion="20120315">
    <LogicalCoordinate>720</LogicalCoordinate>
    <Panel id="IDL_PANEL1">
… /* описание свойств */
    </Panel>
    <Label id="IDC_LABEL1" parent="IDL_PANEL1">
… /* описание свойств */
    </Label>
    <ListView id="IDC_LISTVIEW1" parent="IDL_PANEL1">
… /* описание свойств */
    </ListView>
</ScenePanel>

заменяем на

<ScenePanel Bversion="2.0.0.201311071821" Dversion="20120315">
    <LogicalCoordinate>720</LogicalCoordinate>
    <Panel id="IDL_PANEL1">
… /* описание свойств */
    </Panel>
    <ListView id="IDC_LISTVIEW1" parent="IDL_PANEL1">
… /* описание свойств */
    </ListView>
    <Label id="IDC_LABEL1" parent="IDL_PANEL1">
… /* описание свойств */
    </Label>
</ScenePanel>


После чего сохраняем изменения и снова открываем с помощью UI Builder. Компоненты поменялись местами. Если не поменялись – нужно пойти выпить кофе, затем расслабиться, принять позу, наиболее удобную для постижения Тай’Дзен и начать заново. Еще можно перезагрузить Tizen IDE.

Далее задаем для ListView нужные размеры способом, зависящим от выбранной координатной системы (по умолчанию – логическая относительная). В данном случае я привязал размеры ListView к размерам родительской панели IDL_PANEL1.

Теперь наполним ListView жизнью: вызываем в рабочей области контекстное меню и выбираем Add Event Handler.



Минимально необходимым набором в данном случае являются пункты IListViewItemEventListener и IListViewItemProvider. Выбор этих пунктов означает, что для ListView будет назначен объект, реализующий эти интерфейсы. Первый интерфейс отвечает за реакцию на события элементов списка, а второй предоставляет данные для создания этих элементов. В данном случае таким объектом является экземпляр класса ShoppingListTab1, т.е. родительская панель (описана в файле IDL_PANEL1.xml).

После нажатия кнопки Finish система кодогенерации автоматически пропишет реализацию интерфейсов и подписку на события ListView в файлах ShoppingListTab1.h и ShoppingListTab1.cpp. Результаты ее работы лучше не менять – она любит модифицировать измененный вручную код без предупреждения. В связи с этим эстетствующим адептам лучше отказаться от ее услуг вовсе и прописывать всё ручками. Как выглядит проект на данном этапе – см. метку v0.1.

Относительно IListViewItemProvider все довольно очевидно: создать/удалить элемент списка по индексу, вернуть количество оных элементов. В качестве элемента списка будем использовать CustomItem (я ведь обещал кастомизацию?). В него добавим EnrichedText, который будет использоваться в качестве контейнера для TextElement-ов – так наш элемент будет отображать текст.

Я сознательно не описываю подробно назначение этих элементов – в документации все изложено очень подробно. Моя цель – выявить кое-какие подводные камни.

EnrichedText позволяет очень гибко форматировать текст, но его возможности не безграничны. Прежде всего, в него нельзя добавить длинные тексты. Это значит, что область отображения текста не должна превышать 2048 пикселов (выяснено экспериментально). Я подозреваю, что это ограничение обусловлено максимальными размерами текстур, с которыми может работать библиотека рендеринга текста. Возникает вопрос, зачем такие сложности, когда можно воспользоваться готовым компонентом TextBox? Истина в том, что TextBox начинает тормозить, даже когда размер прокручиваемого текста в два раза превышает размер экрана. В настоящий момент использование связки ListView + CustomItem + EnrichedText позволяет создавать самый плавный скроллинг. Да, разбиваем текст на части и формируем адаптер, который будет потихоньку скармливать его в ListView при прокрутке. Тай’Дзен учит терпению.



Еще одна ловушка – пустая строка в TextElement. При попытке отрисовать текстовый элемент, инициированный пустой строкой, приложение упадет. Чтобы подстраховаться, я обычно в конце текста добавляю символ перевода строки \n:

pTextElement->Construct(strSomeText + "\n");


IDC_LABEL1 теперь не нужна, и ее можно удалить. Как выглядит проект на данном этапе – см. метку v0.2.

Теперь рассмотрим события, которые обрабатываются в IListViewItemEventListener. События OnListViewItemLongPressed и OnListViewItemStateChanged довольно очевидны – они возникают при нажатии на элемент. А вот событие OnListViewItemSwept для меня остается загадкой: мне еще ни разу не удалось его вызвать.

Событие OnListViewContextItemStateChanged заслуживает отдельного упоминания – это некий аналог контекстного меню в рамках элемента списка. Реализуется через довольно элегантный, хотя и очень простой эффект – элемент сдвигается в сторону, в то время как подложка динамически «осветляется». Чтобы события OnListViewContextItemStateChanged возникали, сustomItem-ы должны их генерировать, поэтому нужно подключить к ним так называемый ItemContext:

pItemContext = new (std::nothrow) ListContextItem();// создаем однократно
pItemContext->Construct();
pItemContext->AddElement(ID_CNTX_BTN_DELETE, "Delete");

…

AppAssert(pItemContext);
pItem->SetContextItem(pItemContext); // подключаем к каждому CustomItem




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

Часть третья
Tags:
Hubs:
Total votes 8: ↑6 and ↓2+4
Comments0

Articles

Information

Website
kamagames.ru
Registered
Founded
Employees
101–200 employees
Location
Россия