Створення нативного watchface для Gear S3/S2


Що таке tizen і з чим його їдять найкраще напевно зможе розповісти гугл або будь-який інший бінг. А ми розглянемо як зробити native додаток написавши якомога менше нативного коду.

І так давайте знайомиться c Edje. Edje це спосіб опису інтерфейсу і дій в efl, близький аналог qt quick, але з'явився раніше і не перетягивающий на себе частину функціоналу бібліотеки.
Почнемо з того, що розглянемо приблизну структуру edje файлу:
  • collection — кореневий елемент один на файл;
  • group — група елементів, в колекції їх може бути декілька, в даному контексті наприклад звичайний циферблат і енергозберігаючий, програм, які можна використовувати як окремий компонент форми, або як layout для розміщення віджетів;
  • script — блок для написання свого коду на embryo. Це такий простий мова заснований на Small мову, з його допомогою можна змінювати елементи edje за якогось алгоритму на льоту(наприклад зробити тікає від курсору миші кнопку);
  • image — блок опису використовуваних зображень;
  • parts — в деякому роді основний блок оскільки саме в ньому розміщуються частини, які в подальшому можна використовувати у вікні програми.
Тепер почнемо робити фейс. В першу чергу ми оголосили в блоці image які зображення будемо використовувати.
image
images {
image: "aod_h.png" COMP;
image: "aod_m1.png" COMP;
image: "aod_m2.png" COMP;
image: "aod_m3.png" COMP;
image: "aod_m4.png" COMP;
image: "aod_m5.png" COMP;
}

Далі ми описуємо відображаються елементи та їх стан за замовчуванням і розміщуємо текстове поле для індикації AM/PM. Що і як ми будемо робити ми розглянемо трохи нижче.
parts
parts {
part{ 
name:"aod/h";
scale: 1;
type: IMAGE;
description{ 
state: "default";
max: 360 360;
visible: 1;
image.normal: "aod_h.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z: 210.0;
}
}
part { 
name:"aod/m1";
scale: 1;
type: IMAGE;
description { 
state: "default";
max: 360 360;
visible: 1;
image.normal: "aod_m1.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z:0;
}
}
part { 
name:"aod/m2";
scale: 1;
type: IMAGE;
description { 
state:"default";
max: 360 360;
visible: 1;
image.normal: "aod_m2.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z:0;
}
}
part { 
name:"aod/m3";
scale: 1;
type: IMAGE;
description { 
state:"default";
max: 360 360;
visible: 1;
image.normal: "aod_m3.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z:0;
}
}
part { 
name:"aod/m4";
scale: 1;
type: IMAGE;
description { 
state:"default";
max: 360 360;
visible: 1;
image.normal: "aod_m4.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z:0;
}
}
part { 
name:"aod/m5";
scale: 1;
type: IMAGE;
description { 
state:"default";
max: 360 360;
visible: 1;
image.normal: "aod_m5.png";
aspect: 1 1;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
map.on: 1;
map.rotation.z:0;
}
}
part{ 
name:"aod/act";
scale: 1;
type: RECT;
description { 
state:"default";
color: 0 136 170 55;
visible: 1;
max: 360 360;
align: 0.5 0.5;
rel1.relative: 0.00 0.00;
rel2.relative: 1.00 1.00;
}
}
part { 
name:"ampm";
type: TEXT;
scale: 1;
effect: SOFT_OUTLINE;
description { 
state:"default";
color: 255 255 255 255;
color2: 0 136 170 100;
max: 360 360;
visible: 1;
text {
size: 35;
font: "Sans";
text: "AM";
align: 0.5 0.5;
min: 0 0;
}
align: 0.5 0.5;
rel1.relative: 0.2505 0.3598;
rel2.relative: 0.7505 0.6398;
}
}
} 

Детальна документація parts.
А ось тепер починається найцікавіше, зараз ми опишемо частина яка дозволить використовувати мінімум коду на C.
Заголовок спойлера
script {
public minutes;
public hour;

public hideMinutes(val)
{
new i;
new j;
new pid;
new buf[24];
j = val;
for(i = 5; i > j; i--)
{
snprintf(buf, 23, "aod/m%d", i);
pid = get_part_id(buf);
custom_state(pid, "default", 0.0);
set_state_val(pid, STATE_VISIBLE, 0);
set_state(pid, "custom", 0.0);
}
}

public showMinutes(val)
{
new i;
new j;
new Float:angle;
new pid;
new x;
new buf[24];
j = val;
x = (val / 5);
angle = x * 30.0;
for(i = 0; i < j; i++)
{
snprintf(buf, 23, "aod/m%d", i);
pid = get_part_id(buf);
custom_state(pid, "default", 0.0);
set_state_val(pid, STATE_VISIBLE, 1);
set_state_val(pid, STATE_MAP_ON, 1);
set_state_val(pid, STATE_MAP_ROT_Z, angle);
set_state(pid, "custom", 0.0);
}
}

public setHour(val)
{
new Float:h;
h = (val > 12 ? val - 12 : val)*30.0;
if(val > 12)
{
set_text(PART:"ampm", "PM");
}
else
{
set_text(PART:"ampm", "AM");
}
custom_state(PART:"aod/h", "default", 0.0);
set_state_val(PART:"aod/h", STATE_MAP_ON, 1);
set_state_val(PART:"aod/h", STATE_MAP_ROT_Z, h);
set_state(PART:"aod/h", "custom", 0.0);
}

public setMinutes(val)
{
new y; 
new x;
if((val == 0)||(val == 60))
{
hideMinutes(0);
return;
}
x = val - ((val / 5)*5);
y = val%5;
showMinutes(val);
if(y != 0)
{
hideMinutes(x);
}
}

public setTime()
{
new y;
new m;
new d;
new yd;
new wd;
new hr;
new mn;
new Float:sec;
date(y, m, d, yd, wd, hr, mn, sec);
setHour(hr);
setMinutes(mn);
}

public message(Msg_Type:type, id, ...) 
{
if(id == 0)
{
setTime();
}
}
}

