Madrobots
Компания
82,36
рейтинг
8 декабря 2013 в 23:44

Разработка → Программируем под Pebble. Урок второй: Камешек, дающий ответы, игральные кости и секс-кубики tutorial

Часы — это конечно хорошо, но ими забит весь сайт. Надо сделать что-то более интересное.
image
Помните шарик из «трассы 60»? Давайте сделаем его аналог — приложение, дающее ответ на вопрос.


Шарик Камешек дающий ответы

Только у нас будет не шарик, а камешек, то есть Pebble :)
Что нам для этого надо? Список ответов и генератор случайных чисел. Список ответов мы возьмем на википедии. Создадим массив и заполним его ответами:
static const char* messages[] =  {"Бесспорно", "Это предрешено","Никаких сомнений","Определенно да","Можешь быть уверен в этом","Мне кажется - да","Вероятнее всего","Хорошие перспективы","Да","Знаки говорят - да","Пока не ясно, попробуй еще раз","Спроси позже","Лучше не рассказывать тебе это сейчас","Сейчас нельзя предсказать","Сконцентрируйся и спроси снова","Даже не думай","Мой ответ - нет","Знаки говорят - нет","Перспективы не очень хорошие","Весьма сомнительно","Нет",}; 
Порядок не важен, все равно они будут выбираться из него случайных образом.

Создаем главное окно программы и текстовый слой так же, как и в предыдущем уроке, за исключением небольшой разницы — функцией window_set_fullscreen() мы убираем верхний бар приложения. Вызывается она так:
 window_set_fullscreen(window, true);
Первый агрумент — имя окна, второй, соответственно true — полный экран, false — c баром. Тонкость — эта функция должна вызываться до window_stack_push, иначе чуда не произойдет.

Теперь займемся генератором случайных чисел. Для этого существует функция rand(), которая возвращает при каждом вызове случайное число. Как и любой программный ГСЧ, она нуждается в инициализации случайным числом перед началом работы, иначе строчка цифр будет повторяться при каждом запуске. Делается это функцией srand(). Например в SDK есть пример, где она инициализируется текущим временем. Так как мы не знает, в какое время программа запустится, и это время каждый раз разное — то это достаточный источник энтропии для нашей идеи. Делаем вот так:
srand(time(NULL)); 

Правда, генератор теперь может выдать довольно большое число — заведомо больше, чем у нас возможных вариантов ответов. Поэтому, при применяем вот такую конструкцию messages[rand()%21] . % — это остаток целочисленного деления. Допустим, генератор возвращает 456, они делятся на 21, а остаток(456-21*21) — 15 мы используем в качестве номера ответа. Он не может быть больше 21, потому что при этом остаток от деления будет равен нулю.

Объединяем все вышесказанное в одно целое, и у нас получается вот такой вызов функции:
text_layer_set_text(text_layer, messages[rand() % 21]);

Вот весь исходник:
#include "pebble.h"
    
Window *window; /* Создаем указатель на окно */
TextLayer *text_layer;  /* создаем  указатель на текстовый слой */

static const char* messages[] =  {"Бесспорно", "Это предрешено","Никаких сомнений","Определенно да","Можешь быть уверен в этом","Мне кажется - да","Вероятнее всего","Хорошие перспективы","Да","Знаки говорят - да","Пока не ясно, попробуй еще раз","Спроси позже","Лучше не рассказывать тебе это сейчас","Сейчас нельзя предсказать","Сконцентрируйся и спроси снова","Даже не думай","Мой ответ - нет","Знаки говорят - нет","Перспективы не очень хорошие","Весьма сомнительно","Нет",}; /* Создаем массив ответов */

int main(void) 
{
    window = window_create();  /* инициализируем окно */
    window_set_background_color(window, GColorBlack); /* устанавливаем фоновый цвет */
    window_set_fullscreen(window, true); /* включаем полноэкранность */
    window_stack_push(window, true);  /* открываем окно */
    text_layer = text_layer_create(GRect(1, 47, 142, 77)); /* инициализируем текстовый слой */
    text_layer_set_text_color(text_layer, GColorWhite);  /* устанавливаем цвет текста */
    text_layer_set_background_color(text_layer, GColorClear);  /* устанавливаем цвет фона */
    text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24)); /* устанавливаем шрифт */
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter); /* устанавливаем выравнивание по центру */
    layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));  /* подключаем текстовый слой к основному в качестве дочернего */
    srand(time(NULL)); /* инициализируем генератор случайных чисел текущем временем */
    text_layer_set_text(text_layer, messages[rand() % 21]); /* записываем в текстовый слой случайно выбранный ответ */

    app_event_loop();  /* ожидаем чего-нибудь */

    text_layer_destroy(text_layer); /* уничтожаем объекты, освобождаем ресурсы */
    window_destroy(window);  /* уничтожаем объекты, освобождаем ресурсы */
}

Программа выводит сообщение при запуске и больше не делает ничего. Для показа другого сообщения надо перезапустить программу:


Следующий шаг — сделаем вывод сообщения при нажатии на кнопку.

В API Pebble применяется термин «подписка» для назначения функций, которые будет запущены не в главном цикле, а после каких-то действий — тика таймера, нажатия кнопки и так далее. Помните, в прошлом уроке мы подписывались на таймер, который раз в секунду вызывал функцию обновления экрана? Тут нечто похожее, мы подписываемся на нажатие кнопки(причем в API уже есть несколько готовых вариантов — одиночное нажатие, двойное нажатие, удержание и так далее), после нажатия которой будет запущена заданная нами функция.

API говорит о том, что нам надо вызвать метод window_set_click_config_provider(), которая в качестве аргументов примет указатель на окно, в котором надо ловить нажатия, и название функции, в которой будет описаны подписки. Обратите внимание, подписываться на кнопки мы будет не тут, а только в функции, указанной в window_set_click_config_provider. Делается это вот так:
window_set_click_config_provider(window, WindowsClickConfigProvider);
Указатель окна — window, название функции — WindowsClickConfigProvider. Создаем ее:
void WindowsClickConfigProvider(void *context) { }

