把前 12 章拧成一根绳:规划、搭建、样式、交互、动画,一步不落
核心方法论:统揽全局,先整体后局部
「断案先看全局,画界面先画草图。控件怎么摆、样式什么调、事件往哪走、动画什么节奏——任何一个单点都容易,难的是把它们织成一张网。统揽全局的人,不是记住每个控件长什么样,而是心里先有一张完整的布局图,剩下的只是按图施工。」
做完整界面最容易踩的坑,是"想到哪写到哪"——先画个控件,再调位置,最后发现整体乱成一团。狄仁杰的方法是先看全案卷宗:动手前先把界面拆成五步。① 布局规划:页面骨架用什么容器、分成几块区域;② 控件搭建:每块区域放什么控件;③ 样式主题:统一配色、圆角、字体,让整页像一个整体;④ 事件交互:点击、拖动、输入如何响应,数据如何联动;⑤ 动画润色:过渡、往复动效、周期性刷新,让界面"活"起来。
这五步不是理论——LVGL 自带的 demos/ 就是现成的范本。demos/widgets 是一个三页签的"控制面板"(Profile 个人资料 / Analytics 数据仪表与图表 / Shop 购物),用 lv_tabview 组织,其中 Analytics 页恰好是"仪表 + 图表"组合,本章会反复参照它;demos/music 是手机风格的播放器;demos/benchmark 是性能压测。这些 demo 都由 lv_conf.h 里的 LV_USE_DEMO_* 宏开关启用(lv_conf_template.h 中默认 0),启用后 #include "demos/lv_demos.h",用 lv_demos_create(&argv[1], argc - 1) 按名字运行——比如 lv_demos widgets 跑控件演示、lv_demos benchmark 1 跑压测。
| 步骤 | 做什么 | 前章回顾 | 代表 API |
|---|---|---|---|
| 1 布局规划 | 定容器与分区(Flex/Grid/对齐) | 第 7 章 | lv_obj_set_flex_flow / lv_obj_set_grid_cell |
| 2 控件搭建 | 按分区放控件(scale/arc/chart/label) | 第 5 章 | lv_scale_create / lv_chart_create |
| 3 样式主题 | 统一配色圆角,给数据分区上色 | 第 6 章 | lv_style_* / lv_palette_main |
| 4 事件交互 | 点按/拖动/输入响应,数据联动 | 第 8、11 章 | lv_obj_add_event_cb / lv_*_bind_value |
| 5 动画润色 | 过渡、往复动效、周期刷新 | 第 9 章 | lv_anim_* / lv_timer_create |
启用 demo 的配置长这样(demos/README.md 的「Add the examples to your projects」一节原样给出了模板)。注意到 README 特别叮嘱:跑 widgets demo 时 LV_MEM_SIZE 至少要 38KB(推荐 48KB)——它在源码里甚至用 #error 硬性拦截过小配置。这类"内存预算"提示,就是第 14 章性能话题的预告。
/* lv_conf.h 里启用 widgets demo(demos/README.md 真实模板) */
#define LV_USE_DEMO_WIDGETS 1
#define LV_MEM_SIZE (38ul * 1024ul) /* 至少 38KB,推荐 48KB */
/* 应用入口:按名字创建 demo */
#include "demos/lv_demos.h"
if(!lv_demos_create(&argv[1], argc - 1)) {
lv_demos_show_help();
}
主循环则与普通 LVGL 程序毫无二致——demo 只是"往屏幕对象树上挂了一棵控件树",剩下的交给 lv_timer_handler():
/* demos/README.md:运行 demo 的主循环 */
while(1) {
uint32_t delay = lv_timer_handler();
if(delay < 1) delay = 1;
else if(delay == LV_NO_TIMER_READY) delay = LV_DEF_REFR_PERIOD;
usleep(delay * 1000);
}
动手前先在纸上画一张草图:屏幕分成哪几块、每块放什么、宽高大约多少。这张草图就是你的"全局图"。五步法不是固定顺序的流水线——样式和动画往往要回来反复调整——但先有规划再动手这个次序不能乱。
布局是界面骨架,LVGL 给出三种"摆法":绝对对齐(lv_obj_align 指哪打哪)、Flex(一维流式,子对象沿主轴自动排布)、Grid(二维表格,把对象钉进行列网格)。真实 demo 里布局密度最高的当属 Analytics 页:页根 lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_ROW) 把整页切成横向一列的卡片,卡片内 lv_obj_set_flex_grow(cont, 1) 等宽均分,再用 lv_obj_set_flex_in_new_track 控制换行。小屏幕上装不下就换行,大屏幕上并排展示——一个 Flex 属性切换,响应式布局就到手了。
一个"小而全"的 Flex 用法藏在 examples/widgets/scale/lv_example_scale_heart_rate.c:仪表盘中心的圆形容器里开一个 flex 列,lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER) 让"数值 + 单位"两行文字水平、垂直都居中。布局规划的本质就是"想清楚谁在谁的肚子里、怎么排",想清楚了再写代码,比边写边挪坐标高效一个量级。
/* heart_rate 示例:中心圆形容器里开一个 flex 列,文字两行居中 */
lv_obj_t * circle = lv_obj_create(lv_screen_active());
lv_obj_set_size(circle, 130, 130);
lv_obj_center(circle);
lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0);
lv_obj_t * cont = lv_obj_create(circle);
lv_obj_set_layout(cont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t * value_label = lv_label_create(cont);
lv_label_set_text_fmt(value_label, "%d", 145);
lv_obj_t * unit_label = lv_label_create(cont);
lv_label_set_text(unit_label, "bpm");
当布局更"表格化"时用 Grid。Analytics 页的图表卡内部就是这样:先 lv_obj_set_grid_dsc_array(parent, col_dsc, row_dsc) 声明列、行描述(LV_GRID_CONTENT 表示"按内容自适应",LV_GRID_FR(1) 表示"把剩余空间按比例分"),再用 lv_obj_set_grid_cell 把标题、垂直刻度、图表分别钉进对应格子。
/* analytics 示例:grid 把"标签行 + 主体 + 刻度"钉进网格 */
static const int32_t col_dsc[] = {40, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static const int32_t row_dsc[] = {LV_GRID_CONTENT, LV_GRID_FR(1), 40, LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(parent, col_dsc, row_dsc);
lv_obj_t * label = lv_label_create(parent);
lv_obj_set_grid_cell(label, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_STRETCH, 0, 1);
lv_obj_t * scale_ver = lv_scale_create(parent);
lv_scale_set_mode(scale_ver, LV_SCALE_MODE_VERTICAL_LEFT);
lv_obj_set_grid_cell(scale_ver, LV_GRID_ALIGN_END, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
lv_scale_set_total_tick_count(scale_ver, 11);
lv_scale_set_major_tick_every(scale_ver, 2);
lv_scale_set_range(scale_ver, 0, 100);
本章的目标界面定为"仪表盘"。先纠正一个容易踩的坑:LVGL 9 里没有独立的 meter 控件(老版本叫 meter 的仪表,在 9.x 已被 scale 取代)。仪表盘的标准做法是 lv_scale(刻度 + 量程)+ 指针 + 中心文字 三件套:lv_scale_create() 提供环形模式 LV_SCALE_MODE_ROUND_INNER(圆环、刻度朝内)。真实示例 lv_example_scale_needles.c 用不到二十行就搭出一个带指针的圆表:范围 10~40、共 31 个刻度、主刻度每 5 格一个、扫角 270° 并旋转到 135°(让缺口朝下,表盘"开口")。
/* needles 示例:一个 150×150 的环形表盘骨架 */
lv_obj_t * scale = lv_scale_create(lv_screen_active());
lv_obj_set_size(scale, 150, 150);
lv_scale_set_mode(scale, LV_SCALE_MODE_ROUND_INNER);
lv_scale_set_range(scale, 10, 40);
lv_scale_set_total_tick_count(scale, 31);
lv_scale_set_major_tick_every(scale, 5);
lv_scale_set_angle_range(scale, 270);
lv_scale_set_rotation(scale, 135);
lv_obj_set_style_length(scale, 5, LV_PART_ITEMS);
lv_obj_set_style_length(scale, 10, LV_PART_INDICATOR);
指针有两种挂法:直线指针用 lv_line_create 挂进 scale,再调用 lv_scale_set_line_needle_value(scale, needle, needle_length, value) 让库按数值把指针转到对应角度;图片指针用 lv_image_create 挂一张指针图,用 lv_scale_set_image_needle_value 驱动,配合 lv_image_set_pivot 设定绕哪一点转(needles 示例里指针图片的旋转轴心是 (3,4))。表盘中心再放一个圆形覆盖层容纳数值与单位文字,就拼出了 heart_rate 示例的核心结构。
/* needles 示例:直线指针 + 动画驱动 */
lv_obj_t * needle_line = lv_line_create(scale);
lv_obj_set_style_line_width(needle_line, 6, LV_PART_MAIN);
lv_obj_set_style_line_rounded(needle_line, true, LV_PART_MAIN);
static void set_needle_value(void * var, int32_t v)
{
lv_scale_set_line_needle_value(scale, needle_line, 60, v);
}
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, scale);
lv_anim_set_exec_cb(&a, set_needle_value);
lv_anim_set_values(&a, 10, 40);
lv_anim_set_duration(&a, 1000);
lv_anim_set_reverse_duration(&a, 1000);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
lv_anim_start(&a);
把"刻度"和"指针"分开想,是理解 scale 的关键:lv_scale_* 管刻度盘本身(量程、刻度数、扫角),指针是另一个对象(line 或 image),只是"借" scale 的坐标算角度。这样你就能自由组合——同一个刻度盘,换直线指针还是图片指针,只是挂法不同。
仪表盘光有刻度盘还"冷冰冰",样式系统的价值在于把数据讲出颜色来。heart_rate 示例把仪表按心率分区染色:lv_scale_add_section 加一个分区,lv_scale_set_section_range 定它的数值区间,再给每个分区配三套样式——lv_scale_set_section_style_main 管主弧、lv_scale_set_section_style_indicator 管指示刻度、lv_scale_set_section_style_items 管普通刻度。五个区间分别用灰/蓝/绿/橙/红,恰好对应心率五档:低于 117 是灰色区,一路到 176 以上是红色区。数据一进区间,颜色自己就说话。
/* heart_rate 示例:给分区上色的三套样式(节选) */
static void init_section_styles(lv_style_t * items, lv_style_t * indicator,
lv_style_t * main, lv_color_t color)
{
lv_style_init(items);
lv_style_set_line_color(items, color);
lv_style_set_line_width(items, 0);
lv_style_init(indicator);
lv_style_set_line_color(indicator, color);
lv_style_set_line_width(indicator, 0);
lv_style_init(main);
lv_style_set_arc_color(main, color);
lv_style_set_arc_width(main, 20);
}
lv_scale_section_t * sec = lv_scale_add_section(scale);
lv_scale_set_section_range(scale, sec, 98, 117); /* 第一档区间 */
lv_scale_set_section_style_items(scale, sec, &items);
lv_scale_set_section_style_indicator(scale, sec, &indicator);
lv_scale_set_section_style_main(scale, sec, &main);
除了刻度盘,数据表达还能上图表。Analytics 页的 "Unique visitors" 折线图与 "Monthly revenue" 柱状图就是模板:lv_chart_add_series 往图里加一条序列(可多条,各配一条 Y 轴),lv_chart_set_type(chart, LV_CHART_TYPE_BAR) 切到柱状(默认折线),lv_chart_set_next_value 逐个喂数据点;坐标轴刻度则复用 scale——垂直方向 LV_SCALE_MODE_VERTICAL_LEFT、水平方向 LV_SCALE_MODE_HORIZONTAL_BOTTOM,配上文字源就是横轴标签。配色上有两件顺手工具:lv_palette_main(LV_PALETTE_RED) 取色板主色,lv_theme_get_color_primary(obj) 取当前主题主色——整页配色跟着主题走,不会跑偏。
/* analytics 示例:一条柱状序列 + 12 个随机数据点 */
lv_obj_t * chart = lv_chart_create(parent);
lv_chart_set_div_line_count(chart, 0, 12);
lv_chart_set_point_count(chart, 12);
lv_chart_series_t * ser = lv_chart_add_series(chart,
lv_theme_get_color_primary(chart),
LV_CHART_AXIS_PRIMARY_Y);
lv_chart_set_type(chart, LV_CHART_TYPE_BAR); /* 或保持默认折线 */
uint32_t i;
for(i = 0; i < 12; i++) {
lv_chart_set_next_value(chart, ser, lv_rand(10, 80));
}
交互有两种"流派"。一是数据驱动:让控件直接订阅 subject,几乎不写回调。官方示例 examples/widgets/arc/lv_example_arc_bind_value.c 把滑块和弧表盘绑到同一个 subject_value——lv_slider_bind_value(slider, &subject) 让它当编辑入口,lv_arc_bind_value(arc, &subject) 让它当只读展示(lv_obj_set_clickable(arc, false) 关掉点击),再 lv_label_bind_text(label, &subject, "%d %%") 显示数值。拖一下滑块,表盘与文字一起动——第 11 章讲的"数据是唯一事实来源"在这里直接落地。
/* arc_bind_value 示例:一个 subject,三个控件同时订阅 */
static lv_subject_t subject_value;
lv_subject_init_int(&subject_value, 50);
lv_subject_set_min_value_int(&subject_value, 0);
lv_subject_set_max_value_int(&subject_value, 100);
lv_obj_t * arc = lv_arc_create(screen);
lv_obj_set_size(arc, 160, 160);
lv_arc_bind_value(arc, &subject_value); /* 表盘:只读展示 */
lv_obj_set_clickable(arc, false);
lv_obj_t * label = lv_label_create(arc);
lv_label_bind_text(label, &subject_value, "%d %%");
lv_obj_t * slider = lv_slider_create(screen);
lv_slider_bind_value(slider, &subject_value); /* 滑块:编辑入口 */
二是手动事件 + 周期刷新。需要自己逻辑时用 lv_obj_add_event_cb(obj, cb, LV_EVENT_*, user_data);需要"心跳"式的周期更新时用 lv_timer_create(cb, period_ms, user_data)。heart_rate 的 80ms 计时器就是教科书:数值先走一步(撞到上下界就反向),再同时更新指针角度和文字颜色。动效则由 lv_anim_t 编排——needles 示例里 lv_anim_set_values(&a, 10, 40) + lv_anim_set_exec_cb + lv_anim_set_duration(1000) + lv_anim_set_reverse_duration(1000) + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE),指针就在 10~40 之间无限往复。别忘了生命周期:对象销毁时 LV_EVENT_DELETE 里要 lv_timer_delete、lv_style_reset 把资源还回去(Analytics 的 delete_timer_event_cb 正是这么清理计时器的)。
/* heart_rate 示例:周期计时器驱动指针与文字(节选) */
static int32_t hr = 98;
static int8_t step = 1;
static void hr_timer_cb(lv_timer_t * t)
{
hr += step;
if(hr >= 195) { hr = 195; step = -1; }
else if(hr <= 98) { hr = 98; step = 1; }
lv_scale_set_line_needle_value(scale, needle_line, 180, hr);
lv_label_set_text_fmt(value_label, "%d", hr);
}
lv_timer_create(hr_timer_cb, 80, NULL);
/* 对象销毁时清理计时器(analytics 的 delete_timer_event_cb 思路) */
static void on_delete_cb(lv_event_t * e)
{
if(lv_event_get_code(e) == LV_EVENT_DELETE) {
lv_timer_t * t = lv_event_get_user_data(e);
if(t) lv_timer_delete(t);
}
}
动画与滑块不要各写各的更新逻辑,否则它们会互相打架——指针刚被动画拨到 40,滑块又把 subject 写成 20,界面就抽搐。正确的姿势是让两者写同一个 subject:动画回调只 lv_subject_set_int,observer 或绑定统一负责刷新指针与文字。一个数据源、多处读取,这是第 11 章那套思想的实战版。
现在把五步合拢,写一个完整可读的程序:一个"音量仪表盘"。整体结构:顶部一个标题;中间一个环形 scale(量程 0~100、270° 扫角、直线指针),指针由动画驱动;动画回调只改 volume_subject,数值文字通过 observer 跟随;底部一个滑块,拖动时同样写 volume_subject——于是动画、滑块、指针、文字四者共享同一个数据源,互相不会打架。全部 API 都是前面各章出现过的。
/* 迷你仪表盘:lv_scale 圆表 + 指针动画 + 滑块绑定 + 数据驱动刷新 */
#include "lvgl/lvgl.h"
static lv_subject_t volume_subject; /* 唯一的"事实来源" */
static lv_obj_t * scale;
static lv_obj_t * needle_line;
static lv_obj_t * value_label;
/* 数据变化 → 统一刷新指针与文字 */
static void volume_observer_cb(lv_observer_t * observer, lv_subject_t * subject)
{
LV_UNUSED(observer);
int32_t v = lv_subject_get_int(subject);
lv_scale_set_line_needle_value(scale, needle_line, 40, v);
lv_label_set_text_fmt(value_label, "%d %%", v);
}
/* 动画回调:只改 subject,不碰任何控件 */
static void needle_anim_cb(void * var, int32_t v)
{
LV_UNUSED(var);
lv_subject_set_int(&volume_subject, v);
}
void demo_volume_gauge_create(lv_obj_t * parent)
{
lv_subject_init_int(&volume_subject, 0);
lv_subject_set_min_value_int(&volume_subject, 0);
lv_subject_set_max_value_int(&volume_subject, 100);
/* ① 布局:整页 flex 列,垂直水平居中 */
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_flex_main_place(parent, LV_FLEX_ALIGN_CENTER, 0);
lv_obj_set_style_flex_cross_place(parent, LV_FLEX_ALIGN_CENTER, 0);
lv_obj_set_style_pad_row(parent, 12, 0);
/* ② 控件:标题 + 表盘 + 数值 + 滑块 */
lv_obj_t * title = lv_label_create(parent);
lv_label_set_text(title, "Volume");
scale = lv_scale_create(parent);
lv_obj_set_size(scale, 200, 200);
lv_scale_set_mode(scale, LV_SCALE_MODE_ROUND_INNER);
lv_scale_set_range(scale, 0, 100);
lv_scale_set_total_tick_count(scale, 21);
lv_scale_set_major_tick_every(scale, 5);
lv_scale_set_angle_range(scale, 270);
lv_scale_set_rotation(scale, 135);
lv_obj_set_style_length(scale, 6, LV_PART_ITEMS);
lv_obj_set_style_length(scale, 10, LV_PART_INDICATOR);
needle_line = lv_line_create(scale);
lv_obj_set_style_line_width(needle_line, 6, LV_PART_MAIN);
lv_obj_set_style_line_rounded(needle_line, true, LV_PART_MAIN);
value_label = lv_label_create(parent);
lv_label_set_text(value_label, "0 %%");
lv_obj_t * slider = lv_slider_create(parent);
lv_obj_set_width(slider, 220);
lv_slider_bind_value(slider, &volume_subject);
/* ③ 样式:交给默认主题;④ 交互:订阅 subject */
lv_subject_add_observer(&volume_subject, volume_observer_cb, NULL);
/* ⑤ 动画:让指针在 0~100 之间往返,演示动效 */
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, NULL);
lv_anim_set_exec_cb(&a, needle_anim_cb);
lv_anim_set_values(&a, 0, 100);
lv_anim_set_duration(&a, 2000);
lv_anim_set_reverse_duration(&a, 2000);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
lv_anim_start(&a);
}
这段代码怎么跑起来?把它放进第 2 章搭好的模拟器工程里,编译时加入 lvgl-master 与上面这个 .c 文件;在 lv_init()、创建好显示设备后,调用 demo_volume_gauge_create(lv_screen_active()),再进入 lv_timer_handler() 主循环。你会看到:指针 0→100→0 往复旋转,数值文字同步跳动;拖动滑块,指针和文字立刻跟上。五步法里的每一步都落地了——这就是一个完整的界面。
/* 入口:第 2、4 章的初始化就绪之后,挂上仪表盘并进入主循环 */
lv_init();
/* ... 创建显示设备与输入设备(display / indev,见第 4 章) ... */
demo_volume_gauge_create(lv_screen_active());
while(1) {
uint32_t delay = lv_timer_handler();
if(delay < 1) delay = 1;
/* 交给宿主:sleep 对应毫秒,再继续下一帧 */
}
回头看这一个 demo_volume_gauge_create,它几乎没有一行"新知识":Flex 布局(13.2)、scale 表盘与指针(13.3)、分区与主题色(13.4)、subject 绑定与动画计时器(13.5),全是前面章节 API 的重新组合。这正是一个成熟 UI 工程师的状态——不是会更多 API,而是把已知的 API 组织得更像一个整体。第 14 章起,我们会进一步关心它跑得快不快、吃多少内存。
给自己定个"复盘清单":界面做完了,回头逐条核对五步法——布局有没有冗余容器?样式是不是散的(应该能收敛成几条 lv_style_t)?事件有没有各写各的(应该汇聚到数据源)?动画是不是只为炫而存在(好动画服务于信息)?每对一个,你就离"统揽全局"更近一步。
阅读 demos/widgets/ 的源码与 demos/README.md,回答三个问题:(1) widgets demo 用 lv_tabview 建了哪三个页签?(2) 要在 lv_conf.h 里启用它需要把哪个宏改成 1、对 LV_MEM_SIZE 有什么硬性要求?(3) lv_demos_create(&argv[1], argc - 1) 里的参数是什么语义?
看 lv_demo_widgets.c 的 lv_demo_widgets_with_args:三个 lv_tabview_add_tab;README 里提到 38KB 与 48KB;argv 是命令行参数数组,demo 名从 argv[1] 开始。
(1) 三个页签:Profile(个人资料)、Analytics(数据仪表与图表)、Shop(购物),由 lv_tabview_create + 三次 lv_tabview_add_tab 建立,页签标题分别是 "Profile"、"Analytics"、"Shop"。(2) 把 LV_USE_DEMO_WIDGETS 改成 1;LV_MEM_SIZE 至少 38KB(推荐 48KB),源码里配置过小时有 #error 拦截。(3) 语义是"命令行参数 + 数量":lv_demos_create 按 info[0] 的名字(如 widgets、benchmark)在注册表里查入口函数并调用;不传名字则跑第一个 demo。这实现了"一个可执行文件、按参数切换 demo"。
参照 examples/widgets/scale/lv_example_scale_needles.c,把它改造成两圈不同量程的表盘:左表 0~50,右表 0~100(其余刻度、扫角、指针可照搬)。说明要改哪些参数,以及为什么"改量程"通常要同时改 lv_scale_set_range 与刻度相关参数。
量程由 lv_scale_set_range 决定;刻度密度由 lv_scale_set_total_tick_count(总刻度数)与 lv_scale_set_major_tick_every(每几个算一个主刻度)决定。动画的 lv_anim_set_values 也要同步。
对左表把 lv_scale_set_range(scale, 0, 50)、lv_anim_set_values(&a, 0, 50);右表 lv_scale_set_range(scale, 0, 100)、lv_anim_set_values(&a, 0, 100);总刻度数可保持 31,主刻度每 5 个。原因:range 定的是"数值端点",刻度怎么摆由"总刻度数"和"主刻度间隔"共同决定——只改 range 不改刻度数,数值区间变了但刻度密度不变,看起来刻度会变得过密或过疏。指针动画的取值范围也必须落在新的 range 内,否则指针会越界。顺带可以试试把其中一圈的直线指针换成图片指针(LV_IMAGE_DECLARE(img_hand) + lv_scale_set_image_needle_value)。
把 lv_example_scale_heart_rate.c 的五个心率分区改成三档(例如 98~140 / 140~170 / 170~195),自选三组颜色。作答时说明:section 的 main / indicator / items 三套样式分别作用于刻度盘的哪个部分?
对照 13.4 的 init_section_styles:main 用 lv_style_set_arc_color/width,indicator 与 items 用 lv_style_set_line_color/width。主弧、主刻度、普通刻度,三对一。
改法:把 add_section(scale, ...) 的 5 次调用收成 3 次,区间改为 98~140、140~170、170~195,每档配一种 lv_palette_main 颜色即可(grey/blue/red 之类)。三套样式的分工:main 管主弧(环形底,即表盘外圈那条圆弧,lv_style_set_arc_width/arc_color);indicator 管指示刻度(每个主刻度那条短线,lv_style_set_line_color/width);items 管普通刻度(次刻度短线)。所以"分区上色"实际是给同一区间的主弧与两组刻度同时换色,让该区间从外圈到刻度都亮起同一颜色。
用五步法从零规划并(可选)实现一个"环境监测面板":三块圆表(温度 / 湿度 / 气压)+ 一张 12 点历史折线图 + 一个超限滑块(超过阈值时把面板标题染成警示色)。先写出规划清单:布局怎么分区、各用什么容器与布局;每块放什么控件;样式主题怎么定;交互与数据源如何设计(是否用 subject);动画/刷新放哪里。参考 demos/widgets/lv_demo_widgets_analytics.c 的结构。
布局可以"一行 Flex:三块圆表卡片 + 一张图表卡",复用 analytics 的 grid 卡片写法;数据建议统一放到几个 lv_subject_t,超限判断用 observer 或 lv_obj_bind_state_if_gt;刷新用 lv_timer_create 周期喂数据。
一份可参考的规划:① 布局——页根 LV_FLEX_FLOW_ROW 分两列:左列三张卡片(温度/湿度/气压,各含一个 LV_SCALE_MODE_ROUND_INNER 圆表 + 数值 label),右列一张图表卡(grid 里放 lv_chart + 两个 scale 做坐标轴,参照 create_chart_with_scales);小屏改用 flex 换行或 lv_tabview。② 控件——每块圆表用 13.3 的骨架,图表用 13.4 的 lv_chart_add_series(折线,12 个点)。③ 样式——定义 3 条 lv_style_t 统一卡片底色/圆角,温度表按 13.4 分区间上色,超限时用 LV_STATE_USER_1 + 一条警示样式。④ 交互——三个传感器值各建一个 lv_subject_t;超限判断用 observer 回调(参照本章 volume_observer_cb)把标题染警示色;滑块 lv_slider_bind_value 当阈值输入。⑤ 动画——lv_timer_create(cb, 500, NULL) 周期更新三块表与图表(lv_chart_set_next_value),指针用 lv_scale_set_line_needle_value 写回。逐条对照五步法自查,就是一次完整的"统揽全局"演练。