黑暗模式
9. 经典案例
本章导读:
使用AWTK可以高效开发出漂亮的GUI应用。
9.1 简介
本章以实际的应用场景为案例,来阐述使用AWTK做项目的开发过程及其代码实现过程,帮助开发者积累开发应用的实战经验。经典案例如下:
- 洁净新风系统
- 炫酷图表
- 音乐播放器
- 智能手表
以上案例可以安装 AWStudio 后下载查看。
9.2 洁净新风系统
9.2.1 功能详解
洁净新风系统实现的功能点主要有下面几点:
- 点击"开关"按钮,启动/停止PM2.5、二氧化碳浓度、送风室内外温度、排风室内外温度等模拟读数,以及模拟报警
- 点击"自动"按钮,启动/停止背景(淡入淡出)切换
- 点击"定时"按钮,弹出定时设置对话框,模拟开/关定时功能(即时钟图标的显示/隐藏)
- 点击"记录"按钮,弹出记录列表(模拟数据)页面
- 点击"设置"按钮,弹出设置页面,设置控制、报警参数
- 点击三角形按钮,可以加/减频率、温度、湿度
- 实时更新系统时间
9.2.2 应用实现
本案例使用目录结构、编译和运行步骤与第2章介绍的hello world应用类似,这里就不重复介绍了。
本案例使用XML设计界面,业务逻辑使用C语言实现。主界面下方的五个按钮,通过注册点击事件进入相应的界面。主界面右边的按钮,通过注册点击事件实现参数的递增或递减。主界面中间部分通过创建image_animation和opacity动画实现炫酷的效果。运行的效果详见下图:
本案例使用到的代码均可以在CleanAir-Demo目录找到。
1. 如何打开窗口
(1)在AWTK中,可以使用一个xml文件来描述一个窗口的界面结构。比如描述记录列表窗口的record.xml代码如下:
xml
<!-- assets/default/raw/ui/record.xml -->
<window anim_hint="htranslate">
<button name="close" x="4" y="4" w="60" h="30" text="返回"/>
<view x="4" y="36" w="100%" h="40" layout="r:1 c:5">
<label text="状态" style="header"/>
<label text="室内温度" style="header"/>
<label text="室外温度" style="header"/>
<label text="室内湿度" style="header"/>
<label text="室外湿度" style="header"/>
</view>
<list_view x="4" y="76" w="-8" h="400" item_height="40">
<scroll_view name="view" x="0" y="0" w="100%" h="100%">
<list_item style="odd_clickable" layout="r1 c0">
<image draw_type="icon" w="20%" image="bell"/>
<label w="20%" text="24.4℃"/>
<label w="20%" text="26.4℃"/>
<label w="20%" text="20%"/>
<label w="20%" text="30%"/>
</list_item>
......
</scroll_view>
<scroll_bar_m name="bar" x="right" y="0" w="6" h="100%" value="0"/>
</list_view>
</window>
(2)然后在代码中使用window_open()即可打开该窗口,代码如下:
c
/* src/window_record.c */
widget_t* win = window_open("record");
其中,"record"为xml的文件名,运行效果如下图所示:
(3)如果需要手动关闭窗口使用window_close()即可,代码如下:
c
/* src/window_record.c */
window_close(win);
2. 如何打开对话框
(1)比如创建一个定时按钮窗口,代码如下:
xml
<!-- assets/default/raw/ui/timing.xml -->
<dialog anim_hint="center_scale" h="200" w="580" x="c" y="m">
<dialog_title h="35" text="定时设置" w="100%" x="0" y="0"/>
<dialog_client h="-35" w="100%" x="0" y="bottom">
<row h="30" layout="row:1 col:3" w="200" x="30" y="10">
<label style="timing" text="时"/>
<label style="timing" text="分"/>
<label style="timing" text="秒"/>
</row>
<row h="120" layout="row:1 col:3" w="200" x="30" y="40">
<text_selector options="1-24" text="15" visible_nr="3"/>
<text_selector options="1-60" text="10" visible_nr="3"/>
<text_selector options="1-60" text="16" visible_nr="3"/>
</row>
<button h="30" name="ok" style="timing_switch_btn" text="开" w="150" x="right:30" y="33"/>
<button h="30" name="cancle" style="timing_switch_btn"
text="关" w="150" x="right:30" y="96"/>
</dialog_client>
</dialog>
(2)然后在代码中使用window_open()即可打开该窗口,代码如下:
c
/* src/window_timing.c*/
widget_t* win = window_open("timing");
其中,"timing"为xml的文件名,运行效果如下图所示:
(3)如果需要手动关闭窗口使用dialog_quit()即可,代码如下:
c
/* src/window_timing.c */
dialog_quit(dialog, RET_OK);
3. 如何查找控件
在main.xml文件中设置控件的名字为" timing_status",然后C代码中调用widget_lookup()查找控件,代码如下:
xml
<!-- src/assets/raw/ui/main.xml -->
<image name="timing_status" image="clock" draw_type="icon"/>
c
/* src/window_main.c */
widget_t* timing = widget_lookup(win, "timing_status", TRUE);
4. 如何响应界面事件
比如在主界面上设置开关按钮的点击事件,代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<button name="switch" x="0" w="147" h="100%" style="switch_btn"/>
c
/* src/window_main.c */
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_switch, win);
5. 如何设置控件是否可见
AWTK中可通过控件的visible属性控制该控件是否可见。比如控制定时图标是否可见,代码如下:
c
/* src/window_main.c */
widget_t* timing = widget_lookup(win, "timing_status", TRUE);
if (open_timing_window(&t) == RET_OK) {
widget_set_visible(timing, TRUE, FALSE);
} else {
widget_set_visible(timing, FALSE, FALSE);
}
6. 如何设置窗口顶部容器
比如在主界面上显示"洁净新风系统",代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<app_bar x="0" y="0" w="100%" h="40">
<label name="dev_name" x="0" y="0" w="200" h="100%" text="洁净新风系统" style="title_left"/>
<label name="sys_time" x="200" y="0" w="-200" h="100%" style="title_right"/>
</app_bar>
7. 如何显示文本
比如在主界面上显示"PM2.5"对应的值18,代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<label name="PM2_5" x="80" w="108" h="100%" text="18" style="reading_pm_co"/>
c
/* src/window_main.c */
widget_t* pm2_5 = widget_lookup(win, "PM2_5", TRUE);
...
widget_set_text_utf8(label, tk_itoa(text, sizeof(text), val));
8. 如何显示图片
比如在主界面上显示排风图片(fan_2.png),代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<image name="fan_2" image="fan_2" draw_type="default" y="m"/>
9. 如何显示富文本
比如在主界面上显示μg/m^3^分两部分处理:显示μg/m和显示上标数字3,代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<rich_text x="188" w="-188" h="100%" text="
<font color="a;white"a; align_v="a;top"a; size="a;18"a;>μg/m</font>
<font color="a;white"a; align_v="a;top"a; size="a;10"a;>3</font>" />
10. 如何使用布局
比如在主界面上以一行两列的方式显示时钟(clock.png)和报警图标(bell.png),代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<view x="0" y="44" w="88" h="44" layout="r:1 c:2">
<image name="timing_status" image="clock" draw_type="icon"/>
<image name="alarm_status" image="bell" draw_type="icon"/>
</view>
11. 如何实现动画
(1)控件动画API
AWTK中,可以通过该控件绑定一个动画对象来实现动画。比如实现左下角风扇图标(fan_2.png)旋转,代码如下:
c
/* src/window_main.c */
widget_animator_t* widget_animator_get(widget_t* widget, const char* name,
uint32_t duration, bool_t opacity) {
widget_animator_t* animator = NULL;
value_t val;
value_set_pointer(&val, NULL);
if (widget_get_prop(widget, name, &val) != RET_OK || value_pointer(&val) == NULL) {
if (opacity) {
animator = widget_animator_opacity_create(widget, duration, 0, EASING_SIN_OUT);
} else {
animator = widget_animator_rotation_create(widget, duration, 0, EASING_LINEAR);
}
value_set_pointer(&val, animator);
widget_set_prop(widget, name, &val);
} else {
animator = (widget_animator_t*)value_pointer(&val);
}
return animator;
}
...
animator = widget_animator_get(fan_2, "animator", 4000, FALSE);
widget_animator_rotation_set_params(animator, 0, 2*3.14159265);
widget_animator_set_repeat(animator, 0);
widget_animator_start(animator);
又或者,实现风向箭头的淡入淡出动画(wind_in.png),代码如下:
c
/* src/window_main.c */
animator = widget_animator_get(wind_out, "animator", 1000, TRUE);
widget_animator_opacity_set_params(animator, 50, 255);
widget_animator_set_yoyo(animator, 0);
widget_animator_start(animator);
(2)image_animation组件
在AWTK中,可以使用image_animation组件来实现一组图片顺序播放的动画。比如设置主界面上fan1a.png和fan_1b.png图片动画,代码如下:
xml
<!-- src/assets/raw/ui/main.xml -->
<image_animation name="fan_1" image="fan_1" sequence="ab" auto_play="false"
interval="500" delay="100" y="m"/>
c
/* src/window_main.c */
image_animation_play(fan_1);
12. 如何定时刷新界面数据
比如实时更新主界面右上角的系统时间,代码如下:
c
/* src/window_main.c */
timer_add(on_systime_update, win, 1000);
13. 如何使用时分秒控件
比如点击主界面中的"定时"按钮弹出"定时设置"界面显示时分秒控件,代码如下:
xml
<!-- assets/default/raw/ui/timing.xml -->
<row h="120" layout="row:1 col:3" w="200" x="30" y="40">
<text_selector options="1-24" text="15" visible_nr="3"/>
<text_selector options="1-60" text="10" visible_nr="3"/>
<text_selector options="1-60" text="16" visible_nr="3"/>
</row>
14. 如何显示列表
比如点击主界面中的"记录"按钮弹出的列表视图,代码如下:
xml
<!-- assets/default/raw/ui/record.xml -->
<list_view x="4" y="76" w="-8" h="400" item_height="40">
<scroll_view name="view" x="0" y="0" w="100%" h="100%">
<list_item style="odd_clickable" layout="r1 c0">
<image draw_type="icon" w="20%" image="bell"/>
<label w="20%" text="24.4℃"/>
<label w="20%" text="26.4℃"/>
<label w="20%" text="20%"/>
<label w="20%" text="30%"/>
</list_item>
...
</scroll_view>
<scroll_bar_m name="bar" x="right" y="0" w="6" h="100%" value="0"/>
</list_view>
15. 如何实现分页
(1)比如点击主界面中的"设置"按钮弹出分页视图,代码如下:
xml
<!-- assets/default/raw/ui/setting.xml -->
<window anim_hint="htranslate" name="setting_page">
<button h="30" name="close" text="返回" w="60" x="4" y="4"/>
<pages h="440" w="80%" x="right" y="40">
<view h="60%" layout="r:4 c:3" w="100%">
<label style="setting_page" text="室内温度"/>
...
</pages>
<list_view auto_hide_scroll_bar="true" h="440" item_height="40" w="20%" x="left" y="40">
<scroll_view h="100%" name="view" w="-12" x="0" y="0">
<tab_button text="控制参数设定" value="true"/>
<tab_button text="报警设置"/>
</scroll_view>
<scroll_bar_d h="100%" name="bar" value="0" w="12" x="right" y="0"/>
</list_view>
</window>
(2) 运行效果如下图所示:
16. 如何设置控件样式
比如设置主界面中的"开关"按钮的样式,代码如下:
xml
<!-- assets/default/raw/ui/main.xml -->
<button name="switch" x="0" w="147" h="100%" style="switch_btn"/>
xml
<!-- assets/default/raw/styles/default.xml -->
<button>
...
<style name="switch_btn">
<normal icon="btn_1"/>
<pressed icon="btn_1_push"/>
<over icon="btn_1_hover"/>
</style>
...
</button>
c
/* src/window_main.c */
widget_t* btn = widget_lookup(win, "auto", TRUE);
widget_use_style(btn, "auto_btn_2a");
17. 如何实现中英文互译
比如点击主界面右上角的"中"按钮实现中英文环境下文本和图片的切换。接下来将介绍如何实现该功能。
(1)中英环境下文本互译
比如要实现"洁净新风系统"和" CleanAir Demo"文本之间的互译,步骤如下:
首先,在strings.xml文件中配置要互译的文本,代码如下:
xml
<!-- CleanAir-Demo/res_800_480/assets/default/raw/strings/strings.xml -->
...
<string name="CleanAir Demo">
<language name="en_US">CleanAir Demo</language>
<language name="zh_CN">洁净新风系统</language>
</string>
...
然后,在main.xml中使用tr_text设置要翻译的文本,代码如下:
xml
<!-- CleanAir-Demo/res_800_480/assets/default/raw/ui/main.xml -->
<window anim_hint="htranslate" style="black">
<label name="bkgnd" x="0" y="0" w="100%" h="100%" style="bg0"/>
<app_bar x="0" y="0" w="100%" h="40">
<label name="dev_name" x="0" y="0" w="200" h="100%"
tr_text="CleanAir Demo" style="title_left"/>
<label name="sys_time" x="right:35" y="0" w="-200" h="100%" style="title_right"/>
<button name="language_btn" x="right:5" y="0" w="25" h="100%" style="language_btn"/>
</app_bar>
...
</window>
最后,在window_main.c调用locale_info_change函数实现互译,代码如下:
c
/* CleanAir-Demo/src/window_main.c */
/* 中英文互译 */
static ret_t change_locale(const char *str) {
char country[3];
char language[3];
strncpy(language, str, 2);
strncpy(country, str + 3, 2);
locale_info_change(locale_info(), language, country);
return RET_OK;
}
/* 点击中英文互译按钮 */
static ret_t on_language(void *ctx, event_t *e) {
(void)ctx;
(void)e;
const char *language = locale_info()->language;
if (tk_str_eq(language, "en")) {
change_locale("zh_CN");
} else {
change_locale("en_US");
}
return RET_OK;
}
(2) 中英环境下图片切换
比如要实现点击主界面右上角的"中"和"EN"按钮之间的切换,步骤如下:
首先,将两张图片命名为"language_en.png"和"language_zh.png"分别代表中文和英文环境下的图片,"language_ en.png"中"en"代表的是语言环境,不可以随意命名。
然后,在样式文件default.xml中通过language_$locale$字符串表示要切换的图片。在中文环境下使用language_zh.png,在英文环境下使用language_en.png,代码如下:
xml
<!-- CleanAir-Demo/res_800_480/assets/default/raw/styles/default.xml -->
<button>
<style name="language_btn" >
<normal icon="language_$locale$" />
<pressed icon="language_$locale$"/>
<over icon="language_$locale$" />
</style>
</button>
就这样就完成了中英文环境下图片的切换。
9.3 炫酷图表
9.3.1 功能详解
炫酷图表实现的功能点主要有下面几点:
- 仪表盘
- 饼图
- 曲线图
- 柱状图
9.3.2 应用实现
本案例使用目录结构、编译和运行步骤与第2章介绍的hello world应用类似,这里就不重复介绍了。
本案例使用XML设计界面,业务逻辑使用C语言实现。主界面中间的五个按钮,通过注册点击事件进入相应的界面。其中只有仪表盘界面使用了AWTK提供的guage控件实现,剩下的饼图、曲线图和柱状图是通过调用AWTK提供的接口自绘而成,这也体现了AWTK功能的强大,没有的控件可以自己实现。运行的效果详见图10.5。
本案例使用到的代码均可以在Chart-Demo目录找到。
1. 实现主界面逻辑
主界面实现的功能点如下:
- 中英文环境切换
- 界面切换,通过注册点击事件,然后进入相应的界面
下面,介绍如何实现这些功能点。
(1) 中英文环境切换
本案例实现中英文环境切换与洁净新风系统的类似,这里是通过改变样式的方式实现图片切换(比较复杂,代码如下),而洁净新风系统是通过在样式文件default.xml中给图片加个"$locale$"后缀完成(比较简单,推荐使用这种方式)。由于在上面章节已经介绍过了,这里不重复了。
c
/* Chart-Demo/src/window_main.c */
/* 改变样式 */
static ret_t change_style(widget_t* win, const char* name, const char* style, bool_t flag) {
widget_t* w = widget_lookup(window_manager(), name, TRUE);
if (w) {
if (flag) {
char style_en[20];
tk_snprintf(style_en, sizeof(style_en), "%s_en", style);
widget_use_style(w, style_en);
} else {
widget_use_style(w, style);
}
}
return RET_OK;
}
(2)界面切换
通过函数widget_on给相应的控件注册"EVT_CLICK"点击事件,进入仪表盘、饼图、曲线图和柱状图界面,代码如下:
c
/* Chart-Demo/src/window_main.c */
/* 子控件初始化(主要是设置click回调、初始显示信息) */
static ret_t init_widget(void* ctx, const void* iter) {
widget_t* widget = WIDGET(iter);
(void)ctx;
if (widget->name != NULL) {
const char* name = widget->name;
if (tk_str_eq(name, "meter") || tk_str_eq(name, "meter_image")) {
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_meter, win);
} else if (tk_str_eq(name, "pie") || tk_str_eq(name, "pie_image")) {
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_pie, win);
} else if (tk_str_eq(name, "graph") || tk_str_eq(name, "graph_image")) {
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_graph, win);
} else if (tk_str_eq(name, "histogram") || tk_str_eq(name, "histogram_image")) {
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_histogram, win);
}
}
return RET_OK;
}
2. 实现仪表盘逻辑
仪表盘界面实现的功能点如下:
- 绘制仪表盘
- 启动仪表盘
- 停止仪表盘
下面,介绍如何实现这些功能点。
(1) 绘制仪表盘
通过AWTK自带的guage控件实现仪表盘的绘制,代码如下:
xml
<!-- Chart-Demo/res_800_480/assets/default/raw/ui/window_meter.xml -->
<window anim_hint="htranslate" tr_text="meter_title">
<view name="guage_function_view" x="0" y="0" w="87%" h="90%" layout="r:1 c:2 s:25" style="dark">
<guage image="guage_bg_1" >
<gauge_pointer name="left_pointer" x="c" y="m" w="12" h="160"
value="-128" image="gauge_pointer_1"
animation="value(from=-128, to=128, yoyo_times=0, forever=TRUE, duration=3000,
delay=1000)"/>
</guage>
<guage image="guage_bg_2" >
<gauge_pointer name="right_pointer" x="c" y="m" w="12" h="160"
value="-128" image="gauge_pointer_2"
animation="value(from=-128, to=128, yoyo_times=0, forever=TRUE, duration=3000,
delay=1000)"/>
</guage>
</view>
...
</window>
(2) 启动仪表盘(界面右边的第一个按钮)
完成调用widget_start_animator开始动画,实现启动仪表盘,代码如下:
c
/* Chart-Demo/src/window_meter.c */
/* 点击开始按钮 */
static ret_t on_start(void* ctx, event_t* e) {
widget_t* win = (widget_t*)ctx;
set_btn_style(win, e);
widget_start_animator(NULL, NULL);
return RET_OK;
}
(3) 停止仪表盘(界面右边的第二个按钮)
调用widget_stop_animator停止动画,实现停止仪表盘,代码如下:
c
/* Chart-Demo/src/window_meter.c */
/* 点击停止按钮 */
static ret_t on_stop(void* ctx, event_t* e) {
widget_t* win = (widget_t*)ctx;
set_btn_style(win, e);
widget_stop_animator(NULL, NULL);
return RET_OK;
}
运行效果如下图所示:
3. 实现饼图逻辑
饼图界面实现的功能点如下:
- 绘制饼图
- 生成新的饼图/环形图
- 饼图和环形图之间的切换
下面,介绍如何实现这些功能点。
(1)绘制饼图
饼图是一个自定义控件,通过重新定义控件的虚函数表(主要是create、on_paint_self、on_destroy、set_prop、get_prop等函数),并使用widget_factory_register注册到AWTK中,即可在xml中直接使用,代码如下:
c
/* Chart-Demo/src/custom_widgets/pie_slice/pie_slice.c */
static ret_t pie_slice_on_paint_self(widget_t* widget, canvas_t* c) {
...
if (vg != NULL && (has_image || color.rgba.a)) {
xy_t cx = widget->w / 2;
xy_t cy = widget->h / 2;
float_t end_angle = 0;
float_t r = 0;
bool_t ccw = pie_slice->counter_clock_wise;
float_t start_angle = TK_D2R(pie_slice->start_angle);
float_t angle = (M_PI * 2 * pie_slice->value) / pie_slice->max;
if (ccw) {
end_angle = start_angle - angle + M_PI * 2;
} else {
end_angle = start_angle + angle;
}
vgcanvas_save(vg);
vgcanvas_translate(vg, c->ox, c->oy);
if (end_angle > start_angle) {
vgcanvas_set_fill_color(vg, color);
vgcanvas_begin_path(vg);
r = tk_min(cx, cy);
r -= pie_slice->explode_distancefactor;
if (pie_slice->is_semicircle) {
if (ccw) {
start_angle = M_PI * 2 - (start_angle + angle) / 2;
end_angle = start_angle + angle / 2;
cy = cy + r * 0.5;
r += pie_slice->explode_distancefactor * 1.5;
vgcanvas_arc(vg, cx, cy, r, start_angle, end_angle, !ccw);
r -= pie_slice->inner_radius;
vgcanvas_arc(vg, cx, cy, r, end_angle, start_angle, ccw);
}
} else {
vgcanvas_arc(vg, cx, cy, r, start_angle, end_angle, ccw);
if (r - pie_slice->inner_radius <= 0) {
vgcanvas_line_to(vg, cx, cy);
} else {
r -= pie_slice->inner_radius;
vgcanvas_arc(vg, cx, cy, r, end_angle, start_angle, !ccw);
}
}
vgcanvas_close_path(vg);
if (has_image) {
vgcanvas_paint(vg, FALSE, &img);
} else {
vgcanvas_fill(vg);
}
}
vgcanvas_restore(vg);
}
...
}
(2) 饼图/环形图的值动画(界面右边的第一个按钮)
生成新的饼图/环形图,实现步骤如下:
首先,通过创建值动画将原来的饼图/环形图还原到原点,代码如下:
c
/* Chart-Demo/src/window_pie.c */
/* 创建扇形还原到原点动画 */
static ret_t create_animator_to_zero(widget_t* win) {
set_btn_enable(win, FALSE);
widget_t* pie_view = widget_lookup(win, "pie_view", TRUE);
if (pie_view) {
WIDGET_FOR_EACH_CHILD_BEGIN_R(pie_view, iter, i)
value_t v;
widget_get_prop(iter, PIE_SLICE_PROP_IS_EXPLODED, &v);
save_pie_exploded[i] = value_bool(&v);
pie_slice_t* pie_slice = PIE_SLICE(iter);
if (pie_slice->is_exploded) {
pie_slice_set_exploded(iter);
}
int32_t delay = 80;
delay = delay * (nr - 1 - i);
char param[100];
tk_snprintf(param, sizeof(param),
"value(to=0, duration=50, delay=%d, easing=sin_out)", delay);
widget_create_animator(iter, param);
WIDGET_FOR_EACH_CHILD_END();
}
return RET_OK;
}
然后,通过定时器判断上面的动画是否已经全部还原到原点,如果是重置饼图/环形图的值数据,代码如下:
c
/* Chart-Demo/src/window_pie.c */
/* 点击创建新饼图或者环形图定时器 */
static ret_t on_new_pie_timer(const timer_info_t* timer) {
pie_value_t* pie_data = NULL;
widget_t* win = WIDGET(timer->ctx);
uint32_t count = widget_animator_manager_count(widget_animator_manager());
if (count == 0) {
value_t v;
value_t v1;
ret_t result = widget_get_prop(win, "is_new", &v);
bool_t flag = (result == RET_NOT_FOUND) ? FALSE : value_bool(&v);
if (flag) {
pie_data = old_pie_data;
widget_set_prop(win, "is_new", value_set_bool(&v1, FALSE));
} else {
pie_data = new_pie_data;
widget_set_prop(win, "is_new", value_set_bool(&v1, TRUE));
}
widget_t* pie_view = widget_lookup(win, "pie_view", TRUE);
if (pie_view) {
WIDGET_FOR_EACH_CHILD_BEGIN(pie_view, iter, i)
pie_slice_t* pie_slice = PIE_SLICE(iter);
int32_t delay = DELAY_TIME;
int32_t duration = DURATION_TIME;
delay = delay * i;
const pie_value_t* new_pie = pie_data + nr - 1 - i;
pie_slice_set_start_angle(iter, new_pie->start_angle);
char param[100];
tk_snprintf(param, sizeof(param),
"value(name=%s, to=%d, duration=%d, delay=%d, easing=sin_out)",
SAVE_EXPLODED,
new_pie->value, duration, delay);
widget_create_animator(iter, param);
WIDGET_FOR_EACH_CHILD_END();
}
timer_add(on_save_exploded_timer, win, 1000 / 60);
return RET_REMOVE;
}
return RET_REPEAT;
}
(3) 饼图/环形图的切换动画(界面右边的第二个按钮)
饼图和环形图之间的切换,实现步骤如下:
首先,通过创建值动画将原来的饼图/环形图还原到原点。
然后,通过定时器判断上面的动画是否已经全部还原到原点,如果是就进行饼图和环形图之间的切换,并调用pie_slice_set_semicircle设置是否是环形图,代码如下:
c
/* Chart-Demo/src/window_pie.c */
/* 环形图定时器 */
static ret_t on_arch_timer(const timer_info_t* timer) {
widget_t* win = WIDGET(timer->ctx);
uint32_t new_inner_radius = win->h / 5;
uint32_t inner_radius = 0;
widget_t* pie_view = widget_lookup(win, "pie_view", TRUE);
uint32_t count = widget_animator_manager_count(widget_animator_manager());
if (count == 0) {
value_t v;
value_t v1;
ret_t result = widget_get_prop(win, "is_arch", &v);
bool_t flag = (result == RET_NOT_FOUND) ? FALSE : value_bool(&v);
if (flag) {
widget_set_prop(win, "is_arch", value_set_bool(&v1, FALSE));
inner_radius = 550;
} else {
widget_set_prop(win, "is_arch", value_set_bool(&v1, TRUE));
inner_radius = new_inner_radius;
}
widget_t* pie_view = widget_lookup(win, "pie_view", TRUE);
if (pie_view) {
WIDGET_FOR_EACH_CHILD_BEGIN(pie_view, iter, i)
pie_slice_t* pie_slice = PIE_SLICE(iter);
pie_slice_set_inner_radius(iter, inner_radius);
if (flag) {
pie_slice_set_semicircle(iter, FALSE);
pie_slice_set_counter_clock_wise(iter, FALSE);
} else {
pie_slice_set_semicircle(iter, TRUE);
pie_slice_set_counter_clock_wise(iter, TRUE);
}
int32_t delay = DELAY_TIME;
int32_t duration = DURATION_TIME;
delay = delay * i;
const pie_value_t* new_pie = old_pie_data + nr - 1 - i;
pie_slice_set_start_angle(iter, new_pie->start_angle);
char param[100];
tk_snprintf(param, sizeof(param),
"value(name=%s, to=%d, duration=%d, delay=%d, easing=sin_out)", SAVE_EXPLODED,
new_pie->value, duration, delay);
widget_create_animator(iter, param);
WIDGET_FOR_EACH_CHILD_END();
}
timer_add(on_save_exploded_timer, win, 1000 / 60);
return RET_REMOVE;
}
return RET_REPEAT;
}
运行效果如下图所示:
4. 实现曲线图逻辑
曲线图界面实现的功能点如下:
- 绘制曲线图
- 随机生成新的曲线图
- 设置是否显示曲线
- 设置是否显示闭合区域
- 设置是否显示圆点
- 设置是否显示平滑曲线
下面,介绍如何实现这些功能点。
(1)绘制曲线图
曲线图同样是一个自定义控件,该控件中X轴、Y轴、曲线等是一个个独立的元素,各自完成自身的绘制(比如曲线line_series仅负责将数据点按指定波形显示),但布局受父控件chart_view控制。代码如下:
c
/* Chart-Demo/src/custom_widgets/chart/line_series.c */
static ret_t line_series_on_paint_self(widget_t* widget, canvas_t* c) {
line_series_t* series = LINE_SERIES(widget);
return_value_if_fail(series != NULL, RET_BAD_PARAMS);
line_series_start_init_if_not_inited(widget);
series_p_reset_fifo(widget);
if (series->base.display_mode == SERIES_DISPLAY_COVER) {
return series_p_on_paint_self_cover(widget, c);
} else {
return series_p_on_paint_self_push(widget, c);
}
}
(2) 随机生成新的曲线图(界面右边的第一个按钮)
通过widget_on给该按钮注册on_series_rset_rand_ufloat_data响应事件,然后在函数on_series_rset_rand_ufloat_data中间接调用rand函数生成随机值,最后将生成的随机值通过series_push函数设置给控件,代码如下:
c
/* Chart-Demo/src/window_line_series.c */
static void init_normal_line_series_window(widget_t* widget) {
widget_t* new_graph = widget_lookup(widget, "new_graph", TRUE);
bool_t bring_to_top = strstr(widget->name, "_more_axis") != NULL ? TRUE : FALSE;
if (new_graph != NULL) {
widget_on(new_graph, EVT_CLICK, on_series_rset_rand_ufloat_data, widget);
}
...
}
c
/* Chart-Demo/src/window_series_common.c */
ret_t on_series_rset_rand_ufloat_data(void* ctx, event_t* e) {
widget_t* win = WIDGET(ctx);
widget_t* chart_view = widget_lookup(win, "chartview", TRUE);
if (chart_view) {
on_series_rset_ufloat_data(chart_view, get_series_capacity_min(chart_view));
}
return RET_OK;
}
void on_series_rset_ufloat_data(widget_t* widget, uint32_t count) {
on_series_rset_data(widget, count, sizeof(float_t), generate_ufloat_data);
}
static void generate_ufloat_data(void* buffer, uint32_t size) {
uint32_t i;
float_t* b = (float_t*)buffer;
for (i = 0; i < size; i++) {
b[i] = (float_t)(rand() % 120 + 10);
}
}
(3)设置是否显示曲线(界面右边的第二个按钮)
通过设置控件的SERIES_PROP_LINE_SHOW属性是否显示曲线,代码如下:
c
/* Chart-Demo/src/window_series_common.c */
ret_t on_series_line_show_changed(void* ctx, event_t* e) {
return on_series_prop_changed(ctx, e, SERIES_PROP_LINE_SHOW, "line", "line", "line_select");
}
static ret_t on_series_prop_changed(void* ctx, event_t* e, const char* prop, const char* btn_name,
const char* style, const char* style_select) {
widget_t* win = WIDGET(ctx);
widget_t* chart_view = widget_lookup(win, "chartview", TRUE);
if (chart_view) {
WIDGET_FOR_EACH_CHILD_BEGIN(chart_view, iter, i)
if (widget_is_series(iter)) {
value_t v;
if (widget_get_prop(iter, prop, &v) == RET_OK) {
value_set_bool(&v, !value_bool(&v));
widget_set_prop(iter, prop, &v);
widget_t* btn = widget_lookup(win, btn_name, TRUE);
if (btn) {
widget_use_style(btn, value_bool(&v) ? style_select : style);
}
}
}
WIDGET_FOR_EACH_CHILD_END()
}
return RET_OK;
}
(4)设置是否显示闭合区域(界面右边的第三个按钮)
通过设置控件的SERIES_PROP_LINE_AREA_SHOW属性是否显示闭合区域,代码如下:
c
/* Chart-Demo/src/window_series_common.c */
ret_t on_series_area_show_changed(void* ctx, event_t* e) {
return on_series_prop_changed(ctx, e, SERIES_PROP_LINE_AREA_SHOW, "area", "area", "area_select");
}
(5)设置是否显示圆点(界面右边的第四个按钮)
通过设置控件的SERIES_PROP_SYMBOL_SHOW属性是否显示圆点,代码如下:
c
/* Chart-Demo/src/window_series_common.c */
ret_t on_series_symbol_show_changed(void* ctx, event_t* e) {
return on_series_prop_changed(ctx, e, SERIES_PROP_SYMBOL_SHOW, "symbol", "symbol",
"symbol_select");
}
(6)设置是否显示平滑曲线(界面右边的第五个按钮)
通过设置控件的SERIES_PROP_LINE_SMOOTH属性是否显示平滑曲线,代码如下:
c
/* Chart-Demo/src/window_series_common.c */
ret_t on_series_smooth_changed(void* ctx, event_t* e) {
return on_series_prop_changed(ctx, e, SERIES_PROP_LINE_SMOOTH, "smooth", "smooth",
"smooth_select");
}
运行效果如下图所示:
5. 实现柱状图逻辑
柱状图界面实现的功能点如下:
- 绘制柱状图
- 随机生成新的柱状图
下面,介绍如何实现这些功能点。
(1)绘制柱状图
柱状图与上面曲线图原理类似,不同的是数据显示改为使用bar_series,bar_series负责将数据点显示为一组组的柱条。代码如下:
c
/* Chart-Demo/src/custom_widgets/chart/bar_series.c */
static ret_t bar_series_on_paint(widget_t* widget, canvas_t* c, float_t ox, float_t oy,
fifo_t* fifo, uint32_t index, uint32_t size, rect_t* clip_rect) {
return bar_series_on_paint_internal(widget, c, ox, oy, fifo, index, size, clip_rect, FALSE);
}
(2) 随机生成新的柱状图(界面右边的第一个按钮)
生成新的柱状图的逻辑与曲线图页面的差不多,这里就不累赘了,代码如下:
c
/* Chart-Demo/src/window_bar_series.c */
static void init_normal_bar_series_window(widget_t* widget) {
widget_t* new_graph = widget_lookup(widget, "new_graph", TRUE);
if (new_graph != NULL) {
widget_on(new_graph, EVT_CLICK, on_series_rset_rand_ufloat_data, widget);
}
}
运行效果如下图所示:
9.4 音乐播放器
9.4.1 功能详解
音乐播放器实现的功能点主要有下面几点:
- 黑胶唱片动画效果
- 显示歌词
- 切换歌曲
- 播放列表
- 均衡器
- 旋钮音频参数调节
9.4.2 应用实现
本案例使用目录结构、编译和运行步骤与第2章介绍的hello world应用类似,这里就不重复介绍了。
本案例使用XML设计界面,业务逻辑使用C语言实现。主界面中左侧中部的黑胶唱片音乐播放场景主要通过widget_animation和image_animation动画配合image、gauge_pointer、framer_view控件实现。主界面右侧中部的歌词主要通过list_view、scroll_view和list_item的组合使用实现。主界面底部为播放功能选项,其中左侧三个按钮实现前一首、播放/暂停、下一首的功能;中间部分用两个label显示时间,一个silder显示进度,右侧两个按钮分别实现切换播放模式以及打开歌曲列表,功能主要通过注册点击事件实现。在主界面的右上方为三个按钮,从上到下第一个按钮实现黑胶唱片场景动画的开启或者关闭,其余两个则按钮通过注册点击事件进入相应的界面。运行的效果详见下图。
本案例使用的代码均可以在MusicPlay-Demo目录找到。
1. 实现黑胶唱片动画效果
(1)先创建两个image控件和一个gauge_pointer控件来分别实现唱片、歌曲封面和唱针的绘制。代码如下:
xml
<!-- MusicPlayer-Demo/res_800_480/assets/default/raw/ui/main.xml -->
<frame_view name="frame_menu" x="-20" y="90" w="450" h="300" align_v="bottom" >
<view x="0" y="0" w="300" h="300">
<image name="song_y" w="188" h="188" image="yellow" draw_type="default" />
<image name="Vinyl" image="Vinyl" w="300" h="300" draw_type="default" />
</view>
<view x="0" y="0" w="300" h="300">
<image name="song_q" w="188" h="188" image="see_you_again" draw_type="default" />
<image name="Vinyl" image="Vinyl" w="300" h="300" draw_type="default" />
</view>
<view x="0" y="0" w="250" h="250">
<image name="song_n" w="188" h="188" image="let_it_go" draw_type="default" />
<image name="Vinyl" image="Vinyl" w="300" h="300" draw_type="default" />
</view>
</frame_view>
<gauge_pointer name="vinyl_head" x="165" y="50" w="20" h="20" angle="-70" image="gauge_pointer"/>
(2) 唱片、歌曲封面和唱针的旋转摆动主要通过widget_create_animator创建控件动画来实现,代码如下:
c
/* MusicPlayer-Demo/src/window_main.c */
/* 封面旋转动画 */
ret_t swtich_frame_rotation_animator(widget_t* win, bool_t start_anim) {
return_value_if_fail(win != NULL, RET_BAD_PARAMS);
widget_t* frame_menu = widget_lookup(win, "frame_menu", TRUE);
frame_view_t* frame_view = FRAME_VIEW(frame_menu);
widget_t* btn_anim_switch = widget_lookup(win, "btn_anim_switch", TRUE);
widget_t* frame_child = frame_menu->children->elms[frame_view->value];
if (start_anim && tk_str_eq(btn_anim_switch->style, "s_anim_switch_p")) {
widget_create_animator(frame_child->children->elms[0],
"rotation(from=0, to=628, duration=800000, repeat_times=0)");
} else {
widget_stop_animator(frame_child->children->elms[0], "rotation");
widget_destroy_animator(frame_child->children->elms[0], "rotation");
}
return RET_OK;
}
/* 唱针动画 */
ret_t swtich_frame_vinyl_head_animator(widget_t* win, bool_t start_anim) {
return_value_if_fail(win != NULL, RET_BAD_PARAMS);
widget_t* vinyl_head = widget_lookup(win, "vinyl_head", TRUE);
if (start_anim) {
widget_create_animator(vinyl_head, "value(from=-70, to=-35, duration=300, delay=0)");
} else {
widget_create_animator(vinyl_head, "value(from=-35, to=-70, duration=300, delay=0)");
}
}
2. 实现显示歌词
歌词的显示主要由控件list_view、list_item和scroll_view来实现,其中list_item的添加不在XML中进行,主要通过在代码中解析歌词文件,每解析一行便创建一个list_time作为list_view的子控件。代码如下:
xml
<!-- MusicPlayer-Demo/res_800_480/assets/default/raw/ui/main.xml -->
<list_view name="lrc_view" x="45%" y="30%" w="55%" h="50%" item_height="25" style="empty">
<scroll_view name="lrc_scroll" x="0" y="0" w="100%" h="100%"
xslidable="FALSE" yslidable="TRUE" style="empty">
</scroll_view>
<scroll_bar name="bar" x="right" y="0" w="0" h="0" value="0"/>
</list_view>
c
/* MusicPlayer-Demo/src/custom_function/music_manager.c */
/* 歌词解析 */
static ret_t parse_lrc_line(widget_t* win, const char* name) {
......
while (right != NULL && left != NULL) {
strncpy(buff, left + 1, right - left - 4);
widget_set_text_utf8(slider_max_label, buff);
p2 = strrchr(p, ']');
if (p2 != NULL) {
p2++;
value_set_int32(&v_lrc_time, timetosec(buff));
if (lrc_scroll->children == NULL || lrc_scroll->children->size < n + 1)
list_item_create(lrc_scroll, 0, 0, 0, 0); //添加list_item
widget_use_style(lrc_scroll->children->elms[n], "empty");
widget_invalidate(lrc_scroll->children->elms[n], NULL);
setlocale(LC_CTYPE, "");
widget_set_prop(lrc_scroll->children->elms[n], "lrc_time", &v_lrc_time);
widget_set_text_utf8(lrc_scroll->children->elms[n], p2); //写入歌词
widget_set_visible(lrc_scroll->children->elms[n], TRUE, FALSE);
slider_set_max(slider, value_int32(&v_lrc_time));
n++;
}
......
}
......
return RET_OK;
}
3. 实现切换歌曲
(1) 实现切换歌曲封面
此功能点主要使用自定义容器控件frame_view实现,关于自定义控件的定义、注册请参考10.3.2章节.此处便不再赘述。此容器主要实现的是类似于幻灯片的动画效果,首先,创建一个frame_view容器控件;其次,在frame_view中添加待播放子控件;最后,通过函数frame_view_set_then来切换上一个或者下一个子控件,代码如下:
c
/* MusicPlayer-Demo/src/custom_widgets/photo_frame/frame_view.c */
ret_t frame_view_set_then(widget_t* widget, bool_t operate) {
frame_view_t* frame_view = FRAME_VIEW(widget);
return_value_if_fail(frame_view != NULL, RET_BAD_PARAMS);
int32_t xoffset_end = 0;
if (operate)
xoffset_end = widget->h * -1;
else
xoffset_end = widget->h;
frame_view_scroll_to(WIDGET(frame_view), xoffset_end);
return RET_OK;
}
(2) 实现切换歌曲时间和歌词
在歌曲封面滚动动画完成后,可通过获取frame_view当前显示的子控件(也就是歌曲封面)的序号(frame_view->value)来选择对应的歌词进行解析,最后将歌曲封面、歌词和歌曲时间信息同步到界面上,代码如下:
c
/* MusicPlayer-Demo/src/window_main.c */
static ret_t on_frame_menu_vchange(void* ctx, event_t* e) {
widget_t* win = WIDGET(ctx);
widget_t* frame_menu = WIDGET(e->target);
frame_view_t* frame_view = FRAME_VIEW(frame_menu);
init_player(win);
load_song(win, frame_view->value, TRUE);//通过frame_view->value加载对应歌曲信息
return RET_OK;
}
4. 实现播放列表
(1)实现播放列表
通过list_view、list_scroll、list_item控件实现播放列表的绘制,代码如下:
xml
<!-- MusicPlayer-Demo/res_800_480/assets/default/raw/ui/play_list.xml-->
<list_view name="song_list_view" x="0" y="16%" w="100%" h="83%" item_height="35" style="empty">
<scroll_view name="song_list_scroll" x="0" y="0" w="100%" h="100%"
xslidable="FALSE" yslidable="TRUE" style="empty">
<list_item name="0_song" style="empty_list" layout="r1 c0">
<image draw_type="icon" w="10%" image="icon_list" visible="FALSE"/>
<label w="90%" text="Yellow" style="left_list"/>
</list_item>
<list_item name="1_song" style="empty_list" layout="r1 c0">
<image draw_type="icon" w="10%" image="icon_list" visible="FALSE"/>
<label w="90%" text="see you again" style="left_list"/>
</list_item>
<list_item name="2_song" style="empty_list" layout="r1 c0" image="icon_list">
<image draw_type="icon" w="10%" image="icon_list" visible="FALSE"/>
<label w="90%" text="let it go" style="left_list"/>
</list_item>
</scroll_view>
</list_view>
(2) 通过播放列表切换歌曲
首先为list_item控件注册事件,在所注册的事件函数中进一步响应frame_view所注册事件函数on_frame_menu_vchange切换所选择的歌曲进行播放,代码如下:
c
/* MusicPlayer-Demo/src/window_play_list.c */
static ret_t on_song_item_down(void* ctx, event_t* e) {
if (m_win == NULL) return RET_FAIL;
......
if (frame_view->value != iter_index) {
if (vh->angle == -70) {
widget_create_animator(vinyl_head, "value(from=-70, to=-35, duration=300, delay=0)");
widget_start_animator(vinyl_head, "value");
}
init_player(m_win);
swtich_frame_rotation_animator(m_win, FALSE);
frame_view_set_value(frame_menu, iter_index);//响应EVT_VALUE_CHANGED事件
}
return RET_OK;
}
c
/* MusicPlayer-Demo/src/window_main.c */
static ret_t on_frame_menu_vchange(void* ctx, event_t* e) {
widget_t* win = WIDGET(ctx);
widget_t* frame_menu = WIDGET(e->target);
frame_view_t* frame_view = FRAME_VIEW(frame_menu);
init_player(win);
load_song(win, frame_view->value, TRUE);//切换歌曲
return RET_OK;
}
运行效果如下图所示:
5. 实现均衡器逻辑
(1)绘制滑动条和曲线图
通过slider控件实现滑动条的绘制,曲线图是一个自定义控件,布局受到char_view影响,代码如下:
xml
<!-- MusicPlayer-Demo/res_800_480/assets/default/raw/ui/equalizer.xml -->
<dialog anim_hint="fade" self_layout="default(x=400,y=53,w=400,h=355)" style="bg_setting">
<chart_view name="chartview" x="0" y="75%" w="100%" h="25%" style="default">
<x_axis name="x" w="100%" h="100%" axis_type="value" min="0" max="8"
tick="{show:false}" split_line="{show:false}" label="{show:false}"
data="[0,0,0,0,0,0,0,0,0]"/>
<y_axis name="y" w="100%" h="100%" axis_type="value" min="-15" max="120"
tick="{show:false}" split_line="{show:false}" label="{show:false}"
data="[0,10,20,30,40,50,60,70,80]"/>
<line_series name="s1" w="100%" h="100%" capacity="10" line="{style:s1, smooth:true}"
area="{show:false}" symbol="{show:false}"/>
<tooltip />
</chart_view>
</dialog>
(2)改变频率并生成对应频率波形
首先通过widget_on注册slider的EVT_VALUE_CHANGING事件处理函数,然后在该函数(set_series_data)中完成slider数值和波形图的绘制,代码如下:
c
/* awtk_projects/MusicPlayer-Demo/src/dialog_equalizer.c */
static ret_t on_slider_changing(void* ctx, event_t* e) {
widget_t* win = WIDGET(ctx);
widget_t* child = WIDGET(e->target);
char name_buf[BUFF_LEN] = {0};
strcpy(name_buf, child->name);
strtok(name_buf, "_");
int32_t i = atoi(strtok(NULL, "_"));
val_slider[i] = widget_get_value(child);
widget_t* chart_view = widget_lookup(win, "chartview", TRUE);
if (chart_view) {
set_series_data(val_slider, chart_view, SLIDER_COUNT);
}
return RET_OK;
}
运行的效果如下图所示:
- 实现旋钮音频参数调节
(1)绘制表盘指针和进度圆环控件
创建gauge_pointer和progress_circle控件实现旋钮和旋钮刻度线的绘制,代码如下:
xml
<!-- MusicPlayer-Demo/res_800_480/assets/default/raw/ui/advanced.xml-->
<view name="gauge_pointer_view" x="0" y="10%" w="100%" h="100%">
<view name="guage_low" x="20%" y="10%" w="120" h="120">
<label x="left" y="middle:35" w="10" h="10" text="-"/>
<label x="right" y="middle:35" w="10" h="10" text="+"/>
<label x="center" y="bottom" w="10" h="10" tr_text="Low"/>
<progress_circle name="circle_low" w="120" h="120" max="360" style="blue" start_angle="150" value="120" line_width="2"/>
<gauge_pointer x="17" y="17" name="low_gauge_pointer" w="86" h="86" image="btn_round"/>
</view>
...
<view name="guage_deep" x="60%" y="50%" w="120" h="120">
<label x="left" y="middle:35" w="10" h="10" text="-"/>
<label x="right" y="middle:35" w="10" h="10" text="+"/>
<label x="center" y="bottom" w="10" h="10" tr_text="SURR-Depth"/>
<progress_circle name="circle_deep" w="120" h="120" max="360" style="blue" start_angle="150" value="120" line_width="2"/
<gauge_pointer x="17" y="17" name="deep_gauge_pointer" w="86" h="86" image="btn_round"/>
</view>
</view>
(2)实现旋钮转动
通过注册gauge_pointer的EVT_POINTER_DOWN和EVT_POINTER_MOVE事件函数获取实时坐标点来计算所转动的角度,然后重新绘制gauge_pointer和progress_circle,代码如下:
c
/* MusicPlayer-Demo/src/custom_function/knob_function.c */
ret_t knob_angle_change(widget_t* circle, widget_t* knob_pointer, point_t p) {
..
if (tk_str_eq(value_str(&val_pointer_name), knob_pointer->name) &&
value_str(&val_pointer_name) != NULL) {
int32_t angle_temp = value_int32(&val_angle) + (mv - value_int32(&val_start_P));
if ((widget_get_prop(knob_pointer, "s_pmove", &val) == RET_OK && value_bool(&val) == TRUE)) {
if (mv > 150 || mv < -150) {
value_set_bool(&val, FALSE);
value_set_int32(&val_angle, angle_temp);
widget_set_prop(knob_pointer, "s_pmove", &val);
widget_set_prop(knob_pointer, "val_angle", &val_angle);
return RET_OK;
}
if (angle_temp > 120) angle_temp = 120;
if (angle_temp < -120) angle_temp = -120;
gauge_pointer_set_angle(knob_pointer, angle_temp);//重新绘制gauge_pointer角度
progress_circle_set_value(circle, angle_temp + 120);// 重新绘制progress_circle角度
} else {
if (mv <= 150 && mv >= -150) {
value_set_bool(&val, TRUE);
value_set_int32(&val_start_P, mv);
widget_set_prop(knob_pointer, "s_pmove", &val);
widget_set_prop(knob_pointer, "val_start_P", &val_start_P);
}
}
}
return RET_OK
}
运行效果如下图所示:
9.5 智能手表
9.5.1 功能详解
智能手表实现的功能点主要有下面几点:
- 表盘:右滑切换表盘、长按预览及选择表盘、下滑打开消息列表、上滑打开快捷操作界面、左滑打开应用列表
- 日历应用:右滑返回、上滑切换到下个月、下滑切换到上个月、天气应用
- 音乐应用:右滑返回、播放与暂停、播放模式、切歌、改变音量
- 健康应用:右滑返回、设定目标
- 心率检测:右滑返回、检测心率
9.5.2 应用实现
本应用包括四个表盘:数字和模拟表盘各有两个。每个表盘都具有不同功能,具体如下:
- 模拟表盘一:具有动态显示电量、查看天气及记录和动态显示健康数据的功能,详见下图:
- 数字表盘一:具有显示时间、动态更新刻度(刻度值表示秒)的功能,详见下图:
- 数字表盘二:具有显示天气、日历和健康数据、播放音乐以及动态显示心率图的功能,详见下图:
- 模拟表盘二:具有显示天气和日历以及播放音乐的功能,其中音乐圆环进度与音乐应用同步,下面几个消息提示图标也是动态变化,详见下图:
1. 实现手势识别
通过gesture手势控件来实现的,通过比对手势按下与松开时的坐标,从而判断其方向及距离,代码如下:
c
/* SmartWatch-Demo/src/custom_widgets/ gesture.c */
static ret_t gesture_on_event(widget_t* widget, event_t* e) {
uint16_t type = e->type;
gesture_t* gesture = GESTURE(widget);
...
case EVT_POINTER_UP: {
pointer_event_t* pointer_event = (pointer_event_t*)e;
(void)pointer_event;
if (gesture->child_graded) {
return RET_OK;
}
if (gesture->pressed) {
pointer_event_t evt = *(pointer_event_t*)e;
if (evt.x != gesture->press_point.x || evt.y != gesture->press_point.y) {
float_t distance_x = gesture->press_point.x - evt.x;
float_t distance_y = gesture->press_point.y - evt.y;
distance_x = distance_x > 0 ? distance_x : -distance_x;
distance_y = distance_y > 0 ? distance_y : -distance_y;
if (distance_x > distance_y) {
if (distance_x < 40 && distance_x < widget->w * 0.2f) {
gesture_pointer_up_cleanup(widget);
break;
}
if (evt.x > gesture->press_point.x) {
log_debug("slide right\n");
evt.e = event_init(EVT_SLIDE_RIGHT, widget);
widget_dispatch(widget, (event_t*)&evt);
} else {
log_debug("slide left\n");
evt.e = event_init(EVT_SLIDE_LEFT, widget);
widget_dispatch(widget, (event_t*)&evt);
}
} else {
if (distance_y < 40 && distance_y < widget->h * 0.2f) {
gesture_pointer_up_cleanup(widget);
break;
}
if (evt.y > gesture->press_point.y) {
log_debug("slide down\n");
evt.e = event_init(EVT_SLIDE_DOWN, widget);
widget_dispatch(widget, (event_t*)&evt);
} else {
log_debug("slide up\n");
evt.e = event_init(EVT_SLIDE_UP, widget);
widget_dispatch(widget, (event_t*)&evt);
}
}
}
...
}
2. 实现表盘长按预览
表盘预览窗口主要用了slide_menu控件布局,表盘长按预览的效果详见下图,代码如下:
xml
<!--SmartWatch-Demo/res_800_480/assets/raw/ui/select_dial.xml-->
<window anim_hint="slide_right" style="select_dial" full_screen="false" x="c" y="m" w="390" h="390"
theme="select_dial_styles">
<view x="0" y="0" w="100%" h="100%" name="select:gesture">
<slide_menu name="select:slideview" x="c" y="m" w="240%" h="70%" style="dot" min_scale="1.0">
<view x="0" y="0" w="100%" h="100%">
<button name="watch_04" style="watch_04" x="c" y="m" w="258" h="258"/>
</view>
<view x="0" y="0" w="100%" h="100%">
<button name="watch_01" style="watch_01" x="c" y="m" w="258" h="258"/>
</view>
<view x="0" y="0" w="100%" h="100%">
<button name="watch_02" style="watch_02" x="c" y="m" w="258" h="258"/>
</view>
<view x="0" y="0" w="100%" h="100%">
<button name="watch_06" style="watch_06" x="c" y="m" w="258" h="258"/>
</view>
</slide_menu>
<view name="select:digit_btns" x="c" y="m" w="80%" h="80%" visible="false" sensitive="false"
style="digit_btns" >
<button name="select:top" x="126" y="45" w="60" h="60" text="Top" />
<button name="select:bottom" x="126" y="165" w="60" h="60" text="Bottom" />
<button name="select:left" x="66" y="105" w="60" h="60" text="Left" />
<button name="select:right" x="186" y="105" w="60" h="60" text="Right" />
</view>
<view name="select:time_btns" x="c" y="m" w="80%" h="80%" visible="false" sensitive="false"
style="time_btn" >
<button name="select:1" x="31" y="30" w="60" h="60" text="1" />
<button name="select:2" x="31" y="210" w="60" h="60" />
<button name="select:3" x="126" y="210" w="60" h="60" />
<button name="select:4" x="221" y="210" w="60" h="60" />
</view>
<button name="select:done" style="music" x="c" y="bottom" w="100%" h="30" text="完成"
visible="false" sensitive="false" />
</view>
</window>
3. 实现音乐应用
切歌动画主要通过两个动画实现,先把当前歌曲切走,再把新歌曲切回来,详见下图:
切歌实现的步骤如下:
步骤一:首先记住音乐的播放状态,切歌过程中是不播放的,通过动画把歌名标签移动到视野外,代码如下:
c
/* SmartWatch-Demo/src/application/ music.c */
static ret_t on_music_next(void* ctx, event_t* e) {
(void)e;
widget_t* win = WIDGET(ctx);
music_state_t* music_state = &(global_data.music_state);
music_state->play_progress = 0; /* 进度变为0 */
widget_t* widget = widget_lookup(win, "music:progress", TRUE);
if (widget) {
health_circle_set_value_b(widget, music_state->play_progress);
widget_invalidate(widget, NULL);
}
/* 切歌 */
widget = widget_lookup(win, "music:mode", TRUE);
if (widget != NULL) {
music_state->song_index = next_index(music_state);
}
widget_t* label = widget_lookup(win, "music:song_name", TRUE);
if (label != NULL) {
const char* name = "music_next";
widget_create_animator_with(label, "x(name=%s, from=%d, to=%d, duration=%d)",
name, 0, label->w, 250);
widget_animator_t* animator = widget_find_animator(label, name);
if (animator != NULL) {
uint8_t play_state = music_state->play_state;
if (music_state->play_state == 1) {
music_state->play_state = 0; // 先暂停
}
widget_animator_on(animator, EVT_ANIM_END, on_music_next_play_anim_end,
tk_pointer_from_int(play_state));
}
}
return RET_OK;
}
步骤二:然后再用动画将已经更换歌名的标签从视野外移动到中间,代码如下:
c
/* SmartWatch-Demo/src/application/music.c */
static ret_t on_music_next_play_anim_end(void* ctx, event_t* e) {
(void)e;
uint8_t play_state = tk_pointer_to_int(ctx);
music_state_t* music_state = &(global_data.music_state);
timer_info_t timer;
timer.ctx = music_win;
on_update_timer(&timer);
widget_t* label = widget_lookup(music_win, "music:song_name", TRUE);
if (label != NULL) {
widget_create_animator_with(label, "x(from=%d, to=%d, duration=%d)",
-label->w, 0, 250);
}
music_state->play_state = play_state;
return RET_OK;
}
4. 实现天气应用
(1)显示天气的效果详见下图
(2)小时天气界面主要用到了slide_view布局,其中小时天气用了hour_weather自定义控件,其中点击中间环会更新温度值,效果详见下图,代码如下:
c
/* SmartWatch-Demo/src/custom_widgets/hour_weather.c */
static ret_t hour_weather_on_event(widget_t* widget, event_t* e) {
uint16_t type = e->type;
hour_weather_t* hour_weather = HOUR_WEATHER(widget);
switch (type)
{
case EVT_POINTER_DOWN: {
hour_weather->pressed = TRUE;
break;
}
case EVT_POINTER_UP: {
pointer_event_t* pointer_event = (pointer_event_t*)e;
point_t point = { pointer_event->x, pointer_event->y };
widget_to_local(widget, &point);
log_debug("(%d, %d) clicked!\n", (int32_t)point.x, (int32_t)point.y);
xy_t x_c = point.x - widget->w / 2;
xy_t y_c = point.y - widget->h / 2;
hour_weather->selected = hour_health_get_click_select(widget, x_c, y_c);
on_change_select(widget);
hour_weather->pressed = FALSE;
event_t event = event_init(EVT_CHANGE_SELECT, widget);
widget_dispatch(widget, &event);
break;
}
default:
break;
}
return RET_OK;
}
5. 实现健康应用
健康应用主要是slide_view布局,包括了四个界面,下面主要看看其中的两个界面。
(1)主界面中的分时控件使用了自定义的bar_graph控件,详见下图:
(2)活动界面使用了自定义的health_circle圆环控件(基于progress_circle改造),加了拖动可以动态改变数值的功能,详见下图:
6. 实现日历应用
日历应用布局主要通过view控件实现,详见下图:
其中,切换月份通过自定义动画实现,步骤如下:
步骤一:先通过动画把当前月份界面移走,代码如下:
c
/* SmartWatch-Demo/src/application/calendar.c */
static ret_t on_calendar_down_prev(void* ctx, event_t* e) {
(void)e;
widget_t* window = widget_get_window(WIDGET(ctx));
widget_t* widget = widget_lookup(window, s_calendar_labels, TRUE);
if (widget != NULL) {
widget_t* calendar_date = widget_lookup(window, s_calendar_date, TRUE);
wh_t h = widget->h;
(void)h;
if (calendar_date != NULL) {
const char* name = "down";
widget_create_animator_with(widget, "y(name=%s, from=%d, to=%d, duration=%d)",
name, widget->y - 10, window->h, 500);
widget_animator_t* animator = widget_find_animator(widget, name);
if (animator != NULL) {
widget_animator_on(animator, EVT_ANIM_END, on_calendar_down, window);
}
}
}
return RET_OK;
}
步骤二:切走当前月份后再切换到下月界面,更换完成也是通过动画载入下月界面,代码如下:
c
/* SmartWatch-Demo/src/application/calendar.c */
static ret_t on_calendar_down(void* ctx, event_t* e) {
(void)e;
if (date_current.month == 12) {
date_current.month = 1;
date_current.year++;
} else {
date_current.month += 1;
}
calendar_cal(ctx, &date_current);
widget_t* window = widget_get_window(WIDGET(ctx));
widget_t* widget = widget_lookup(window, s_calendar_labels, TRUE);
if (widget != NULL) {
widget_t* calendar_date = widget_lookup(window, s_calendar_date, TRUE);
wh_t h = widget->h;
if (calendar_date != NULL) {
widget_create_animator_with(widget, "y(from=%d, to=%d, duration=%d)",
-h, calendar_date->h, 500);
}
}
return RET_OK;
}
7. 实现心率应用
(1)心率图主要通过自定义控件bar_graph实现,bar_graph通过data字段实现数据持久化,图表数据放在全局域中,每次加载窗口就动态载入,详见下图:
(2)心率检测窗口中的心跳动画用了image_animation控件,检测完成或者右滑返回心率窗口,效果详见下图,代码如下:
xml
<!--SmartWatch-Demo/res_800_480/assets/raw/ui/heart_rate_check.xml-->
<window anim_hint="slide_left(start_alpha=0, end_alpha=200)" style="heart_rate" full_screen="false"
x="c" y="m" w="390" h="390" theme="heart_rate_styles">
<gesture name="heart_rate_check_gesture" x="c" y="m" w="390" h="390" >
<label name="show" style="check_top" x="c" y="60" w="139" h="34" text="正在检测"/>
<image_animation x="c" y="m" w="238" h="238" image="heart_" start_index="0"
end_index="2" format="%s%05d" auto_play="true" interval="100" delay="0"/>
<label name="bottom" style="check_bottom" x="c" y="308" w="194" h="26" text="请保持手腕平衡"/>
</gesture>
</window>
8. 实现通知列表
通知列表主要通过list_view控件实现,居中效果则通过不可见的空项实现。效果详见下图,代码如下:
xml
<!--SmartWatch-Demo/res_800_480/assets/raw/ui/message.xml-->
<window anim_hint="slide_down" style="message" full_screen="false" x="c" y="m" w="390" h="390"
theme="message_styles">
<gesture x="0" y="0" h="100%" w="100%" name="message_window" >
<list_view x="c" y="-55" w="100%" h="120%" item_height="100">
<scroll_view name="view" x="c" y="m" w="100%" h="100%">
<list_item style="a" children_layout="default(rows=1,cols=1)" >
<view x="c" y="m" w="100%" h="95%" style="dragger" visible="false">
</view>
</list_item>
<list_item style="a" children_layout="default(rows=1,cols=1)">
<view x="c" y="m" w="100%" h="95%" style="dragger">
<column x="50" y="m" w="100%" h="100%">
<image x="0" y="m" w="88" h="88" image="app_cloudy_normal_88"/>
<row x="90" y="m" w="200" h="100%">
<digit_clock x="0" y="0" w="100%" h="50%" format="hh:mm" style="time"/>
<label x="0" y="50%" w="100%" h="50%" text="天气晴朗" style="text"/>
</row>
</column>
</gesture>
</window>
滑动删除操作则通过自定义动画实现,代码详见SmartWatch-Demo/src/slide_appliction/message.c这里就不列举了。
9. 实现应用菜单
应用菜单主要用了自定义控件slide_menu_v布局,与slide_menu效果基本一样,只不过滑动方向改为上下,效果详见下图,代码详见SmartWatch-Demo/src/custom_widgets/slide_menu_v.c这里就不列举了。
10. 实现快捷操作
快捷操作界面主要用了slide_view布局,效果详见下图,代码如下:
xml
<!--SmartWatch-Demo/res_800_480/assets/raw/ui/quick.xml-->
<window anim_hint="slide_up" full_screen="false" style="pay" x="c" y="m" w="390" h="390"
theme="quick_styles">
<gesture name="gesture" x="0" y="0" w="100%" h="100%">
<slide_view x="m" y="c" w="100%" h="100%">
<view x="c" y="m" w="100%" h="100%">
<label x="c" y="62" w="213" h="34" text="支付宝付款码" style="text"/>
<button x="c" y="125" w="306" h="156" name="alipay" style="alipay"/>
</view>
<view x="c" y="m" w="100%" h="100%">
<label x="c" y="62" w="70" h="34" text="请靠近读卡器" style="text" />
<button name="gongshang" style="gongshang" x="c" y="125" w="274" h="173" />
</view>
</slide_view>
<slide_indicator_arc x="0" y="b" w="100%" h="20" spacing="5"/>
</gesture>
</window>