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

в 6:50, , рубрики: C, edje, efl, elementary, GUI, tizen, tizen sdk, tizen watch, watchface, Разработка для интернета вещей, Разработка под Tizen
Создание нативного watchface для Gear S3-S2 - 1

Что такое tizen и с чем его едят лучше всего наверное сможет рассказать гугл или любой другой бинг. А мы рассмотрим как сделать native приложение написав как можно меньше нативного кода.

И так давайте знакомится c Edje. Edje это способ описания интерфейса и действий в efl, близкий аналог qt quick, но появившийся раньше и не перетягивающий на себя часть функционала библиотеки.

Hачнем с того что рассмотрим примерную структуру 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

Автор: trollsid

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js