И добавляем внутрь методы window_single_click_subscribe, принимающие аргументами название кнопки и функцию, в которую будет передано управление:
window_single_click_subscribe(BUTTON_ID_UP, click);
В данном случае при нажатии кнопки вверх, мы вызываем click.
Кнопки(их у нас 3) называются соответственно BUTTON_ID_UP, BUTTON_ID_SELECT, BUTTON_ID_DOWN. Подписаться на кнопку «назад» можно, но она всегда будет выбрасывать вас на предыдущий экран(в случае, если у приложения один экран — в меню).

Теперь приложение выглядит вот так:


И его исходник:
#include "pebble.h"

Window *window; /* Создаем указатель на окно */
TextLayer *text_layer;  /* создаем  указатель на текстовый слой */

static const char* messages[] = {"Бесспорно", "Это предрешено","Никаких сомнений","Определенно - да","Будь уверен в этом","Мне кажется - да","Вероятнее всего","Хорошие перспективы","Да","Знаки говорят - да","Пока не ясно, попробуй еще раз","Спроси позже","Лучше не рассказывать тебе это сейчас","Сейчас нельзя предсказать","Сконцентрируйся и спроси снова","Даже не думай","Мой ответ - нет","Знаки говорят - нет","Перспективы не очень хорошие","Весьма сомнительно","Нет",}; /* Создаем массив ответов */

void click(ClickRecognizerRef recognizer, void *context)  /* эта функция запустится при клике на кнопку  */
{
    text_layer_set_text(text_layer, messages[rand() % 21]); /* записываем в текстовый слой случайно выбранный ответ */
}

void WindowsClickConfigProvider(void *context)  /* функция, внутри которой должны находиться подписки на кнопки */
{
    window_single_click_subscribe(BUTTON_ID_UP, click); /* при нажатии на верхнюю кнопку запустить click */
    window_single_click_subscribe(BUTTON_ID_SELECT, click); 
    window_single_click_subscribe(BUTTON_ID_DOWN, click); 
}
    
int main(void)
{
    window = window_create();  /* Инициализируем окно */
    window_set_background_color(window, GColorBlack); /* устанавливаем фоновый цвет */
    window_set_fullscreen(window, true); /* включаем полноэкранность */
    window_stack_push(window, true);  /* открываем окно */
    text_layer = text_layer_create(GRect(0, 7, 144, 168)); /* инициализируем текстовый слой */
    text_layer_set_text_color(text_layer, GColorWhite);  /* устанавливаем цвет текста */
    text_layer_set_background_color(text_layer, GColorClear);  /* устанавливаем цвет фона */
    text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24)); /* устанавливаем шрифт */
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter); /* устанавливаем выравнивание по центру */
    layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));  /* подключаем текстовый слой к основному в качестве дочернего */
    srand(time(NULL)); /* инициализируем генератор случайных чисел текущем временем */
    window_set_click_config_provider(window, WindowsClickConfigProvider); /* определяем функцию, в которой будут находиться подписки на кнопки */
    
    text_layer_set_text(text_layer, "Magic Pebble \n Задай вопрос, на который можно ответить \"да\" или \"нет\" и нажми на кнопку");  /* Выводим сообщение при запуске */
    
    app_event_loop();  /* ждем событий */
    
    text_layer_destroy(text_layer); /* уничтожаем объекты, освобождаем ресурсы */
    window_destroy(window);  /* уничтожаем объекты, освобождаем ресурсы */
}



Работает! Можно присваивать версию 0.1 :)
Но чего-то не хватает. Хорошо бы добавить каких-нибудь эффектов. Можно сделать анимацию изменения ответов, чтобы часы отвечали не сразу при нажатии на кнопку, а несколько секунд «перебирали» варианты. В кавычках — потому что ответ будет случайным в любом случае, но в первом случае у пользователя будет больше доверия к приложению — все мы знакомы с случайным процессом выбора карточек из мешка или шаров с цифрами при игре в лотерею.

Еще более красиво будет, если мы добавим немного физики. Дело в том, что люди, сами того не подозревая, окружены процессами протекающими по обратной экспоненте — когда процессы «быстрые» в самом начале, проходят не линейно, а теряя скорость тем сильнее, чем больше времени прошло с начала.
Это может быть что угодно — например, остывание горячего чая на столе. Когда разница температур чая и окружающего воздуха велика, он остывает гораздо быстрее, чем когда он чуть теплый. Или пузырьки в стакане с газированной водой. Сначала, когда концентрация угольной кислоты в воде велика, она разлагается очень активно, но через некоторое время концентрация уменьшается, а вместе с ней уменьшается и количество пузырьков.
В данном случае я бы хотел сделать что-то вроде колеса фортуны — там, где сначала сегменты сменяют друг друга быстро, постепенно замедляясь. Тут «виновато» трение — чем больше скорость, тем больше колесо трется об ось и тем больше теряет энергию, переводя ее в тепло и замедляясь. Но при уменьшении скорости уменьшаются потери и на трение — в итоге 50% своей скорости колесо теряет в первые 20% времени, а остальные 50% — за оставшиеся 70%.

Отсчитывать время мы будем в миллисекундах между сменами сообщений. Путем опытов, было выяснено, что хороший диапазон значений — от 0 до 300-400мс. Если промежутки больше — возникает ощущение, что уже показанный ответ внезапно поменялся.
Наиболее простая функция, которая обеспечит такое поведение — это что-то типа x=x*2. Но при умножении на 2 график функции поднимается слишком резко, каждую итерацию время задержки увеличивается в два раза. Мы получим 256(максимальную задержку, поскольку следующее значение — 512 уже выходит за границы удобства) уже на 8 шаге с длительностью работы всего в 1+2+4+8+16+32+64+128+256=511мс. Пол-секунды это слишком быстро.
Опытным путем я понял, что множитель должен быть в районе 1.08-1.2, тогда мы получаем около 30 шагов и длительность около 3 секунд. Но мне не нравится функция x=x*y — она слишком полого поднимается, и мелькание замедляется слишком медленно(да, я странный), хотелось бы, чтобы оно дольше мелькало «быстро», а потом резко остановилось.

Можно было поступить проще — забить значение задержек в массив и втирать править, править их до удовлетворения, благо их не так и много — штук 40. Но гораздо интереснее вывести функцию, которая бы работала нужным нам образом. Раз умножение нам не подходит, попробуем деление. Что-то вроде x=x/0.7. Но сама по себе она слишком резко возрастает — 300 мы получаем уже на 18 шаге, а хотелось бы чуть больше. Но можно разделить например, на 100. Или на 1000. Построим графики всех функций при начальном значении 1. Цифры над графиками — количество шагов.

