Pebble: работа со статической графикой на примере создания 7-segment watchface

  • Tutorial
Все приложения для часов Pebble делятся на две категории watchapp — просто приложения, и watchface — приложения «часы», которые исходя из названия являются лицом устройства. Отличие «фейсов» — отсутствие реакции на хардварные кнопки, так как «UP» и «DOWN» используются для циклического переключения между установленными watchface.
Но, наверное, в силу низкого разрешения экрана 144x168 px, найти органично вписывающийся в дизайн часов ватчфейс, который при этом выполняет основную свою функцию — отсчет времени, довольно непросто.
Как мне кажется лучше всего на таком экране смотрятся цифры в стиле семисегментных индикаторов.
Ниже, подробнее о том, как добавить в свой watchface элегантного минимализма, индивидуальности и уникальных фишек.
Итак, немного картинок, обрывков кода и в итоге ссылка на готовый проект.


Создание watchface, структура и построение проекта подробно описано в соответствующем разделе документации Build Your Own Watchface [1]. Не буду повторятся, а сразу перейду к

Особенности


Что будет отличать наше приложение от полутора десятков из Examples и от сотен с mypebblefaces:

Обо всем по порядку:

7-segment шрифт


Для отображения цифр понадобятся два набора, основной большой (цифра 20x38px), для времени:

и дополнительный маленький (цифра 8x16px), для даты и секунд:

Оба набора отрисованы в графическом редакторе, в виде двухцветного png-файла.
Подключаем их как ресурсы в appinfo.json:
  "resources": {
    "media": [
      {
        "type": "png",
        "name": "DIGITS",
        "file": "images/digits.png"
      },
      {
        "type": "png",
        "name": "DIGITS_MIDI",
        "file": "images/digits_midi.png"
      }
    ]

Графический фреймворк описан в документации [2]. Нас интересует раздел, касающейся работы с растровыми изображениями [3].
Подготовительные действия для работы с растром, создание из ресурса вынесем в отдельную функцию:
/*...*/

static GBitmap *bmp_digits;
static GBitmap *bmp_digits_midi;
/*..*/

static void load_resources() {
    bmp_digits = gbitmap_create_with_resource(RESOURCE_ID_DIGITS);
    bmp_digits_midi = gbitmap_create_with_resource(RESOURCE_ID_DIGITS_MIDI);
}
/*..*/

static void window_load(Window *window) {
    load_resources();
}

не забудем освободить ресурсы:
static void destroy_resources() {
    gbitmap_destroy(bmp_digits);
    gbitmap_destroy(bmp_digits_midi);
}
/*..*/

static void window_unload(Window *window) {
    destroy_resources();
}

Наборы цифр считаны из ресурсов и созданы в виде растра в памяти, теперь необходима функция отрисовки отдельной цифры, при вызове функции указываем графический контекст, на котором будем рисовать, исходный набор из которого мы должны «выдрать» изображение и порядковый номер изображения в наборе:
/*
ctx - графический контекст;
sources - исходное изображение;
bounces - координаты и размер изображения для отрисовки;
number - порядковый номер изображения в наборе.
*/

static void draw_picture(GContext* ctx, GBitmap **sources, GRect bounces,
                         int number) {
    GPoint origin = bounces.origin;
    bounces.origin = GPoint(bounces.size.w*number, 0);
    GBitmap* temp = gbitmap_create_as_sub_bitmap(*sources, bounces);
    bounces.origin = origin;
    graphics_draw_bitmap_in_rect(ctx, temp, bounces);
    gbitmap_destroy(temp);
}

и для примера, чтобы нарисовать троечку по координатам (10, 0) в контексте:
    draw_picture(ctx, &bmp_digits, GRect(10, 0, 20, 38), 3);

к содержанию

Два экрана


Так как у нас будет два независимых экрана, каждый со своим наполнением, реализуем их в виде отдельных слоев, размером с экран часов:
/*..*/
static Layer *standby_layer;
static Layer *info_layer;
/*..*/

static void window_load(Window *window) {
    Layer *window_layer = window_get_root_layer(window);
    GRect bounds = layer_get_bounds(window_layer);

    load_resources();

    standby_layer = layer_create(bounds);
    layer_add_child(window_layer, standby_layer);
    info_layer = layer_create(bounds);
    layer_add_child(window_layer, info_layer);
}

Переключение между экранами вынесем в обработчик сервиса «Tick Timer»:
/*..*/
int current_screen = 0;
/*..*/
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
    // Каждую минуту помечаем "standby" для отрисовки
    if (units_changed & MINUTE_UNIT) {
        layer_mark_dirty(standby_layer);
    };

    switch (current_screen) {
        case 0:
            // Если слой "standby" скрыт, делаем его видимым и убираем "info"
            if (layer_get_hidden(standby_layer)) {
                layer_set_hidden(info_layer, true);
                layer_set_hidden(standby_layer, false);
            };
            break;
        case 1:
            layer_mark_dirty(info_layer);
            // Если слой "info" скрыт, делаем его видимым и убираем "standby"
            if (layer_get_hidden(info_layer)) {
                layer_set_hidden(standby_layer, true);
                layer_set_hidden(info_layer, false);
                // В зависимости от настройки запускаем таймер возврата на "standby"
                if (settings.s_auto) {
                    standby_timer = app_timer_register(30000, timer_callback, NULL);
                };
            };
            break;
    };
}

