Все приложения для часов Pebble делятся на две категории watchapp — просто приложения, и watchface — приложения «часы», которые исходя из названия являются лицом устройства. Отличие «фейсов» — отсутствие реакции на хардварные кнопки, так как «UP» и «DOWN» используются для циклического переключения между установленными watchface.Но, наверное, в силу низкого разрешения экрана 144x168 px, найти органично вписывающийся в дизайн часов ватчфейс, который при этом выполняет основную свою функцию — отсчет времени, довольно непросто.
Как мне кажется лучше всего на таком экране смотрятся цифры в стиле семисегментных индикаторов.
Ниже, подробнее о том, как добавить в свой watchface элегантного минимализма, индивидуальности и уникальных фишек.
Итак, немного картинок, обрывков кода и в итоге ссылка на готовый проект.
Создание watchface, структура и построение проекта подробно описано в соответствующем разделе документации Build Your Own Watchface [1]. Не буду повторятся, а сразу перейду к
Особенности
Что будет отличать наше приложение от полутора десятков из Examples и от сотен с mypebblefaces:
- шрифт приближенный к 7-segment индикаторам;
- два экрана:
- основной «standby»-экран, экономичный и минималистичный, обновляется раз в минуту, показывает текущее время;
- дополнительный «info»-экран, расширенная информация: текущее время с секундами, дата, день недели, прогноз погоды (куда же без него).
- переключение между экранами жестом (встряхивание кистью);
- автоматический переход в «standby»;
- отображение состояния батареи и bluetooth-соединения.
Обо всем по порядку:
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