Вот, значит нам вполне подходит y=y/0.7 x=y/100, можно делать.

Но для начала — исправим один глюк: т.к. информация при запуске и сообщения выводятся у нас в одном и том же слое, то сообщения показываются в верхней части экрана(выравнивания по вертикали, к сожалению, нет). Да еще и мелким шрифтом, хотя их можно сделать и больше — они гораздо короче и поместятся в экран. Я не нашел в API методов изменения размеров слоя, поэтому нам придется удалить слой и создать его опять, но с другими координатами. Однако, при этом получится некрасиво — часть кода будет дублироваться(настройки слоя после его создания), чтобы этого не происходило — вынесем часть кода в отдельную функцию.
Создаем:
void config_text_layer(int16_t x,int16_t y,int16_t h,int16_t w, const char *font_key) {
}

Первые 4 параметра — это информация о слое — координаты точки и размеры, пятый параметр — это шрифт. Соответственно, перепишем функции инициализации для использования этих переменных:
text_layer = text_layer_create(GRect(x, y, h, w));
text_layer_set_font(text_layer, fonts_get_system_font(font_key)); 

И вставим их в нашу функцию вместе с остальным кодом настройки слоя. Теперь можно просто сделать в нужном месте вот так:
text_layer_destroy(text_layer); 
config_text_layer(5, 40, 134, 120, FONT_KEY_GOTHIC_28); 

Так и сделаем в функции click, которая вызывается у нас при нажатии кнопки. Да, слой будет создаваться каждый раз при нажатии кнопки, избежать этого можно простой проверкой, что-то типа:
bool first_time=true;

if (first_time == true)
{
text_layer_destroy(text_layer); 
config_text_layer(5, 40, 134, 120, FONT_KEY_GOTHIC_28); 
first_time = false;
}

Но я не стал заморачиваться. Лучше займемся таймером. Поиск в API дал функцию app_timer_register(), которая принимает в качестве аргументов значение в мс, через которое сработает таймер, имя функции, которую надо вызвать при его срабатывании и указатель на данные, которые надо передать в эту функцию. К сожалению, как работать с указателем я не разобрался, так что придется плюнуть на красоту кода и сделать через глобальную переменную:
float timer_delay=1; 


Создаем функцию, которая будет рекурсивно вызывать таймер со все возрастающими интервалами.

void timer_call() /* эта функция вызывается при срабатываниии таймера */
{
    text_layer_set_text(text_layer, messages[rand() % 21]); /* выводим случайное сообщение */
    if (timer_delay < 300*100 ) /* если задержка еще не достигла 300мс... */
    {
        timer_delay=timer_delay/0.7; /* ...увеличиваем задержку... */
        app_timer_register(timer_delay/100, timer_call, NULL); /* ...и взводим таймер заново */
    }
    else /* если задержка уже больше 300мс... */
    {
        timer_delay=1; /* сбрасываем таймер на начало и выходим - сообщение же уже вывели */
    }

}

Центром функции является переменная с плавающей точкой timer_delay. Она делится на 0.7, пока не достигнет 30000. Каждое новое значение переменной делится на 100 и отдается в качестве аргумента задержки функции app_timer_register, которая при срабатывании опять вызовет эту функцию.

Осталось только добавить вызов этой функции в click():
void click(ClickRecognizerRef recognizer, void *context)  /* функция, срабатывающая при клике на кнопки */
{
    text_layer_destroy(text_layer); /* очищаем и удаляем старый слой */
    config_text_layer(5, 40, 134, 120, FONT_KEY_GOTHIC_28); /* создаем новый слой с другими координатами и шрифтом */
    timer_call(); /* взводим таймер для быстрой смены сообщений */

}

И можно наслаждаться результатом:

Если немного подумать, что у нас получилось, то окажется, что мы создали платформу для симуляции любых штук, которые используются в реальном мире как ГСЧ. Сложное движение многогранной фигуры в жидкости внутри шарика — процесс, не поддающийся предсказанию. Так же как и игральные кости. Кстати, игральные кости! Чем не вариант для еще одного приложения?
Я действительно полез искать рисунки на гранях костей и наткнулся на… нет, ну это тоже рисунки. И тоже на игральных костях. Только не точечками. В общем, я наткнулся вот на это:


И все. Какие там игральные кости, когда тут есть такая замечательная идея. Пошли реализовывать!

Секс-рулетка

Логика работы программы остается прежней. Меняем массив messages:
static const char* messages[] = {"В ванной","На кухне","На полу","На кровати","В туалете","В коридоре","В гостях","На балконе","В шкафу","В ванной","В лифте","На улице","В машине","В воде","В общественном туалете","При свечах","В спальне","В гостиной","В примерочной кабинке","В кинотеатре","Перед камерой","На пляже","В чужом доме",}; 

И сообщение при запуске:
text_layer_set_text(text_layer, "Sex Roulette \n Нажми на любую кнопку, чтобы выбрать позу и место -->");  

Создаем в памяти, на которую указывает указатель графический массив, указывая координаты. Синтаксис абсолютно такой же, как и в text_layer_create:
image_layer = bitmap_layer_create(GRect(0 , 0, 144, 144));

Делаем созданный слой ребенком слоя главного окна. Зачем это нужно? Это настраивает «высоту»(в терминах css, если кому понятнее — z-index) слоя по отношению к другим слоям, от этого зависит то, как слои будут друг друга перекрывать. Все дети находятся выше всего родителя и закрывают его своим выводом.
layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(image_layer));

Настраиваем режим наложения. В зависимости от этого параметра картинка будет по разному взаимодействовать с фоном.
bitmap_layer_set_compositing_mode(image_layer, GCompOpAssignInverted);

Все варианты можно описаны тут, или можно повтыкать в картинку:
image
Source — это в данном случае графический слой, а Destination — его родитель, главный слой окна. Как все просто и замечательно укладывается в 6 вариантов при работе с монохромными картинками…

Теперь займемся выводом рандомных картинок. Поищем в гугле подходящие картинки(двухцветные и маленькие) по какому-нибудь запросу типа "иконки позы".
На этом сайте нашлось вот такое:

Скачиваем вектор, удаляем всякие границы, экспортируем в PNG разрешением повыше, открываем в каком-нибудь графическом редакторе.
Выделяем каждую картинку, удаляем неудачные и непонятные, вписываем ее в квадрат, уменьшаем до 144 пикселей, подчищаем недостатки. Итогом нашей работы должна стать папка с картинками:

Из одной из картинок делаем логотип для меню, размером 24x28 пикселей:
->

Открываем файл appinfo.json в папке с проектом и добавляем в него наши созданные ресурсы. К сожалению, не могу рассказать как это делается в CloudPebble — она внезапно перестала у меня работать, не принимая пароль и зажав исходники всех моих проектов. Так что я поставил себе среду разработки на компьютер по инструкции с официального сайта, и пишу там.

Так вот, о appinfo.json. Он выглядит вот так:
{
  "uuid": "0f7b8336-d72d-4806-9191-566ffd8f3a8c",
  "shortName": "SexRoulette",
  "longName": "SexRoulette",
  "companyName": "vvzvlad",
  "versionCode": 1,
  "versionLabel": "1.0.0",
  "watchapp": {
    "watchface": false
  },
  "appKeys": {
    "dummy": 0
  },
  "resources": {
 "media": [
    {
        "menuIcon": true,
        "type": "png",
        "name": "DEFAULT_MENU_ICON",
        "file": "img/logo.png"
    },
    {
        "type": "png",
        "name": "POSE_1",
        "file": "img/1.png"
    },
В начале идут знакомые вам поля, смысл которых понятен и без объяснений, а дальше в нем описываются все 32 картинки. Обратите внимание, структура должна быть именно вот такая:

    {
        "type": "png",
        "name": "Первая картиника",
        "file": "img/1.png"
    },
    {
        "type": "png",
        "name": "Последняя картиника",
        "file": "img/1.png"
    }

После последней скобки не должно быть запятой, как и после указания адреса файла. А поля должны следовать именно в порядке type, name, file.
Что, блин, идет вразрез с официальной документацией:

Клавиатуру в жопу запихать! Посылаю лучики поноса тем, кто писал эту документацию, я полчаса тупил и гадал, на что же он ругается. Еще и ругается так невнятно, что толком не поймешь, что его не устраивает:
Traceback (most recent call last):
  File "/Users/vvzvlad/Documents/PebbleSDK-2.0-BETA2/Pebble/.waf-1.7.11-478a7e7e9ed077304a8092741524bf8b/waflib/Scripting.py", line 351, in execute
    return execute_method(self)
  File "/Users/vvzvlad/Documents/PebbleSDK-2.0-BETA2/Pebble/.waf-1.7.11-478a7e7e9ed077304a8092741524bf8b/waflib/Build.py", line 106, in execute
    self.execute_build()
  File "/Users/vvzvlad/Documents/PebbleSDK-2.0-BETA2/Pebble/.waf-1.7.11-478a7e7e9ed077304a8092741524bf8b/waflib/Build.py", line 109, in execute_build
    self.recurse([self.run_dir])
  File "/Users/vvzvlad/Documents/PebbleSDK-2.0-BETA2/Pebble/.waf-1.7.11-478a7e7e9ed077304a8092741524bf8b/waflib/Context.py", line 128, in recurse
    user_function(self)
  File "/Users/vvzvlad/Documents/PebbleSDK-2.0-BETA2/SexRoulette/wscript", line 18, in build
    ctx.load('pebble_sdk')
Ну ладно, хрен с ним. Как вы уже поняли, картинка-иконка для меню помечается полем «menuIcon»: true, причем к ней тоже можно обращаться из программы.

Вернемся к нашим картинкам. Документация предлагает загружать картинку вот так:
image = gbitmap_create_with_resource(RESOURCE_ID_POSE_1);

Что для нас не подходит. Не городить же монструозную конструкцию вида
select rand() % 31
case 1 
image = gbitmap_create_with_resource(RESOURCE_ID_POSE_1);
case 2
image = gbitmap_create_with_resource(RESOURCE_ID_POSE_2);

Это некрасиво. Из документации мы понимаем, что RESOURCE_ID_POSE_2 — это всего лишь переменная типа uint32_t в которой хранится номер ресурса.

Создаем массив нужного нам типа:
static const uint32_t images[]

И перечисляем в нем нужные нам ресурсы:
static const uint32_t images[] = {RESOURCE_ID_POSE_1,RESOURCE_ID_POSE_2,RESOURCE_ID_POSE_3,RESOURCE_ID_POSE_4,RESOURCE_ID_POSE_5,RESOURCE_ID_POSE_6,RESOURCE_ID_POSE_7,RESOURCE_ID_POSE_8,RESOURCE_ID_POSE_9,RESOURCE_ID_POSE_10,RESOURCE_ID_POSE_11,RESOURCE_ID_POSE_12,RESOURCE_ID_POSE_13,RESOURCE_ID_POSE_14,RESOURCE_ID_POSE_15,RESOURCE_ID_POSE_16,RESOURCE_ID_POSE_17,RESOURCE_ID_POSE_18,RESOURCE_ID_POSE_19,RESOURCE_ID_POSE_20,RESOURCE_ID_POSE_21,RESOURCE_ID_POSE_22,RESOURCE_ID_POSE_23,RESOURCE_ID_POSE_24,RESOURCE_ID_POSE_25,RESOURCE_ID_POSE_26,RESOURCE_ID_POSE_27,RESOURCE_ID_POSE_28,RESOURCE_ID_POSE_29,RESOURCE_ID_POSE_30,RESOURCE_ID_POSE_31,RESOURCE_ID_POSE_32,};

Теперь мы можем вызвать случайную картинку так же, как и текст:
image = gbitmap_create_with_resource(images[rand() % 31]);

Мы вызвали картинку из небытия из флеш-памяти и разместили ее в оперативке. Теперь надо передать указатель на место хранение изображения функции bitmap_layer_set_bitmap, которая и отобразит ее на нужном нам графическом слое.
Делается это вот так:
bitmap_layer_set_bitmap(image_layer, image); 

image_layer — это указатель на кусок памяти графического слоя, а image — указатель на картинку в памяти.
Вроде все. Запускаем!

Часы зависли, потом перезагрузились, а после второго запуска ушли в Recovery и попросили перепрошиться.
Правильно, мы, загружая в каждом цикле картинку в память, не выгрузили ее оттуда. Забили оперативку, и залезли куда-то еще, судя по тому, что часы отказались загружаться.
Это spaaarta! embeeeeedded! Тут такое не прощают.
Делаем правильно. Сначала очищаем память, а потом загружаем в нее новую картинку для показа.
gbitmap_destroy(image); 
image = gbitmap_create_with_resource(images[rand() % 31]); 
bitmap_layer_set_bitmap(image_layer, image); 

Но при самом первом клике у нас в указателе image ничего нет, и gbitmap_destroy обязательно это обнаружит. App Crashed…
Можно во время инициализации программы подсунуть ему туда картинку, чтобы было что удалять, но это как-то некрасиво. Похоже, без флага первого запуска нам все-таки не обойтись. Создаем переменную:
bool first_time=true;

Сбрасываем ее при первом запуске функции, вызываемой таймером:
void timer_call() {
    first_time = false;


И оборачиваем gbitmap_destroy — в проверку на first_time == false, а text_layer_destroy и config_text_layer в функции click — соответственно, в проверку на first_time == true:
void timer_call() {
    if (first_time == false) {
        gbitmap_destroy(image);  
        bitmap_layer_destroy(image_layer); 
}
    first_time = false;
.....

void click(ClickRecognizerRef recognizer, void *context)  {
  if (first_time == true)  {
    text_layer_destroy(text_layer); 
    config_text_layer(0, 146, 144, 168-146, FONT_KEY_GOTHIC_18);  }

Работает!


Нет, стоп. Посмотрим внимательно на последние строчки нашей программы:
    text_layer_destroy(text_layer); 
    window_destroy(window);  
    gbitmap_destroy(image);
    bitmap_layer_destroy(image_layer); 

В числе прочих мы уничтожаем gbitmap. Но если мы запустим программу и тут же выйдем, он не будет создан! Уничтожать нам еще нечего, и при выполнении этой функции программа упадает и потянет за собой лаунчер — часы перезагрузятся. А это нехорошо. Раз уж у нас есть флаг первого включения — можно использовать его, проверяя перед уничтожением ресурсов:
if (first_time == false) 
{
gbitmap_destroy(image); 
}

Вот теперь точно все.
Полный исходник программы
#include "pebble.h"

float timer_delay=1; /* Создаем переменную для с временем для таймера */
Window *window; /* Создаем указатель на окно */
TextLayer *text_layer;  /* создаем  указатель на текстовый слой */
static BitmapLayer *image_layer; /* создаем  указатель на графический слой */
static GBitmap *image; /* создаем  указатель на изображение в памяти */
bool first_time=true; /* создаем флаг первого запуска */

static const char* messages[] = {"В ванной","На кухне","На полу","На кровати","В туалете","В коридоре","В гостях","На балконе","В шкафу","В ванной","В лифте","На улице","В машине","В воде","В общественном туалете","При свечах","В спальне","В гостиной","В примерочной кабинке","В кинотеатре","Перед камерой","На пляже","В чужом доме",}; /* Создаем массив ответов */

static const uint32_t images[] = {RESOURCE_ID_POSE_1,RESOURCE_ID_POSE_2,RESOURCE_ID_POSE_3,RESOURCE_ID_POSE_4,RESOURCE_ID_POSE_5,RESOURCE_ID_POSE_6,RESOURCE_ID_POSE_7,RESOURCE_ID_POSE_8,RESOURCE_ID_POSE_9,RESOURCE_ID_POSE_10,RESOURCE_ID_POSE_11,RESOURCE_ID_POSE_12,RESOURCE_ID_POSE_13,RESOURCE_ID_POSE_14,RESOURCE_ID_POSE_15,RESOURCE_ID_POSE_16,RESOURCE_ID_POSE_17,RESOURCE_ID_POSE_18,RESOURCE_ID_POSE_19,RESOURCE_ID_POSE_20,RESOURCE_ID_POSE_21,RESOURCE_ID_POSE_22,RESOURCE_ID_POSE_23,RESOURCE_ID_POSE_24,RESOURCE_ID_POSE_25,RESOURCE_ID_POSE_26,RESOURCE_ID_POSE_27,RESOURCE_ID_POSE_28,RESOURCE_ID_POSE_29,RESOURCE_ID_POSE_30,RESOURCE_ID_POSE_31,RESOURCE_ID_POSE_32,}; /* Создаем массив идентификаторов ресурсов */
 

void timer_call() /* эта функция вызывается при срабатывании таймера */
{
    if (first_time == false) /* если запускается не в первый раз... */
      {
        bitmap_layer_destroy(image_layer); /* ...то удаляем старый слой... */
        gbitmap_destroy(image); /* ..и очищаем память от предыдущей картинки */
      }
    first_time = false; /* сбрасываем флаг первого запуска */
    image = gbitmap_create_with_resource(images[rand() % 31]); /* загружаем в память случайную картинку из подключенных ресурсов */
    bitmap_layer_set_bitmap(image_layer, image); /* выводим загруженную картинку в слой */
    text_layer_set_text(text_layer, messages[rand() % 23]); /* выводим случайное сообщение */
    if (timer_delay < 300*100 ) /* если задержка еще не достигла 300мс... */
    {
        timer_delay=timer_delay/0.7; /* ...увеличиваем задержку... */
        app_timer_register(timer_delay/100, timer_call, NULL); /* ...и взводим таймер заново */
    }
    else /* если задержка уже больше 300мс... */
    {
        timer_delay=1; /* сбрасываем таймер на начало и выходим - сообщение и картинку мы же уже показали */
    }
}

void config_text_layer(int16_t x,int16_t y,int16_t h,int16_t w, const char *font_key)  /* для исключения дублирования кода, создали функцию, которая занимается инициализаций и настройкой текстового массива*/
{
    text_layer = text_layer_create(GRect(x, y, h, w)); /* создаем текстовый массив, указываем размер и координаты */
    text_layer_set_text_color(text_layer, GColorWhite);  /* устанавливаем цвет текста */
    text_layer_set_background_color(text_layer, GColorClear);  /* устанавливаем цвет фона */
    text_layer_set_font(text_layer, fonts_get_system_font(font_key)); /* устанавливаем шрифт */
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter); /* устанавливаем выравнивание по центру */
    layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));  /* подключаем текстовый слой к основному в качестве дочернего */
}

void click(ClickRecognizerRef recognizer, void *context)  /* функция, срабатывающая при клике на кнопки */
{
  if (first_time == true) /* если программа запускается в первый раз... */
  {
    text_layer_destroy(text_layer); /* ... то очищаем и удаляем приветствия... */
    config_text_layer(0, 146, 144, 168-146, FONT_KEY_GOTHIC_18); /* ... и создаем новый слой сообщений с другими координатами и шрифтом */
  }
    timer_call(); /* взводим таймер для быстрой смены сообщений */
}

void WindowsClickConfigProvider(void *context)  /* функция, внутри которой должны находиться подписки на кнопки */
{
    window_single_click_subscribe(BUTTON_ID_UP, click); /* при нажатии на верхнюю кнопку запустить click */
    window_single_click_subscribe(BUTTON_ID_SELECT, click); 
    window_single_click_subscribe(BUTTON_ID_DOWN, click); 
}

int main(void)
{
    window = window_create();  /* Инициализируем окно */
    window_set_background_color(window, GColorBlack); /* устанавливаем фоновый цвет */
    window_set_fullscreen(window, true); /* включаем полный экран */
    window_stack_push(window, true);  /* открываем окно */
    srand(time(NULL)); /* инициализируем генератор случайных чисел текущем временем */
    window_set_click_config_provider(window, WindowsClickConfigProvider); /* определяем функцию, в которой будут находиться подписки на кнопки */
    config_text_layer(0, 20, 144, 168, FONT_KEY_GOTHIC_24); /* настраиваем создание текстового слоя с приветственным сообщением */
    text_layer_set_text(text_layer, "Sex Roulette \n Нажми на любую кнопку, чтобы выбрать позу и место -->");  /* показываем сообщение при запуске */
    image_layer = bitmap_layer_create(GRect(0 , 0, 144, 144)); /* создаем графический массив, указываем размер и координаты */
    layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(image_layer)); /* подключаем графический слой к основному в качестве дочернего */
    bitmap_layer_set_compositing_mode(image_layer, GCompOpAssignInverted); /* настраиваем параметр наложения */
    app_event_loop();  /* ждем событий */
    text_layer_destroy(text_layer); /* уничтожаем текстовый слой, освобождаем ресурсы */
    window_destroy(window);  /* уничтожаем главное окно, освобождаем ресурсы */
    bitmap_layer_destroy(image_layer); /* уничтожаем графический слой, освобождаем ресурсы */
    if (first_time == false) /* если мы выходим после запуска рисования... */
    {
    gbitmap_destroy(image); /* то уничтожаем массив с графикой, освобождаем ресурсы, иначе - не уничтожаем, т.к. он еще не создан */
    }
}
А игральные кости?! Придется сделать.

Игральные кости

Берем за основу предыдущую программу. Картинки мы возьмем из википедии. Уменьшим их до размера 75х75 пикселей и сделаем иконку:

Подключим их в appinfo.json, так же как и в предыдущем примере, и опишем их в массиве images:
static const uint32_t images[] = {RESOURCE_ID_DICE_1,RESOURCE_ID_DICE_2,RESOURCE_ID_DICE_3,RESOURCE_ID_DICE_4,RESOURCE_ID_DICE_5,RESOURCE_ID_DICE_6,}; 

Удалим из программы обработку нажатий: WindowsClickConfigProvider и click. Это же кубики, их надо трясти! Будем использовать акселерометр.

Подписываемся на события от акселерометра:
accel_tap_service_subscribe(accel_int);

Создаем функцию, которая будет вызываться при встряхивании часов:
void accel_int(AccelAxisType axis, int32_t direction)
{
    if (first_time == true)
    { 
        text_layer_destroy(text_layer); 
    }
    timer_call(); 
}

Логика ее работы проста: если у нас на экране еще есть текстовый слой(мы встряхиваем часы в первый раз после запуска) — удаляем его. Если нет — то не удаляем :) И в любом случае запускаем перебор.

Теперь изменяем функцию timer_call. Картинка у нас меньше экрана(я пробовал делать картинку кубика такой же, как в прошлом разе — 144х144, но смотрелось это не очень), поэтому нам ее надо двигать по экрану. Двигать мы будем простым способом — удаляя и создавая новую в случайном месте. Используем для этого мы опять любимую функцию rand. У нас есть картинка 75х75 пикселей и экран 144х168. Так как мы указываем при создании слоя его верхний левый угол, то, чтобы картинка не попала за край экрана, нам надо указать его в диапазоне 0...69(144-75) для х-координаты, и 0...93(168-75) для y-координаты. Делаем это вот так: rand()%(144-75) и rand()%(168-75). В итоге создание слоя выглядит вот так:
image_layer = bitmap_layer_create(GRect(rand()%(144-75), rand()%(168-75), 75, 75));


Еще меняем способ расчета задержки:
if (timer_delay < 300*1000 ) 
{
timer_delay=timer_delay/0.7; 
app_timer_register(timer_delay/1000, timer_call, NULL); 
}
Если в прошлом варианте у нас был множитель 100, то в этом — 1000. Это опять же сделано из соображений эстетики. Как мы видим из графика, такой множитель почти не изменяет скорость возрастая функции, но добавляет немного шагов. Дело в том, что во время движения рукой — начало смены картинок плохо видно, и чтобы увидеть то, ради чего затевалась эпопея с таймером — красивую смену картинок, надо немного продлить работу функции.

Дальше все так же, как и в предыдущем примере. Вот только небольшая проблема: так как мы не нажимаем кнопку, подсветка не загорается. Хорошо бы ее зажечь после движения. Смотрим, что нам предоставляет API для этого:
light_enable(bool enable)
light_enable_interaction()

Не густо. Но нам хватит. Первая функция может зажечь или погасить подсветку постоянно: light_enable(true) или light_enable(false), а вторая — зажигает подсветку, и она сама гасится через некоторое время(как при нажатии на кнопку). Чтобы не городить отдельный таймер для отключения подсветки(мы же не хотим, чтобы она горела постоянно при работе программы?), воспользуемся вторым вариантом. Поместим его куда-нибудь в конец timer_call.
До кучи — в main поправим сообщение на начальном экране:
text_layer_set_text(text_layer, "Встряхните часы для броска костей"); 

И при выходе из программы поменяем логику удалениия ресурсов и добавим отписку от акселерометра:
if (first_time == true) 
{ 
text_layer_destroy(text_layer); 
}
else 
{    
bitmap_layer_destroy(image_layer); 
gbitmap_destroy(image); 
}
accel_tap_service_unsubscribe();  
Тут мы делаем вот что. Если выходим сразу после запуску — с начального экрана, то нам надо удалить только текстовый слой. Если мы выходим уже после запуска — после того, как часы потрясли — то нам надо удалить графический слой и память под картинку, а текстовый слой не надо — он уже удален при срабатывании таймера в функции accel_int

В итоге код приобретает следующий вид
#include "pebble.h"

float timer_delay=1; /* Создаем переменную для с временем для таймера */
Window *window; /* Создаем указатель на окно */
TextLayer *text_layer;  /* создаем  указатель на текстовый слой */
static BitmapLayer *image_layer; /* создаем  указатель на графический слой */
static GBitmap *image; /* создаем  указатель на изображение в памяти */
bool first_time=true; /* создаем флаг первого запуска */
static const uint32_t images[] = {RESOURCE_ID_DICE_1,RESOURCE_ID_DICE_2,RESOURCE_ID_DICE_3,RESOURCE_ID_DICE_4,RESOURCE_ID_DICE_5,RESOURCE_ID_DICE_6,}; /* создаем массив с номерами картинок */

void timer_call() /* эта функция вызывается при срабатываниии таймера и при первом запуске перебора */
{
    if (first_time == false)/* если запускается не в первый раз... */
    { 
        bitmap_layer_destroy(image_layer); /* ...то удаляем старый слой, чтобы он не мешался позади... */
        gbitmap_destroy(image); /* ...и очищаем память от предыдущей картинки */
    }
    first_time = false; /* сбрасываем флаг первого запуска */
    image_layer = bitmap_layer_create(GRect(rand()%(144-75), rand()%(168-75), 75, 75));  /* создаем слой со случайными координатами, но в пределах экрана */
    layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(image_layer)); /* делаем его дочерним */
    image = gbitmap_create_with_resource(images[rand() % 6]); /* загружаем в память случайную картинку из подключенных ресурсов */
    bitmap_layer_set_bitmap(image_layer, image); /* выводим загруженную картинку в слой */
    light_enable_interaction(); /* включаем подсветку */
    if (timer_delay < 300*1000 ) /* если задержка еще не достигла 300мс... */
    {
        timer_delay=timer_delay/0.7; /* ...увеличиваем задержку... */
        app_timer_register(timer_delay/1000, timer_call, NULL); /* ...и взводим таймер заново */
    }
    else /* если задержка уже больше 300мс... */
    {
        timer_delay=1; /* сбрасываем таймер на начало и выходим - сообщение и картинку мы же уже показали */
    }
}