static void init(void) {
    /*..*/
    tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
}

Вот и подошли к тому, что нам уже надо вырисовывать контент на экранах. Для примера код для отображения контента на экране ожидания — цифровой циферблат.
Для начала зададим функцию отрисовки для слоя «standby_layer», которая автоматически вызывается, когда это необходимо:
static void window_load(Window *window) {
    standby_layer = layer_create(bounds);
    layer_add_child(window_layer, standby_layer);
    layer_set_update_proc(standby_layer, update_standby);
}

и реализуем отрисовку контента:
update_standby
static void update_standby(Layer *layer, GContext* ctx) {
    GRect bounds = layer_get_bounds(layer);

    // Цвет фона - черный
    graphics_context_set_fill_color(ctx, GColorBlack);
    // Режим композитинга - инвернтный
    graphics_context_set_compositing_mode(ctx, GCompOpAssignInverted);
    // Заливаем слой
    graphics_fill_rect(ctx, bounds, 0, GCornerNone);

    time_t temp = time(NULL);
    struct tm *tick_time = localtime(&temp);

    int hour_dicker = tick_time->tm_hour/10;
    int hour_unit = tick_time->tm_hour%10;

    int min_dicker = tick_time->tm_min/10;
    int min_unit = tick_time->tm_min%10;

    // Рисуем цифры
    draw_picture(ctx, &bmp_digits, GRect(20, 55, 20, 38), hour_dicker);
    draw_picture(ctx, &bmp_digits, GRect(42, 55, 20, 38), hour_unit);

    draw_picture(ctx, &bmp_digits, GRect(78, 55, 20, 38), min_dicker);
    draw_picture(ctx, &bmp_digits, GRect(100, 55, 20, 38), min_unit);

    // Рисуем разделитель
    graphics_context_set_fill_color(ctx, GColorWhite);
    GRect frame = (GRect) {
        .origin = GPoint(bounds.size.w/2-4, 63),
        .size = GSize(4, 4)
    };
    graphics_fill_rect(ctx, frame, 0, GCornerNone);
    frame = (GRect) {
        .origin = GPoint(bounds.size.w/2-4, 81),
        .size = GSize(4, 4)
    };
    graphics_fill_rect(ctx, frame, 0, GCornerNone);
}


Результат:

аналогично, используя draw_picture рисуем информационный экран, подробнее в исходниках.
Результат:

к содержанию

Переключение между экранами


Для переключения экранов задействуем встроенный акселерометр [4]. Для этого подпишемся на «Tap Event Service»:

static void tap_handler(AccelAxisType axis, int32_t direction) {
    current_screen = !current_screen;
}

static void init(void) {
   /*...*/

  tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
  accel_tap_service_subscribe(tap_handler);
}

к содержанию

Переход в «standby»


Для автоматического перехода на экран ожидания воспользуемся таймером [4].

/*..*/
AppTimer *standby_timer = NULL;
/*..*/

static void timer_callback() {
  current_screen = 0;
}

static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
    /*..*/
        case 1:
            layer_mark_dirty(info_layer);
            // Если слой "info" скрыт, делаем его видимым и убираем "standby"
            if (layer_get_hidden(info_layer)) {
                layer_set_hidden(standby_layer, true);
                layer_set_hidden(info_layer, false);
                // В зависимости от настройки запускаем таймер возврата на "standby"
                if (settings.s_auto) {
                    standby_timer = app_timer_register(30000, timer_callback, NULL);
                };
            };
            break;
    /*..*/
}

к содержанию

Состояние батарейки и bluetooth


Для отображения состояния батарейки [6] создадим ресурс с изображениями, соответствующими десяткам процентов (с такой точностью API отдает величину заряда):

"resources": {
    "media": [
    /*..*/
      {
        "type": "png",
        "name": "BATTERY",
        "file": "images/battery.png"
      },
    /*..*/
    ]
  }

И рисуем на соответствующем экране:
    /*..*/
    BatteryChargeState charge_state = battery_state_service_peek();
    int bat_percent = charge_state.charge_percent/10;
    if (charge_state.is_charging) {
        bat_percent = 110/10;
    };
    draw_picture(ctx, &bmp_battery, GRect(0, 0, 8, 15), bat_percent);
    /*..*/

Состояние bluetooth [7] отображаем соответствующей иконкой:
"resources": {
    "media": [
    /*..*/
      {
        "type": "png",
        "name": "BT",
        "file": "images/bluetooth.png"
      },
    /*..*/
    ]
  }

    if (bluetooth_connection_service_peek()) {
        draw_picture(ctx, &bmp_bt, GRect(0, 0, 8, 15), 0);
    };


к содержанию

Итог: watchface, который основное время не раздражает избыточной информацией, читаем и довольно смотрибелен, при желании делится расширенной информацией.

Для заинтересовавшихся:
Код проекта на Bitbucket
Приложение в Pebble App Store

1. Pebble Developers // Build Your Own Watchface
2. Pebble Developers // Graphics
3. Pebble Developers // Graphics Types
4. Pebble Developers // Detecting Acceleration
5. Pebble Developers // Timer
6. Pebble Developers // Measuring Battery Level
7. Pebble Developers // Managing Bluetooth Events
  • +17
  • 9,3k
  • 6
Поделиться публикацией

Комментарии 6

    0
    Спасибо, скрещу вашу статью с MyPebbleData (была тут статья от создателя, приложение забирающее данные в JSON с сервера, исходники есть) и сделаю себе шикарную часоморду.
      –1
      Пора им то же, но в форм-факторе настольных часов/будильника делать.

      Получится удобно, тем более с приличной часомордой и с MyPebbleData.
      0
      Было бы неплохо у стэндбай экрана сделать покрупнее шрифт и добавить возможность выключать секунды на подробном. А так отличный вотчфэйс.
        0
        Все никак не сяду за реализацию переключения экранов листанием watchface.
          0
          Уже давно такое реализовал в своём вотчфейсе.
          Реализация простая — при инициализации считывается булеанова переменная из памяти, если она истина, то один вид, если ложна, то другой, и перезаписывается значение в памяти на противоположное.
          Но как показала практика — данный метод не особо жизненный, ибо при открытии меню или уведомлений от сторонних нотификаторов при возврате на вотчфейс снова происходит его инициализация и вид меняется
            0
            Да я тоже так сделал, но листание циферблатов, если он один, не работает на последних прошивках. Пришлось разделять и делать два циферблата.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое