面向Watchapp的Hello World,CloudPebble上的Pebble开发
使用C语言进行Pebble Watchapp开发。从CloudPebble(Pebble的在线开发环境)的入门开始,我们将构建一个具有独特功能的游泳圈数计数器。
引言
大约20年来,我一直在寻找一种记录游泳圈数的方法,但收效甚微。我尝试过一些游泳手表,有些声称能自动计数,但它们不能,另一些则需要手动操作。在智能手表中,Pebble手表具有足够的防水性,可以在游泳时佩戴,现在Pebble也有许多游泳watchapp。但是,和所有其他以前的设备一样,这些watchapp都没有解决一个根本问题。那就是,如果你正在积极游泳,你看不到手指或手腕上显示圈数的屏幕。精美奖品的诱惑终于促使我不再等待完美的应用程序,而是动手编写自己的应用程序。
在这里,我打算通过构建一个watchapp来解决这个问题,该watchapp利用Pebble的振动电机向游泳者提供反馈。每游10圈,我都会实现一种振动模式,每次振动代表10圈。当我游泳时,我将知道何时达到10、20、30圈,因为手表会每10圈振动1次。例如,当我达到30圈时,我会感觉到3次明显的振动。
游泳圈数计数器通常以两种方式计数:通过按钮点击手动计数或通过运动检测计数。除了佩戴在手指上的手动计数器外,我的经验是,在游泳时这两种方法都效果不佳。在这个watchapp中,我使用Pebble的加速度计检测手腕的轻弹来增加一圈。
我对这个watchapp的最后一个要求是提供一个模式,可以让你设置一个圈数目标。也就是说,我那天想游多少圈,并在达到目标时提供一个“游泳完成”的振动模式,以便是时候擦干身体,开一瓶啤酒了。
我将本文以教程形式组织,在每个部分中,我们将随着介绍新的Pebble SDK函数而为圈数计数器添加功能。在第一部分中,我们将介绍CloudPebble,Pebble的基于浏览器的IDE。本节面向任何编程新手或C语言编程新手。如果您更高级,请随意点击CloudPebble链接,创建一个新项目并选择HelloWorld模板以查看模拟器运行情况。
第2部分开始圈数计数器watchapp的开发。在本节中,我们研究TextLayers(一种显示对象)和按钮点击事件,以创建圈数目标显示并更改计数。
第3部分介绍了在Pebble上持久化变量、添加图像和实现Action Bar(一种控制和显示按钮点击的显示对象)。
第4部分我们将为watchapp添加游泳模式,并构建我们的敲击事件处理程序(即手腕轻弹)以增加游泳圈数。
在第5节中,我们将通过根据当前圈数动态生成振动模式来完成watchapp。
从第2节开始,我为每个部分提供了一个项目zip文件,您可以将其上传到您的CloudPebble环境。
背景
我这篇文章的范围是Aplite平台上的SDK 2。这意味着我针对的是带有黑白显示屏的Pebble Classic和Pebble Steel手表。较新的Pebble手表具有64色显示屏,运行在SDK 3及更高版本上。Pebble手表也可以用JavaScript编程,这也不在本文的讨论范围之内。
CloudPebble上的Hello World
CloudPebble位于这里
创建账户后,您将被重定向到您的项目页面。
点击“创建”打开“创建新项目”窗口。
输入项目名称;保持默认项目类型为Pebble C SDK;SDK版本选择SDK 2,模板选择HelloWorld。
选择“创建”,CloudPebble将创建您的第一个项目。在左侧的“源文件”下,点击“hello_world.c”以查看CloudPebble为我们生成的代码。
滚动到文件的底部,找到main函数。如果您不熟悉C语言或类似C的语言,那么main函数是处理器在大多数程序中首先运行的代码。在我们调用main中的三个函数中,大部分工作将在handle_init函数中完成。它不需要被命名为handle_init,也不需要是一个单独的函数。重要的是,在我们到达app_event_loop函数之前,所有设置,包括显示和事件处理程序,都已完成。
int main(void) {
handle_init();
app_event_loop();
handle_deinit();
}
SDK函数`app_event_loop`运行watchapp。它等待事件发生并将其定向到任何适用的处理程序。这些通常是SDK处理程序或我们在`handle_init`函数中创建的自定义处理程序。当我们的watchapp运行时,我们将一直处于`app_event_loop`中,直到watchapp收到关闭和退出的调用。
你创建的,你也必须销毁。在C语言中,我们是自己的垃圾收集器,所以handle_deinit函数是我们清理在handle_init中创建的所有东西的地方。这会释放我们的watchapp不再需要的内存和其他watch资源,因为我们正在关闭。和handle_init一样,handle_deinit可以有任何名称或分成多个函数。对handle_deinit重要的是它在app_event_loop之后的位置,这样我们就可以在关闭和退出watchapp代码时执行它。
让我们仔细看看handle_init中的设置代码。在我们的Hello World watchapp中,我们没有任何需要配置的事件,但我们确实设置和配置了我们的显示。在Pebble上显示任何内容的基本对象是Window对象,在handle_init中我们分配了静态窗口指针。该指针在文件顶部附近声明为Window对象的指针。在handle_init中,使用SDK函数window_create,我们将指针分配给新创建的窗口对象的地址。
现在我们有了一个可以操作的窗口,我们将添加一个TextLayer对象,以便在窗口中显示我们的文本消息。与Window对象一样,TextLayer对象也有一个SDK创建函数`text_layer_create`,用于将我们的TextLayer指针分配给TextLayer对象的地址。创建TextLayer对象后,我们可以添加文本并通过设置字体和文本对齐方式来自定义文本格式。现在,我们可以使用SDK函数`layer_add_child`将TextLayer添加到Window对象。设置好Window及其新的TextLayer后,我们就可以使用SDK函数`window_stack_push`将Window发送到Pebble的显示屏,并将我们的Window对象作为第一个参数。
void handle_init(void) {
// Create a window and text layer
window = window_create();
text_layer = text_layer_create(GRect(0, 0, 144, 154));
// Set the text, font, and text alignment
text_layer_set_text(text_layer, "Hi, I'm a Pebble!");
text_layer_set_font(text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD));
text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
// Add the text layer to the window
layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
// Push the window
window_stack_push(window, true);
// App Logging!
APP_LOG(APP_LOG_LEVEL_DEBUG, "Just pushed a window!");
}
CloudPebble HelloWorld 模板在 `handle_init` 中演示了另一个函数:`APP_LOG`。`APP_LOG` 实际上并不需要显示我们的 Hello World 消息,但迟早你会需要它来排除故障。`APP_LOG` 是 watchapp 开发的必备工具。SDK 函数 `APP_LOG` 是我们的打印语句,可以放置在代码各处,以帮助找出代码中出现的问题。在模拟器或手表上运行时,您可以在 watchapp 运行时实时查看日志文件中的 `APP_LOG` 消息。
在`handle_init`中我们创建了一个Window对象和一个TextLayer对象,所以在`handle_deinit`中我们需要使用每个对象的销毁函数`text_layer_destroy`和`window_destroy`来清除它们。
void handle_deinit(void) {
// Destroy the text layer
text_layer_destroy(text_layer);
// Destroy the window
window_destroy(window);
}
清理完成后,我们现在可以编译代码并在Pebble Aplite模拟器中运行我们的watchapp。在浏览器的右侧,点击播放按钮进行编译和运行。
如果一切顺利,模拟器将在您的左侧显示,如下所示。当您看到“Installed successfully!”时,点击“view logs”查看我们的APP_LOG调试消息。
模拟器运行结束后,点击手表右下角的齿轮图标。然后,选择“关机”。
那是快乐的路径。不那么快乐的是编译错误。如果抛出编译错误,编译日志将显示记录的错误。如果您是编译语言编程新手,解密编译器消息可能是一门艺术。我最好的建议是从项目开始就编译代码,之后经常编译,这样当您遇到错误时,您就能很好地了解自上次成功编译以来所做的更改。
按钮点击和文本层
下载 buttonclicksandtextlayer.zip
在我们的Hello World项目中,watchapp的设置和清理通过handle_init和handle_deinit完成。在本节中,我们将它们缩短为init和deinit。新重命名的init函数有一些重大更改。首先,我们将为窗口提供自己的加载和卸载处理程序。这更简洁,并为在运行时加载和卸载多个窗口提供了结构。尽管本文我们只使用一个窗口,但这种结构更容易管理,并为我们提供了在需要时轻松添加多个窗口的灵活性。任何作为窗口子对象的对象现在都将由新的加载事件处理程序window_load管理,而这些对象的清理将由卸载事件处理程序window_unload管理。文本层配置将移至window_load,文本层的清理将移至window_unload,只留下window_destroy在deinit中。
static void init(void) {
s_LapGoal = TOTAL_LAPS;
window = window_create();
window_set_click_config_provider(window, click_config_provider);
window_set_window_handlers(window, (WindowHandlers) {
.load = window_load,
.unload = window_unload,
});
const bool animated = true;
window_stack_push(window, animated);
}
在 `init` 中,我们将使用 SDK 函数 `window_set_window_handlers` 来注册我们的 2 个新的窗口处理程序。在注册窗口处理程序的同时,我们还将使用 SDK 函数 `window_set_click_config_provider` 注册处理程序 `click_config_provider`。这个函数将窗口与按钮点击连接起来。然后,处理程序 `click_config_provider` 将每个按钮点击事件注册到,等等……更多的处理程序,每个按钮点击一个。我们将在下一节中介绍按钮点击处理程序。
Hello World 项目中 `init` 的最后一个更改是一个外观/样式更改,添加了布尔变量 `animated`,将其设置为 `true`,并将其用作函数 `window_stack_push` 的第二个参数。那么 `animated` 的作用是什么?如果为 `true`,窗口将使用滑动动画滑入。`animated` 也是函数 `window_stack_pop` 的一个参数,在该函数中,窗口将在离开时使用滑动动画。
有趣的新东西从这里开始。在这个应用程序中,我们只有一个窗口,但该窗口将有两种模式,运行模式和目标圈数设置模式。我们将从目标圈数设置模式开始。在目标圈数设置模式下,向上按钮将增加文本层显示的圈数,向下按钮将减少显示。中间按钮是选择按钮,它启动运行模式,但在这里我们只让它在文本层上显示9。返回按钮将保持其默认功能,退出应用程序并转到Pebble菜单。
回到`init`中,我们注册了点击配置处理程序`click_config_provider`。在这里,我们将连接三个点击处理程序,`up_click_handler`、`down_click_handler`和`select_click_handler`到它们的点击事件。这通过SDK函数`window_single_click_subscribe`实现。`window_single_click_subscribe`的第一个参数是一个用于标识按钮的常量。第二个参数是点击处理程序。
static void click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
window_single_click_subscribe(BUTTON_ID_UP, up_click_handler);
window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler);
}
对于选择点击处理程序,我们将使用 `text_layer_set_text` 显示“99999”,这是一个占位符,直到我们添加运行模式。向上和向下按钮点击处理程序将在此模式下完成工作,以设置我们的圈数目标。
static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
text_layer_set_text(s_text_layer, "99999");
}
static void up_click_handler(ClickRecognizerRef recognizer, void *context) {
inc_and_display_laps();
}
static void down_click_handler(ClickRecognizerRef recognizer, void *context) {
dec_and_display_laps();
}
静态整数变量 `s_LapGoal` 将保存我们的值,并通过使用 `#define TOTAL_LAPS` 将默认值设置为 10。在 `init` 中,我们将 `s_LapGoal` 设置为 `TOTAL_LAPS`,并在 `window_load` 中使用 `text_layer_set_text` 将显示设置为 `s_LapGoal`。但是,有一个小问题。函数 `text_layer_set_text` 只接受指向字符数组的指针,而我们有一个整数。尽管我已经悄悄引入了一些指针变量,但现在是时候讨论指向字符数组的指针了,在大多数语言中它是一个字符串。
我听到呻吟声了,指针……一定是指针……
我们无论如何都需要将一个整数转换为一个字符数组。幸运的是,我们有一个函数`snprintf`可以为我们完成这项工作。我们所需要的只是一个设置正确大小的字符缓冲区,`snprintf`会完成其余的工作。那么我们的字符缓冲区的正确大小是多少呢?
为了回答这个问题,让我们简要地跳到 `window_load` 中文本层的格式化。除了感知圈数外,我还想以尽可能大的字体显示圈数。这样,即使戴着湿漉漉的模糊泳镜,也能清楚地看到数值。
在我们的例子中,我们将使用 Roboto 49 Bold Subset,这是较大的系统字体之一,这种字体非常大,它只包含数字和冒号,以节省 Pebble 内存空间。
Pebble系统字体及其键可以在这里找到
在尺寸49下,这种字体可以在窗口中显示5位数字,因此这为我们提供了main.c中定义的最大物理值99999。这也给了我们字符缓冲区的大小,5个整数数字。
/* Text buffer for the size required to display the lap count goal */
static char s_LapDisplayBuffer[sizeof(int)*5];
现在我们有了缓冲区大小,我们使用`snprintf`用整数值填充缓冲区,从而允许我们在文本层中加载`s_LapGoal`的字符表示。
snprintf(s_LapDisplayBuffer, sizeof(s_LapDisplayBuffer), "%u", ++s_LapGoal);
text_layer_set_text(s_text_layer, s_LapDisplayBuffer);
如果您想知道,“%u”是无符号整数的格式说明符。我们将把圈数目标限制为非负整数。
这就引出了我们的向上和向下点击处理程序调用的两个函数:`inc_and_display_laps` 和 `dec_and_display_laps`。两者都遵循相同的逻辑:检查 `s_LapGoal` 的上限或下限并显示结果。
static void inc_and_display_laps() {
if (s_LapGoal >= MAX_LAPS)
{
--s_LapGoal;
}
snprintf(s_LapDisplayBuffer, sizeof(s_LapDisplayBuffer), "%u", ++s_LapGoal);
text_layer_set_text(s_text_layer, s_LapDisplayBuffer);
}
static void dec_and_display_laps() {
if (s_LapGoal < 2)
{
s_LapGoal = 1;
}
snprintf(s_LapDisplayBuffer, sizeof(s_LapDisplayBuffer), "%u", --s_LapGoal);
text_layer_set_text(s_text_layer, s_LapDisplayBuffer);
}
在重新编译和运行之前,我们的最后一步是完成Pebble的window_load函数中text_layer的设置和格式。在Hello World中,text_layer是使用硬编码的GRect结构体值创建的。现在我们将使用window_layer部分填充结构体,这样我们就可以根据窗口大小来调整text_layer的大小。
要获取窗口的`window_layer`,我们使用`window_get_root_layer`。然后,要获取`GRect`结构体,我们使用`layer_get_bounds`。使用这个结构体,我们可以创建`text_layer`,其大小和位置相对于父`window_layer`。`GRect`有2个数据点用于原点(x和y坐标)和2个数据点用于大小(高度和宽度)。我将原点设置为最左侧,即0,并设置为窗口层的一半,`bounds.size.h/2`,偏移量为49,以补偿基于字体大小的文本层高度。除了文本层高度的49,我还将窗口层宽度`bounds.size.w`用作文本层宽度。
static void window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
s_action_bar = action_bar_layer_create();
action_bar_layer_add_to_window(s_action_bar, window);
action_bar_layer_set_click_config_provider(s_action_bar, click_config_provider);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_UP, s_icon_plus);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_DOWN, s_icon_minus);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_SELECT, s_icon_start);
GRect bounds = layer_get_bounds(window_layer);
text_layer = text_layer_create((GRect) { .origin = { 0, bounds.size.h/2 - 49 },
.size = { bounds.size.w - ACTION_BAR_WIDTH - 3, 49 } });
snprintf(s_LapDisplayBuffer, sizeof(s_LapDisplayBuffer), "%u", s_LapGoal);
text_layer_set_text(s_text_layer, s_LapDisplayBuffer);
text_layer_set_text_alignment(s_text_layer, GTextAlignmentCenter);
text_layer_set_font(s_text_layer,fonts_get_system_font(FONT_KEY_ROBOTO_BOLD_SUBSET_49));
layer_add_child(window_layer, text_layer_get_layer(s_text_layer));
}
现在,您可以点击浏览器右侧的绿色播放按钮来编译并运行模拟器。
您可以测试向上和向下按钮,以验证它们确实可以增加和减少显示。选择按钮将显示设置为99999,但再次选择向上和向下按钮将显示圈数目标。
现在尝试按下手表左侧的返回按钮。
这将带您进入Pebble的菜单。要返回我们的应用程序,请在菜单上向下滚动到“SwimLapCounter”并按下中间的选择按钮。
当我们的应用程序重新初始化时,圈数目标将重置为10,并且18圈的目标将丢失。我们将在下一节中解决这个问题。
变量持久性和图像资源
在Pebble应用程序中持久化变量分为3个步骤:为该值创建一个值键,在init中检查并从该键加载值,并在deinit中写入该键位置。
为了创建地址位置键,我使用`#define`并在地址值后面加上`_PERSISTKEY`后缀。对于总圈数的初始值,我添加了`_DEFAULT`后缀。这个常量现在只在我们`TOTAL_LAPS_PERSISTKEY`没有值时使用。
#define TOTAL_LAPS_DEFAULT 10
#define TOTAL_LAPS_PERSISTKEY 42
#define MAX_LAPS 99999
下一步是用持久化值加载我们的watchapp变量。在这里,我使用三元运算符与SDK函数`persist_exists`来检查键是否存在,并使用`persist_read_int`从持久化内存中提取值。如果该值不存在,我们将从我们定义的默认值开始;在本例中为10。
static void init(void) {
s_LapGoal = persist_exists(TOTAL_LAPS_PERSISTKEY) ?
persist_read_int(TOTAL_LAPS_PERSISTKEY) :TOTAL_LAPS_DEFAULT;
最后一步是使用SDK函数`persist_write_int`在`deinit`中将我们在watchapp会话中设置的值保存到持久化内存中。现在我们可以设置我们的圈数目标一次,并将其用于多次游泳。
static void deinit(void) {
persist_write_int(TOTAL_LAPS_PERSISTKEY, s_LapGoal);
现在我们的watchapp可以保存我们的圈数目标了,我们可以继续为窗口添加一个动作条。动作条沿着窗口的右侧,在向上、向下和选择按钮旁边运行,并包含每个按钮的图标,以向用户提供每个按钮当前功能的视觉提示。
完成后,我们的动作条将在向上按钮上有一个加号图标,向下按钮上有一个减号图标,以及一个播放图标,用于进入游泳模式。
Pebble的动作条图标是`.png`图像,使用黑色和白色两种颜色,分辨率不得超过28x28。这些图标的一个好资源可以在这里找到:
https://github.com/pebble-hacks/pebble-icons
要将图标添加到您的项目,请前往浏览器左侧,点击“RESOURCES”标题后面的“ADD NEW”按钮。这将打开资源导入表单。点击“浏览…”按钮上传您的文件。Pebble建议在文件名中使用“~bw”和“~color”以支持多个SDK和构建,但在这里我们只使用SDK 2.0和Pebble经典版的Aplite构建,并将删除图像后缀。
在“文件”和“浏览…”按钮下方是“IDENTIFIER”字段。这个带`RESOURCE_ID_`前缀的标识符就是您在代码中访问图像的方式。在下面的示例中,`IMAGE_ACTION_ICON_START`将通过标识符`RESOURCE_ID_IMAGE_ACTION_ICON_START`进行访问。
添加标识符后,按“保存”,图标将显示如下。
在CloudPebble上,添加资源后会发生什么并不太清楚,但如果你将项目下载为zip文件,你会看到项目主文件夹中有一个resources文件夹和appinfo.json文件。resources文件夹包含图像,在项目根文件夹中,appinfo.json文件将图像文件中的图像链接到SDK和编译器。
...
"longName": "swim_lap_menu_persist",
"projectType": "native",
"resources": {
"media": [
{
"file": "images/action_bar_icon_start.png",
"name": "IMAGE_ACTION_ICON_START",
"type": "png"
},
{
"file": "images/action_icon_minus.png",
"name": "IMAGE_ACTION_ICON_MINUS",
"type": "png"
},
{
"file": "images/action_icon_plus.png",
"name": "IMAGE_ACTION_ICON_PLUS",
"type": "png"
}
]
},
"sdkVersion": "2",
...
将图标添加到项目后,我们可以将动作栏及其图像添加到main.c。在这里,我们将首先为ActionBarLayer添加静态变量指针,并为图标添加3个GBitmaps指针。
static ActionBarLayer *s_action_bar;
static GBitmap *s_icon_plus, *s_icon_minus, *s_icon_start;
接下来,我们转到 `init`,使用函数 `gbitmap_create_with_resource` 和带 `RESOURCE_ID_` 前缀的图标标识符,将图标资源分配给 `GBitmap` 变量。
s_icon_plus = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ACTION_ICON_PLUS);
s_icon_minus = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ACTION_ICON_MINUS);
s_icon_start = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ACTION_ICON_START);
我们创造的,我们必须销毁……
在 `deinit` 中,我们需要释放资源使用的内存。这是通过 `gbitmap_destroy` 函数完成的,该函数的参数是图标的变量。
gbitmap_destroy(s_icon_plus);
gbitmap_destroy(s_icon_minus);
gbitmap_destroy(s_icon_start);
现在我们转向`window_load`来创建动作栏层,将动作栏附加到窗口,并将图标分配给按钮。动作栏提供自己的按钮点击提供者,因此我们还将我们的点击处理程序从窗口对象移动到动作栏,方法是在`window_load`中添加`action_bar_layer_set_click_config_provider`函数,并从`init`中删除`window_set_click_config_provider`。
window = window_create();
// window_set_click_config_provider(window, click_config_provider);
window_set_window_handlers(window, (WindowHandlers) {
...
action_bar_layer_add_to_window(s_action_bar, window);
action_bar_layer_set_click_config_provider(s_action_bar, click_config_provider);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_UP, s_icon_plus);
我们创造的,我们必须毁灭。这句我好像在哪里听过?
像我们之前创建的文本层一样,动作栏也需要清理并释放内存。这通过在 `window_unload` 中实现 SDK 函数 `action_bar_layer_destroy` 来完成。
static void window_unload(Window *window) {
text_layer_destroy(s_text_layer);
action_bar_layer_destroy(s_action_bar);
}
在编译之前,我们需要处理一些边界问题。
由于我们将文本层宽度设置为窗口层的宽度,所以动作栏和文本层重叠了。
这很容易通过从窗口层宽度中减去动作栏宽度(`ACTION_BAR_WIDTH`加上一点点摆动空间)来补救。
点击绿色的播放按钮编译并启动模拟器。我们的动作栏现在以其全部荣耀显示出来,手表初始化为10,因为它找不到TOTAL_LAPS的键。由于按钮点击现在由动作栏管理,所以当按下右侧按钮时,动作栏会提供视觉反馈,反转黑白。通过点击左侧的返回按钮进入Pebble主菜单来测试持久性。在主菜单中,向下滚动到swim_lap_counter并选择。如果一切顺利,您应该会看到手表上自豪地显示着上次的值。
有人点击中间按钮了吗?由于文本层现在更薄,它无法再容纳5位数字,因此显示现在在999之后添加了一个省略号。我们将在下一节中添加运行模式时删除我们的99999占位符。
游泳模式和点击事件
在本节中,我们将完全实现选择按钮的点击事件处理程序,向窗口层添加一个状态文本层,并创建一个处理程序以连接到点击事件,以便我们可以增加我们的圈数计数器。
在文本窗口的底部,我们添加了一个状态文本层,它将在黑白文本和背景之间切换。这与动作条一起,向用户提供watchapp运行状态的视觉反馈。
标题文本层与我们的主文本层添加方式相同。首先,创建一个静态文本层变量,在 `window_load` 中添加新层,并在 `window_unload` 中销毁。对于标题文本层,我们将使用较小的字体,并通过一个新函数 `set_header_format` 来控制显示格式。这个函数将根据 watchapp 的状态(运行或设置圈数目标)来管理显示。
布尔变量 `s_RunMode` 将处理我们的状态,因此 `set_header_format` 将根据 `s_RunMode` 的值设置文本、文本颜色和背景颜色。
static void set_header_format() {
if (s_RunMode){
text_layer_set_background_color(s_header_layer,GColorWhite);
text_layer_set_text_color(s_header_layer, GColorBlack);
display_curent_lap_status();
}
else{
text_layer_set_background_color(s_header_layer, GColorBlack);
text_layer_set_text_color(s_header_layer, GColorWhite);
text_layer_set_text(s_header_layer, "Set Lap Goal");
}
}
当watchapp处于运行模式时,动作栏也会通过将启动图标(发令枪)更改为停止图标(X)来向用户提供视觉提示。
如果你跟着我的步骤,是的,我稍微发挥了一点艺术创造力,创建了一个发令枪图标,并在不更改代码中标识符的情况下替换了资源文件中的播放图标。
按照之前的相同步骤添加一个解除图标:导入图标资源,创建一个静态GBitmap变量`s_icon_dismiss`,在`init`中将资源附加到`s_icon_dismiss`,并在`deinit`中销毁。在我们通过`windows_load`将动作栏按钮连接到其图标之前,我们将动作栏的图标格式化移动到它自己的函数`set_action_bar_format`。在这里,我们将根据`s_RunMode`的状态设置动作栏的外观。
static void set_action_bar_format() {
if (s_RunMode){
action_bar_layer_clear_icon(s_action_bar,BUTTON_ID_UP);
action_bar_layer_clear_icon(s_action_bar,BUTTON_ID_DOWN);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_SELECT, s_icon_dismiss);
}
else{
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_UP, s_icon_plus);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_DOWN, s_icon_minus);
action_bar_layer_set_icon(s_action_bar, BUTTON_ID_SELECT, s_icon_start);
}
}
新的函数 `set_action_bar_format` 将使用 SDK 函数 `action_bar_layer_clear_icon` 清除向上和向下按钮上的图标,并在 watchapp 处于运行模式时从图标启动切换到关闭图标。 `else` 是我们最初在 `windows_load` 中设置目标模式的默认格式。
清除上下按钮上的图标告知用户这些按钮在运行模式下没有功能。对于两个点击处理程序,我们希望通过检查`s_RunMode`的状态来确保在运行模式下按下按钮不会发生任何事情,并且只在设置目标模式下增加或减少圈数目标。
static void up_click_handler(ClickRecognizerRef recognizer, void *context) {
if (!s_RunMode){
inc_and_display_laps();
}
}
static void down_click_handler(ClickRecognizerRef recognizer, void *context) {
if (!s_RunMode){
dec_and_display_laps();
}
}
在 `select_click_handler` 中,我们将检查 `s_RunMode`,切换状态,并显示我们的圈数目标或已游圈数的运行计数。对于运行计数,我们需要添加另一个静态整数变量 `s_Laps`,在运行模式下 `select_click_handler` 将 `s_Laps` 初始化为 0。显示圈数目标或运行圈数现在由一个单独的函数 `display_count` 处理,该函数接受一个整数参数并在文本层中显示它。选择点击处理程序的最后一步是通过调用我们上面添加的两个新函数 `set_header_format` 和 `set_action_bar_format` 来更新标题和动作栏。
static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
if (s_RunMode){
s_RunMode = false;
display_count(s_LapGoal);
}
else {
s_RunMode = true;
s_Laps = 0;
display_count(s_Laps);
}
set_header_format();
set_action_bar_format();
}
我们现在准备实现点击事件来增加我们的圈数。我们将使用Pebble的加速度计通过手腕的轻弹来触发此事件。加速度计感知三维运动,可以使用x、y和z轴的任何组合。使用SDK,您可以收集采样加速度计数据并对采样集做出反应。一些游泳手表应用程序使用加速度计数据来检测转弯时的模式并自动增加圈数。在这里,我们保持简单,将使用SDK的点击事件服务。点击事件提供轴和整数作为检测到的加速度的度量。与表带平行的手表长度是y轴,这就是我们将用于手腕轻弹的轴。x轴是手表的左右宽度,z轴是表面的上方-下方。
我们首先创建函数 `tap_handler`,在该函数中,当检测到 y 轴敲击时,我们将在游泳模式下增加并显示已游圈数。轴通过检查 `AccelAxisType` 的第一个参数来确定。
static void tap_handler(AccelAxisType axis, int32_t direction) {
if (axis == ACCEL_AXIS_Y && s_RunMode ) {
display_count(++s_Laps);
display_curent_lap_status();
}
}
剩下的就是在 `init` 中使用 SDK 函数 `accel_tap_service_subscribe` 注册我们的点击处理程序,并在 `deinit` 中使用 `accel_tab_service_unsubscribe()` 进行清理。
敲击事件完成后,我们准备使用模拟器进行编译和测试。运行模拟器时,可以通过x、y和z键触发加速度计敲击事件。根据我的Firefox使用经验,它们间歇性工作,所以您可能需要按几次键才能看到任何反应。如果您的按键触发了多个事件,导致圈数以2、3…递增,请刷新浏览器,这是一个已知错误。
在本节中,我们创建了运行模式并实现了Pebble SDK的点击事件,以在我们游泳时增加圈数。在最后一节中,我们将使用手表的振动电机作为反馈,以确认手腕轻弹,每10圈返回一次圈数,并发出信号表示我们已达到目标圈数。
产生良好的振动
在本节中,我们将使用Vibes API来控制Pebble的振动电机,为游泳者提供触觉反馈,以确认圈数增加,每10圈提供当前计数,并在游泳完成后发出信号。
为了生成我们的响应,我们将使用 API 函数 `vibes_enqueue_custom_pattern`,它以一个无符号整数数组作为参数。每个数组元素表示一个毫秒级的时间间隔,其中至少包含一个振动-开元素。如果数组中有两个或更多元素,则模式将交替进行开-间隔、关-间隔,用于数组中剩余的元素。使用 `vibes_enqueue_custom_pattern`,我们将动态生成一个数组,该数组至少包含一次振动以确认手腕轻弹。如果我们在可被10整除的圈数中和/或达到了我们的圈数目标,那么这种确认振动之后还将跟着一系列的开/关计数振动或第二个开/关模式以表示圈数目标已完成。
在我们的watchapp中,振动模式将在一个函数`vibe_response`中生成和执行,并借助一些`#defines`。这个新函数将在`tap_handler`(我们的点击事件处理程序)中调用。由于我们正在内存中动态生成一个数组,我们也将尽可能无痛苦地使用指针和内存分配。首先,我们将数组大小变量初始化为1。在大多数情况下,我们只需要一次振动来确认手腕轻弹。如果我们没有达到我们的游泳圈数目标,或者没有设置目标,我们接下来将当前圈数与我们的通知间隔(10)取模,以查看是否需要生成一个模式来报告我们游了多少圈。如果我们在10的间隔上,那么我们通过将计数除以间隔,将结果加倍并将其添加到当前数组大小值(1)来计算数组大小。我们之所以将结果加倍,是因为我们需要考虑睡眠或关闭间隔以及振动间隔。如果我们已经达到了圈数目标,我们只需将数组大小设置为我们已完成的数组大小,然后继续。
static void vibe_response(int count){
// 1 contains the ack vibe for the tap and will always be there
int arraysize = 1;
//Generate the tracking response based on the count
if (count < s_LapGoal || s_LapGoal == 0){
if (count%INTERVAL_NOTICIFATION_DEFAULT == 0){
//for each interval we need a sleep and a vibe length so we * by 2
arraysize = arraysize + (count/INTERVAL_NOTICIFATION_DEFAULT * 2);
}
}
else{
arraysize = COMPLETED_ARRAY_SIZE;
}
现在我们有了数组大小,就可以为我们的振动模式数组分配内存了。首先,我们创建一个无符号整数的指针,然后使用`malloc`函数为数组分配内存。对于函数的参数,我们通过将计算出的数组大小乘以`sizeof`函数计算出的无符号32字节整数所需的大小来计算所需的内存。
else{
arraysize = COMPLETED_ARRAY_SIZE;
}
uint32_t *response;
response = (uint32_t*)malloc(arraysize * sizeof(uint32_t));
if (count < s_LapGoal || s_LapGoal == 0){
任何时候我们用`malloc`分配内存,我们都希望在使用完对象后释放该内存。因此,我们跳到函数的末尾,用`free`和我们创建的指针作为函数的参数来释放内存。
//Run the generated pattern
vibes_enqueue_custom_pattern(patttern);
free(response);
}
现在我们的数组有了内存,下一步是用我们将发送到振动电机的模式填充数组。如果达到我们的圈数目标,我们会用`SWIM_COMPLETE`的值(300毫秒)填充数组,进行10次开-关模式振动。如果我们仍在游泳,我们知道第一个元素将是确认间隔,`ACK_TAP_RESPONSE`或400毫秒。如果数组大小大于1,那么我们还知道数组中的第二个元素将是确认响应和已游圈数(以10为单位)之间的睡眠时间,即`SEPARATION_SLEEP_TIME`,2000毫秒。对于以10为单位的已游圈数元素,偶数索引的元素将是振动时间或`LAP_COUNT_RESPONSE`,500毫秒,大于1的奇数索引元素将是`LAP_COUNT_SLEEP`,也是500毫秒。
if (count < s_LapGoal || s_LapGoal == 0){
for (int i = 0; i < arraysize; ++i ){
if(i == 0){
response[i] = ACK_TAP_RESPONSE;
}
else if (i%2==1){
if (i == 1){
// sleep time between ack and count feedback
response[i] = SEPARATION_SLEEP_TIME;
}
else{
// interval sleep time
response[i] = LAP_COUNT_SLEEP;
}
}
else{
// vibrate time
response[i] = LAP_COUNT_RESPONSE;
}
}
}
else{
// completed pattern on/off 10 times
for (int i = 0; i < arraysize; ++i ){
response[i] = SWIM_COMPLETE;
}
}
函数`vibes_enqueue_custom_pattern`将其参数作为`VibePattern`类型的结构体,其中`durations`是我们的数组,`num_segments`是数组大小。所以我们创建这个结构体,清除振动电机并执行我们的模式。
VibePattern patttern = {
.durations = response,
.num_segments = arraysize
};
//Clear any current viberation
vibes_cancel();
//Run the generated pattern
vibes_enqueue_custom_pattern(patttern);
free(response);
}
编译并在模拟器上运行。模拟器将在电机启用时震动,并遵循我们生成的模式。
添加振动反馈完成了游泳圈数计数器watch app。在本节中,我们根据已游圈数动态创建了一个数组,并将该模式发送到Pebble的振动电机,以便游泳者能够感受到他们当前的圈数状态。
历史
首次发布于2015年10月14日