void accel_int(AccelAxisType axis, int32_t direction) /* при поступлении прерывания от акселерометра... */
{
    if (first_time == true) /* если это первый запуск... */
    { 
        text_layer_destroy(text_layer); /* ...то удаляем текстовый слой с информацией */
    }
    timer_call(); /* запускаем перебор */
}

int main(void)
{
    window = window_create();  /* Инициализируем окно */
    window_set_background_color(window, GColorBlack); /* устанавливаем фоновый цвет */
    window_set_fullscreen(window, true); /* включаем полноэкранность */
    window_stack_push(window, true);  /* открываем окно с анимацией */
    srand(time(NULL)); /* инициализируем генератор случайных чисел текущем временем */
    text_layer = text_layer_create(GRect(0 , 30, 144, 168)); /* создаем текстовый массив, указываем размер и координаты */
    text_layer_set_text_color(text_layer, GColorWhite);  /* устанавливаем цвет текста */
    text_layer_set_background_color(text_layer, GColorClear);  /* устанавливаем цвет фона */
    text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_28)); /* устанавливаем шрифт */
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter); /* устанавливаем выравнивание по центру */
    layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));  /* подключаем текстовый слой к основному в качестве дочернего */
    text_layer_set_text(text_layer, "Встряхните часы для броска костей");  /* показываем сообщение при запуске */
    accel_tap_service_subscribe(accel_int);  /* подписываемся на прерывания от акселерометра */
    app_event_loop();  /* ждем событий */
    if (first_time == true) /* если выходим без запуска перебора... */
    { 
        text_layer_destroy(text_layer); /* ...то удаляем текстовый слой с сообщением */
    }
    else  /* если выходим уже после запуска... */
    {    
        bitmap_layer_destroy(image_layer); /* ...то уничтожаем текстовый слой... */
        gbitmap_destroy(image); /* ... и уничтожаем массив с графикой, текстовый слой уже удален в функции accel_int */
    }
    accel_tap_service_unsubscribe();  /* отписываемся от прерываний акселерометра */
    window_destroy(window);  /* уничтожаем главное окно, освобождаем ресурсы */
}


