# 9. 经典案例

本章导读:

使用AWTK可以高效开发出漂亮的GUI应用。

# 9.1 简介

本章以实际的应用场景为案例,来阐述使用AWTK做项目的开发过程及其代码实现过程,帮助开发者积累开发应用的实战经验。经典案例如下:

  • 洁净新风系统
  • 炫酷图表
  • 音乐播放器
  • 智能手表

以上案例可以安装 AWStudio (opens new window) 后下载查看。

# 9.2 洁净新风系统

# 9.2.1 功能详解

洁净新风系统实现的功能点主要有下面几点:

  • 点击"开关"按钮,启动/停止PM2.5、二氧化碳浓度、送风室内外温度、排风室内外温度等模拟读数,以及模拟报警
  • 点击"自动"按钮,启动/停止背景(淡入淡出)切换
  • 点击"定时"按钮,弹出定时设置对话框,模拟开/关定时功能(即时钟图标的显示/隐藏)
  • 点击"记录"按钮,弹出记录列表(模拟数据)页面
  • 点击"设置"按钮,弹出设置页面,设置控制、报警参数
  • 点击三角形按钮,可以加/减频率、温度、湿度
  • 实时更新系统时间

# 9.2.2 应用实现

本案例使用目录结构、编译和运行步骤与第2章介绍的hello world应用类似,这里就不重复介绍了。

本案例使用XML设计界面,业务逻辑使用C语言实现。主界面下方的五个按钮,通过注册点击事件进入相应的界面。主界面右边的按钮,通过注册点击事件实现参数的递增或递减。主界面中间部分通过创建image_animation和opacity动画实现炫酷的效果。运行的效果详见下图:

图9.1 洁净新风系统
图9.1 洁净新风系统

本案例使用到的代码均可以在CleanAir-Demo目录找到。

1. 如何打开窗口

(1)在AWTK中,可以使用一个xml文件来描述一个窗口的界面结构。比如描述记录列表窗口的record.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()即可打开该窗口,代码如下:

/* src/window_record.c */
widget_t* win = window_open("record");

其中,"record"为xml的文件名,运行效果如下图所示:

图9.2 记录界面
图9.2 记录界面

(3)如果需要手动关闭窗口使用window_close()即可,代码如下:

/* src/window_record.c */
window_close(win);

2. 如何打开对话框

(1)比如创建一个定时按钮窗口,代码如下:

<!-- 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()即可打开该窗口,代码如下:

/* src/window_timing.c*/
widget_t* win = window_open("timing");

其中,"timing"为xml的文件名,运行效果如下图所示:

图9.3 定时界面
图9.3 定时界面

(3)如果需要手动关闭窗口使用dialog_quit()即可,代码如下:

/* src/window_timing.c */
dialog_quit(dialog, RET_OK);

3. 如何查找控件

在main.xml文件中设置控件的名字为" timing_status",然后C代码中调用widget_lookup()查找控件,代码如下:

<!-- src/assets/raw/ui/main.xml -->
<image name="timing_status" image="clock" draw_type="icon"/>
/* src/window_main.c */
widget_t* timing = widget_lookup(win, "timing_status", TRUE);

4. 如何响应界面事件

比如在主界面上设置开关按钮的点击事件,代码如下:

<!-- assets/default/raw/ui/main.xml -->
<button name="switch" x="0" w="147" h="100%" style="switch_btn"/>	
/* src/window_main.c */
widget_t* win = widget_get_window(widget);
widget_on(widget, EVT_CLICK, on_switch, win);

5. 如何设置控件是否可见

AWTK中可通过控件的visible属性控制该控件是否可见。比如控制定时图标是否可见,代码如下:

/* 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. 如何设置窗口顶部容器

比如在主界面上显示"洁净新风系统",代码如下:

<!-- 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,代码如下:

<!-- assets/default/raw/ui/main.xml -->
<label name="PM2_5" x="80" w="108" h="100%" text="18" style="reading_pm_co"/>
 /* 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),代码如下:

<!-- 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,代码如下:

<!-- assets/default/raw/ui/main.xml -->
<rich_text x="188" w="-188" h="100%" text="
  <font color=&quota;white&quota; align_v=&quota;top&quota; size=&quota;18&quota;>μg/m</font>
  <font color=&quota;white&quota; align_v=&quota;top&quota; size=&quota;10&quota;>3</font>" />

10. 如何使用布局

比如在主界面上以一行两列的方式显示时钟(clock.png)和报警图标(bell.png),代码如下:

 <!-- 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)旋转,代码如下:

/* 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),代码如下:

/* 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图片动画,代码如下:

<!-- 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"/>
/* src/window_main.c */
image_animation_play(fan_1);

12. 如何定时刷新界面数据

比如实时更新主界面右上角的系统时间,代码如下:

/* src/window_main.c */
timer_add(on_systime_update, win, 1000);

13. 如何使用时分秒控件

比如点击主界面中的"定时"按钮弹出"定时设置"界面显示时分秒控件,代码如下:

<!-- 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. 如何显示列表

比如点击主界面中的"记录"按钮弹出的列表视图,代码如下:

<!-- 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)比如点击主界面中的"设置"按钮弹出分页视图,代码如下:

<!-- 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) 运行效果如下图所示:

图9.4 设置页面
图9.4 设置页面

16. 如何设置控件样式

比如设置主界面中的"开关"按钮的样式,代码如下:

<!-- assets/default/raw/ui/main.xml -->
<button name="switch" x="0" w="147" h="100%" style="switch_btn"/>
<!-- 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>
/* 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文件中配置要互译的文本,代码如下:

<!-- 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设置要翻译的文本,代码如下:

<!-- 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函数实现互译,代码如下:

/* 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,代码如下:

<!-- 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。

图9.5 炫酷图表
图9.5 炫酷图表

本案例使用到的代码均可以在Chart-Demo目录找到。

1. 实现主界面逻辑

主界面实现的功能点如下:

  • 中英文环境切换
  • 界面切换,通过注册点击事件,然后进入相应的界面

下面,介绍如何实现这些功能点。

(1) 中英文环境切换

本案例实现中英文环境切换与洁净新风系统的类似,这里是通过改变样式的方式实现图片切换(比较复杂,代码如下),而洁净新风系统是通过在样式文件default.xml中给图片加个"$locale$"后缀完成(比较简单,推荐使用这种方式)。由于在上面章节已经介绍过了,这里不重复了。

/* 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"点击事件,进入仪表盘、饼图、曲线图和柱状图界面,代码如下:

/* 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控件实现仪表盘的绘制,代码如下:

<!-- 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开始动画,实现启动仪表盘,代码如下:

/* 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停止动画,实现停止仪表盘,代码如下:

/* 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;
}

运行效果如下图所示:

图9.6 仪表盘
图9.6 仪表盘

3. 实现饼图逻辑

饼图界面实现的功能点如下:

  • 绘制饼图
  • 生成新的饼图/环形图
  • 饼图和环形图之间的切换

下面,介绍如何实现这些功能点。

(1)绘制饼图

饼图是一个自定义控件,通过重新定义控件的虚函数表(主要是create、on_paint_self、on_destroy、set_prop、get_prop等函数),并使用widget_factory_register注册到AWTK中,即可在xml中直接使用,代码如下:

/* 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) 饼图/环形图的值动画(界面右边的第一个按钮)

生成新的饼图/环形图,实现步骤如下:

首先,通过创建值动画将原来的饼图/环形图还原到原点,代码如下:

/* 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;
}

然后,通过定时器判断上面的动画是否已经全部还原到原点,如果是重置饼图/环形图的值数据,代码如下:

/* 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设置是否是环形图,代码如下:

/* 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;
}

运行效果如下图所示:

图9.7 饼图
图9.7 饼图

4. 实现曲线图逻辑

曲线图界面实现的功能点如下:

  • 绘制曲线图
  • 随机生成新的曲线图
  • 设置是否显示曲线
  • 设置是否显示闭合区域
  • 设置是否显示圆点
  • 设置是否显示平滑曲线

下面,介绍如何实现这些功能点。

(1)绘制曲线图

曲线图同样是一个自定义控件,该控件中X轴、Y轴、曲线等是一个个独立的元素,各自完成自身的绘制(比如曲线line_series仅负责将数据点按指定波形显示),但布局受父控件chart_view控制。代码如下:

/* 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函数设置给控件,代码如下:

/* 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);
  }
  ...
}
/* 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属性是否显示曲线,代码如下:

/* 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属性是否显示闭合区域,代码如下:

/* 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属性是否显示圆点,代码如下:

/* 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属性是否显示平滑曲线,代码如下:

/* 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");
}

运行效果如下图所示:

图9.8 曲线图
图9.8 曲线图

5. 实现柱状图逻辑

柱状图界面实现的功能点如下:

  • 绘制柱状图
  • 随机生成新的柱状图

下面,介绍如何实现这些功能点。

(1)绘制柱状图

柱状图与上面曲线图原理类似,不同的是数据显示改为使用bar_series,bar_series负责将数据点显示为一组组的柱条。代码如下:

/* 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) 随机生成新的柱状图(界面右边的第一个按钮)

生成新的柱状图的逻辑与曲线图页面的差不多,这里就不累赘了,代码如下:

/* 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.9 柱状图
图9.9 柱状图

# 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显示进度,右侧两个按钮分别实现切换播放模式以及打开歌曲列表,功能主要通过注册点击事件实现。在主界面的右上方为三个按钮,从上到下第一个按钮实现黑胶唱片场景动画的开启或者关闭,其余两个则按钮通过注册点击事件进入相应的界面。运行的效果详见下图。

图9.10 音乐播放器
图9.10 音乐播放器

本案例使用的代码均可以在MusicPlay-Demo目录找到。

1. 实现黑胶唱片动画效果

(1)先创建两个image控件和一个gauge_pointer控件来分别实现唱片、歌曲封面和唱针的绘制。代码如下:

<!-- 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创建控件动画来实现,代码如下:

/* 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的子控件。代码如下:

<!-- 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>
/* 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来切换上一个或者下一个子控件,代码如下:

/* 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)来选择对应的歌词进行解析,最后将歌曲封面、歌词和歌曲时间信息同步到界面上,代码如下:

/* 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控件实现播放列表的绘制,代码如下:

<!-- 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切换所选择的歌曲进行播放,代码如下:

/* 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;
}
/* 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;
}

运行效果如下图所示:

图9.11 播放列表
图9.11 播放列表

5. 实现均衡器逻辑

(1)绘制滑动条和曲线图

通过slider控件实现滑动条的绘制,曲线图是一个自定义控件,布局受到char_view影响,代码如下:

<!-- 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数值和波形图的绘制,代码如下:

/* 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;
}

运行的效果如下图所示:

图9.12 均衡器
图9.12 均衡器
  1. 实现旋钮音频参数调节

(1)绘制表盘指针和进度圆环控件

创建gauge_pointer和progress_circle控件实现旋钮和旋钮刻度线的绘制,代码如下:

<!-- 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,代码如下:

/* 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.13 高级设置
图9.13 高级设置

# 9.5 智能手表

# 9.5.1 功能详解

智能手表实现的功能点主要有下面几点:

  • 表盘:右滑切换表盘、长按预览及选择表盘、下滑打开消息列表、上滑打开快捷操作界面、左滑打开应用列表
  • 日历应用:右滑返回、上滑切换到下个月、下滑切换到上个月、天气应用
  • 音乐应用:右滑返回、播放与暂停、播放模式、切歌、改变音量
  • 健康应用:右滑返回、设定目标
  • 心率检测:右滑返回、检测心率

# 9.5.2 应用实现

本应用包括四个表盘:数字和模拟表盘各有两个。每个表盘都具有不同功能,具体如下:

  • 模拟表盘一:具有动态显示电量、查看天气及记录和动态显示健康数据的功能,详见下图:
图9.14 模拟表盘一
图9.14 模拟表盘一
  • 数字表盘一:具有显示时间、动态更新刻度(刻度值表示秒)的功能,详见下图:
图9.15 数字表盘一
图9.15 数字表盘一
  • 数字表盘二:具有显示天气、日历和健康数据、播放音乐以及动态显示心率图的功能,详见下图:
图9.16 数字表盘二
图9.16 数字表盘二
  • 模拟表盘二:具有显示天气和日历以及播放音乐的功能,其中音乐圆环进度与音乐应用同步,下面几个消息提示图标也是动态变化,详见下图:
图9.17 模拟表盘二
图9.17 模拟表盘二

1. 实现手势识别

通过gesture手势控件来实现的,通过比对手势按下与松开时的坐标,从而判断其方向及距离,代码如下:

/* 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控件布局,表盘长按预览的效果详见下图,代码如下:

图9.18 表盘预览及选择
图9.18 表盘预览及选择
<!--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. 实现音乐应用

切歌动画主要通过两个动画实现,先把当前歌曲切走,再把新歌曲切回来,详见下图:

图9.19 音乐应用界面
图9.19 音乐应用界面

切歌实现的步骤如下:

步骤一:首先记住音乐的播放状态,切歌过程中是不播放的,通过动画把歌名标签移动到视野外,代码如下:

/* 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;
}

步骤二:然后再用动画将已经更换歌名的标签从视野外移动到中间,代码如下:

/* 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)显示天气的效果详见下图

图9.20 天气应用界面
图9.20 天气应用界面

(2)小时天气界面主要用到了slide_view布局,其中小时天气用了hour_weather自定义控件,其中点击中间环会更新温度值,效果详见下图,代码如下:

图9.21 小时天气界面
图9.21 小时天气界面
/* 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控件,详见下图:

图9.22 主界面
图9.22 主界面

(2)活动界面使用了自定义的health_circle圆环控件(基于progress_circle改造),加了拖动可以动态改变数值的功能,详见下图:

图9.23 活动界面
图9.23 活动界面

6. 实现日历应用

日历应用布局主要通过view控件实现,详见下图:

图9.24 日历应用界面
图9.24 日历应用界面

其中,切换月份通过自定义动画实现,步骤如下:

步骤一:先通过动画把当前月份界面移走,代码如下:

/* 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;
}

步骤二:切走当前月份后再切换到下月界面,更换完成也是通过动画载入下月界面,代码如下:

/* 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字段实现数据持久化,图表数据放在全局域中,每次加载窗口就动态载入,详见下图:

图9.25 心率图
图9.25 心率图

(2)心率检测窗口中的心跳动画用了image_animation控件,检测完成或者右滑返回心率窗口,效果详见下图,代码如下:

图9.26 心率检测界面
图9.26 心率检测界面
<!--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控件实现,居中效果则通过不可见的空项实现。效果详见下图,代码如下:

图9.27 通知消息界面
图9.27 通知消息界面
<!--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这里就不列举了。

图9.28 应用选择界面
图9.28 应用选择界面

10. 实现快捷操作

快捷操作界面主要用了slide_view布局,效果详见下图,代码如下:

图9.29 快捷操作界面
图9.29 快捷操作界面
<!--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>