Тут і зараз ми розглянемо тільки найважливіші елементи embryo і пропустимо кілька моментів, а те що викличе питання чи не буде розглянуто зараз розберемо наступний раз.
set_text(PART:"ampm", "PM");
— задаємо значення елемента з текстом;
custom_state(PART:"aod/h", "default", 0.0);
— створюємо новий стан елемента на основі стану за замовчуванням;
set_state_val(PART:"aod/h", STATE_MAP_ON, 1);

set_state_val(PART:"aod/h", STATE_MAP_ROT_Z, h);
— повертаємо елемент h градусів;
set_state(PART:"aod/h", "custom", 0.0);
— робимо перехід зі стану за замовчуванням в новий стан, тривалість переходу 0.0 секунд.
Так само до частин можна звертатися за part_id:
new buf[24];

snprintf(buf, 23, "aod/m%d", i);
— навіть не знаю що тут сказати
pid = get_part_id(buf);
— отримуємо part_id по імені частини;
custom_state(pid, "default", 0.0);

public message(Msg_Type:type, id, ...)
— функція приймає сигнал з нативного коду.
Синтаксис embryo дуже сильно схожий на звичайний С. Кілька відмінностей звичайно ж є.
new оголошення локальних цілочисельних елементів
new <var>;
— змінна
new <var>[<size>];
— масив з 8 елементів
Числа з плаваючою точкою оголошуються хитріше:
new Float:<var name>;

Функції та глобальні змінні оголошуються і використанням ключового слова public;
public message()
— функція, як і в функція може повертати результат чи перервано з допомогою return .
public <varName>
— цілочисельна змінна, власне все те ж саме що вище про локальних змінних, тільки замість new використовується public і для встановлення значень замість знака = використовується вбудовані функції set_int(), get_int(), set_float() і т. д.
На цьому ми перейдемо до частини на С. Тут все досить просто створюємо проект watchface.У прикладі доступному в меню Tizen Studio ми зміною кілька функцій.
Додамо в struct appdata нове поле edje щоб постійно не отримувати вказівник на завантажений файл.
typedef struct appdata {
Evas_Object *win;
Evas_Object *conform;
Evas_Object *layout;
Evas_Object *edje;
} appdata_s;

Завантаження описана в 2 функціях нижче, там все досить просто elm_layout_file_set передаємо layout який буде базою для нашого edje файлу, шлях до файлу і ім'я групи елементів
Завантаження Edje файлу
void data_get_resource_path(const char *file_in, char *file_path_out, int file_path_max)
{
char *res_path = app_get_resource_path();
if (res_path) {
snprintf(file_path_out, file_path_max, "%s%s", res_path, file_in);
free(res_path);
}
}

void setup_layout(Evas_Object *layout)
{
char edje_path[PATH_MAX];
data_get_resource_path(EDJE_FILE_PATH, edje_path, sizeof(edje_path)-1);
elm_layout_file_set(layout, edje_path, "layout/watchface");
evas_object_size_hint_weight_set(layout, 360, 360);
evas_object_show(layout);
}

Тут створення UI все крім setup_layout залишилося без змін.
Створення UI
static void
create_base_gui(appdata_s *ad, int width, int height)
{
int ret;
watch_time_h watch_time = NULL;

/* Window */
ret = watch_app_get_elm_win(&ad->win);
if (ret != APP_ERROR_NONE) {
dlog_print(DLOG_ERROR, LOG_TAG, "failed to get window. err = %d", ret);
return;
}

evas_object_resize(ad->win, width, height);

/* Conformant */
ad->conform = elm_conformant_add(ad->win);
evas_object_size_hint_weight_set(ad->conform, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
elm_win_resize_object_add(ad->win, ad->conform);
evas_object_show(ad->conform);

/*Layout*/
ad->layout = elm_layout_add(ad->conform);
elm_object_content_set(ad->conform, ad->layout);
setup_layout(ad->layout);
ad->edje = elm_layout_edje_get(ad->layout);
ret = watch_time_get_current_time(&watch_time);
if (ret != APP_ERROR_NONE)
dlog_print(DLOG_ERROR, LOG_TAG, "failed to get current time. err = %d", ret);

update_watch(ad, watch_time, 0);
watch_time_delete(watch_time);

/* Show window after base gui is set up */
evas_object_show(ad->win);
}

І додамо останній штрих функцію.
Відправляємо в Edje сигнал що треба оновитися
static void
update_watch(appdata_s *ad, watch_time_h watch_time, int ambient)
{
Edje_Message_Int *msg;
if (watch_time == NULL)
return;

msg = alloca(sizeof(Edje_Message_Int));
msg->val = 1;
dlog_print(DLOG_VERBOSE, __PRETTY_FUNCTION__, "EDJE NOT NULL");
edje_object_message_send(ad->edje, EDJE_MESSAGE_INT_SET, 0, msg);
}

Все залишилося все це зібрати і запустити на годиннику або симуляторі.
результатimage
image
Репозиторій з прикладом
Документація по Edje
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.