Работает!


Видео, в котором показана работа всех программ:


Ссылки

image Скачать приложение MagicPebble с сайта mypebblefaces.com
image Скачать приложение Секс-рулетка с сайта mypebblefaces.com
image Скачать приложение Игральные кости с сайта mypebblefaces.com
image Исходники приложения MagicPebble на github.com
image Исходники приложения Секс-рулетка на github.com
image Исходники приложения Игральные кости на github.com
image Магазин, в котором я покупал Pebble Пожалуйста, не сритесь насчет цены. Это и так одна из самых низких цен у нас в наличии(забейте в гугле «купить pebble в москве»). Я же не заставляю вас там покупать, кто хочет — может заказывать с официального сайта.


Продолжать?

Проголосовал 721 человек. Воздержалось 150 человек.

GIF для демонстрации?

Проголосовало 868 человек. Воздержалось 114 человек.

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

Автор: @vvzvlad
Madrobots
рейтинг 82,36
Компания прекратила активность на сайте

Комментарии (28)

  • +4
    Глядя на
    srand(time(NULL)); 
    
    и
    messages[rand()%20] 
    

    не могу удержаться от ссылки: rand() Considered Harmful
    • +3
      Ну и
      if (first_time == true)
      

      улыбнуло
      • 0
        не могу удержаться от ссылки: rand() Considered Harmful

        Я оттуда понял только то, что так делать плохо. А как хорошо? Я не понял тот пример, который показывался в конце.

        Ну и if (first_time == true) улыбнуло

        Почему? Потому что можно написать if (first_time)?
        • 0
          Хорошо делать через классы заголовка random из C++11. Правда мой комментарий получается немного мимо кассы, т.к. у вас C, да и вообще для такой задачи ничего страшного в небольшой неравномерности распределения нет.

          Да, не понимаю зачем сравнивать булевские значения на равенство, на мой взгляд и длиннее и менее читаемо чем использовать их напрямую.
          • 0
            Хорошо делать через классы заголовка random из C++11. Правда мой комментарий получается немного мимо кассы, т.к. у вас C, да и вообще для такой задачи ничего страшного в небольшой неравномерности распределения нет.

            Может лучше тогда так сделать? rand()/(65535/21)
            • 0
              В указанном вами коде получается совсем печально. С вероятностью около 0.5% будет получаться 21 на выходе — наверняка это не было запланировано.

              В видео кстати похожий пример был рассмотрен. рекомендую.

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

              Более-менее будет работать следующий код:

              // работает только если range <= RAND_MAX
              int rand(int range) {
                int x;
                while ((x = rand()) >= RAND_MAX / range * range) ;
              
                return x % range;
              }
              
            • 0
              arc4random_uniform(upper_bound) из stdlib
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              А какая разница? Лишь бы не
              isOddNumber(i).toString().length() < 5
    • +1
      Там 30 минут. Можно в кратце, пожалуйста?
      • +2
        1) time(NULL) — возвращает не совсем тот тип, что принимает srand — но это мелочь
        2) time(NULL) возвращает время с точностью до секунды, что не делает seed довольно предсказуемым
        3) сам по себе rand() довольно низкокачественный генератор
        4) rand() возвращает грубо говоря число от 0 до 65535, что значит что числа от 0 до 15 в rand()%20 будут более вероятны чем числа от 15 до 19. Мораль: не делать rand() % N.
        5) Предлагается использовать заголовок random из C++11, но в данном случае как я понял у автора C, так что это недоступно
        • 0
          что-то я не понят про пункт 4, можно поподробнее? это все же остаток от деления, а не его результат. Грубо говоря, это группа вычетов, поэтому при равномерном распределении все будет от 0 до 19
          • +1
            Для простоты рассмотрим более простой пример (вывод будет верен и для исходного, просто числа другие). Пусть rand() равновероятно возвращает число от 0 до 15. А у нас есть 10 поз с номерами от 0 до 9.

            Что случится если мы будем выбирать позу по формуле rand() % 10?

            Вероятность выбрать 0 будет равна 2/16 = 1/8, т.к. она будет выбираться в случаях когда rand() вернул 0 или 10.
            Вероятность выбрать 9 будет равна 1/16, т.к. она будет выбирать только в случае когда rand() вернул 9.

            Что ещё хуже (не в примере с позами. а вообще) — у нас не просто неравномерное распределение — а ещё и среднее смещено в сторону нуля, т.к. более вероятны более маленькие числа.
            • 0
              Ваши рассуждения для большого числа верны только на «хвосте», потому что это число не делится на цело на 20. Если число достаточно большое, то вероятности будут очень близки.

              Насчет неравномерности распределения вполне возможно, здесь я тонкостей реализации не знаю
              • +1
                Не, реализация тут ни при чём. Просто даже если предполагаем что rand() идеально равномерен — после взятия модуля получается некрасиво. Причём погрешность хоть и маленькая, но вполне ощутимая, потому что гарантированный RAND_MAX всего 32767, что не так уж и много. Поэтому при генерации чисел от 0 до 20 «хвост» будет смещать вероятности части исходов на 0.06%. Примерно на эту же величину будет смещено среднее — не из-за особенностей реализации rand(), а из-за взятия модуля.

                А представьте что мы пытаемся выбрать случайным образом город из списка в 10000 городов (не знаю зачем, но допустим)? Тогда города с номерами, меньшими 2767, будут выпадать на 33% чаще чем остальные!
                • 0
                  Ну, для такого приложения, как здесь, 0,06% — это приемлемо :) а для общего случая, разумеется, это не вариант, и надо иметь ввиду.

                  Добавим в кучку к таким примерам, как подсчет среднего значения, что нельзя просто посчитать сумму и поделить пополам. Спасибо за пояснения!
      • +2
        Если вкратце, то вот ссылка: sdrv.ms/1e11LXl

        Хотя, конечно, в контексте данной статьи она совсем не при чем, какая разница, если распределение у ответов будет не идеально равномерное?
        • +21
          Это будет ужасно! Одни позы будут встречаться чаще других!
          • +3
            и это, кстати, можно использовать в своих интересах!
  • 0
    А с чем может быть связана ситуация когда в pebble application эти watchapps посылаются на часы, но ни в самих часах, ни в «Watch Apps» списке они не появляются?
    Обновление прошивки стоит последнее.
    • 0
      Ну вот такое поведение я видел только когда была несовместимость версий прошивки/программы на телефоне и приложения для часов. Последнее — это 1.13 или 2.0?
      • 0
        1.13, да… а как на 2.0 перейти? официальное приложение не дает таких опций.
        • +1
          Вот тут описано, раздел Flashing your Pebble with a Beta firmware.
          Кратко — поставить вот это приложение, и открыть в нем вот эту прошивку, если в серийнике нет Q, и вот эту, если есть.
          • 0
            Спасибо!
  • 0
    Цикл статей интересный и для меня лично актуален (новоиспеченный владелец). Если не сложно, хотелось бы увидеть в следующих уроках пример взаимодействия часов с почтовыми програмами, считывание количества новых сообщений от различных ящиков (яху, стандартное приложение Email и т.п.). Работа с смс, погодой от разных поставщиков.
    • 0
      Ой, было бы время. Я вот по общению с телефоном и интернетом не могу никак дописать. Еще есть про хранилище данных на часах и меню
      • 0
        Жду с нетерпением. Просто именно взаимодействие с телефоном первоочередная функция часов :)
        • 0
          Там замороченная система, я матерился на разработчиков, когда раскуривал. Причем нигде полностью не описана, предполагается, что читающий знает логику взаимодействия. Вот с логикой как раз и самая большая проблема.

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

Самое читаемое Разработка