4. LVGL 样式系统:让控件真正“活”起来

样式系统,说白了就是 LVGL 控件的“皮肤”和“表情”。

我刚开始接触 LVGL 时,以为样式就是改改颜色、调调大小。后来才发现,样式系统是整个框架里最灵活、也最容易踩坑的部分。你想想看,一个按钮在按下时要有反馈,在禁用时要变灰,这些状态切换全靠样式来驱动。

4.1 样式的概念与作用

在 LVGL 里,样式(lv_style_t)是一组属性的集合。它不绑定具体控件,而是像贴纸一样,可以贴到任意控件上。

核心思想:样式 = 属性集合 + 状态条件。同一个控件在不同状态下,可以展示完全不同的外观。

我习惯把样式理解成“CSS 的简化版”。但 LVGL 的样式更轻量,没有层叠、继承那些复杂规则。每个控件维护一个样式链表,渲染时按优先级叠加。

  • 复用性高:一个样式可以给多个控件用,节省内存
  • 状态分离:默认、按下、禁用、聚焦……各自独立配置
  • 局部覆盖:子控件可以覆盖父控件的样式,粒度很细

我的经验:项目里尽量把通用样式提取出来,比如“主按钮样式”“警告按钮样式”。别每个控件都单独写样式,后期维护会疯掉。

4.2 样式属性的四大分类

LVGL 的样式属性很多,但归纳起来就四大类。我按项目中的使用频率排个序:

分类 典型属性 使用频率 说明
背景 bg_color, bg_opa, bg_grad 极高 几乎每个控件都要配
边框 border_color, border_width, border_side 高 按钮、卡片、输入框常用
文本 text_color, text_font, text_align 高 标签、按钮文字、列表项
阴影 shadow_color, shadow_width, shadow_ofs_x/y 中 提升层次感,但耗性能

4.2.1 背景属性

背景是控件的“底子”。LVGL 支持纯色、渐变、圆角,甚至图片背景。

// 创建一个样式,设置渐变背景
lv_style_t style;
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_color_hex(0x3498db));
lv_style_set_bg_grad_color(&style, lv_color_hex(0x2980b9));
lv_style_set_bg_grad_dir(&style, LV_GRAD_DIR_VER);  // 垂直渐变
lv_style_set_radius(&style, 8);                      // 圆角

我曾经踩过的坑:渐变方向用错了,按钮看起来像被切了一刀。后来我习惯先画个草图,再写代码。另外,bg_opa 不要设成 0 还指望控件显示,那等于透明。

4.2.2 边框属性

边框不只是画个框。你可以控制哪条边显示、边框宽度、颜色,甚至虚线。

lv_style_set_border_color(&style, lv_color_hex(0x2c3e50));
lv_style_set_border_width(&style, 2);
lv_style_set_border_side(&style, LV_BORDER_SIDE_BOTTOM);  // 只显示底边

嗯,这里要注意:border_side 可以组合,比如 LV_BORDER_SIDE_LEFT | LV_BORDER_SIDE_RIGHT。我做过一个列表,每项只显示底边分割线,效果很干净。

4.2.3 文本属性

文本属性控制控件里文字的样子。字体、颜色、对齐方式、行间距……

lv_style_set_text_color(&style, lv_color_hex(0xffffff));
lv_style_set_text_font(&style, &lv_font_montserrat_14);
lv_style_set_text_align(&style, LV_TEXT_ALIGN_CENTER);

个人习惯:字体尽量用内置的,自定义字体会增加 flash 占用。如果必须用,记得做字体压缩,后面章节会讲。

4.2.4 阴影属性

阴影能让控件“浮”起来。但阴影是性能杀手,尤其嵌入式设备。

lv_style_set_shadow_color(&style, lv_color_hex(0x000000));
lv_style_set_shadow_opa(&style, 80);
lv_style_set_shadow_width(&style, 10);
lv_style_set_shadow_ofs_x(&style, 3);
lv_style_set_shadow_ofs_y(&style, 5);

避坑指南:我曾经在一个 240x320 的屏幕上给 20 个按钮都加了阴影,结果帧率直接掉到 15fps。后来只给弹窗和悬浮按钮加阴影,其他用边框代替。阴影宽度超过 15 时,渲染开销会指数级增长。

4.3 样式状态的机制

这才是样式系统的精髓。同一个控件,在不同状态下可以呈现不同外观。

LVGL 支持的状态用位掩码表示:

状态宏 值 说明
LV_STATE_DEFAULT 0x0000 默认状态,无特殊交互
LV_STATE_PRESSED 0x0002 按下状态
LV_STATE_DISABLED 0x0004 禁用状态
LV_STATE_FOCUSED 0x0008 聚焦状态(键盘/遥控器)
LV_STATE_CHECKED 0x0010 选中状态(复选框、开关)

4.3.1 默认状态

默认状态是控件的“素颜”。所有没被其他状态覆盖的属性,都走默认状态。

lv_style_set_bg_color(&style_default, lv_color_hex(0xecf0f1));
lv_style_set_text_color(&style_default, lv_color_hex(0x2c3e50));

4.3.2 按下状态

按下状态给用户“触感”反馈。颜色变深、阴影缩小、甚至位移。

lv_style_set_bg_color(&style_pressed, lv_color_hex(0x2980b9));
lv_style_set_shadow_ofs_y(&style_pressed, 2);  // 阴影下移,模拟按下

为什么按下时阴影要下移?因为视觉上按钮被“按下去”了,阴影应该更贴近按钮底部。我做过一个测试,阴影下移 2-3 像素,用户反馈手感最好。

4.3.3 禁用状态

禁用状态告诉用户“这个按钮现在不能用”。通常降低透明度、变灰。

lv_style_set_bg_color(&style_disabled, lv_color_hex(0xbdc3c7));
lv_style_set_text_color(&style_disabled, lv_color_hex(0x95a5a6));
lv_style_set_opa(&style_disabled, 128);  // 半透明

我曾经犯过的错:禁用状态只改了颜色,没改透明度。结果在深色背景下,禁用按钮和正常按钮几乎分不清。后来我强制要求:禁用状态必须同时降低饱和度和透明度,两者缺一不可。

4.4 状态与样式的绑定

样式和状态怎么绑定?通过 lv_obj_add_state() 和 lv_obj_clear_state()。

// 创建按钮
lv_obj_t *btn = lv_btn_create(lv_scr_act());

// 添加默认样式
lv_obj_add_style(btn, &style_default, 0);

// 添加按下样式
lv_obj_add_style(btn, &style_pressed, LV_STATE_PRESSED);

// 添加禁用样式
lv_obj_add_style(btn, &style_disabled, LV_STATE_DISABLED);

// 模拟禁用
lv_obj_add_state(btn, LV_STATE_DISABLED);

注意第三个参数:状态掩码。0 表示默认状态。LVGL 会自动根据当前状态选择对应的样式。如果某个属性在按下状态没定义,就回退到默认状态。

优先级规则:精确匹配 > 默认状态 > 父控件样式。LVGL 会遍历控件的样式链表,找到第一个匹配状态的样式。

4.5 知识体系总览

下面这张图概括了本章的核心内容。我建议你保存下来,写代码时对照着看。

LVGL 样式系统知识体系 lv_style_t 样式 属性分类 状态机制 背景 边框 文本 阴影 默认 按下 禁用 聚焦/选中 核心原则 样式 = 属性集合 + 状态条件 | 精确匹配 > 默认状态 > 父控件样式

4.6 本章小结

样式系统是 LVGL 的“面子工程”。掌握它,你的界面才能有层次、有反馈、有质感。

  • 样式是属性的集合,可以复用、叠加、覆盖
  • 四大属性分类:背景、边框、文本、阴影。阴影慎用,性能敏感
  • 状态机制让控件“活”起来。默认、按下、禁用是最常用的三个状态
  • 绑定方式:lv_obj_add_style() + 状态掩码,LVGL 自动匹配

我的建议:写样式前先画状态转换图。哪个状态变颜色?哪个状态变阴影?画清楚了再写代码,能省一半调试时间。