【LVGL自学笔记暂存】
前言
之前一直在用cdroid和qt,没有用过其他图形框架,因工作原因需要用到,今天自学了一下lvgl在此记录一下自己的笔记(仅个人记录)
屏幕切换
1. 使用对象显示/隐藏(推荐简单场景)
这是最简单直接的页面切换方式:
// 创建两个页面
lv_obj_t * page1 = lv_obj_create(lv_scr_act());
lv_obj_t * page2 = lv_obj_create(lv_scr_act());// 初始化状态:显示page1,隐藏page2
lv_obj_clear_flag(page1, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(page2, LV_OBJ_FLAG_HIDDEN);// 切换函数
void switch_page(lv_obj_t * show_page, lv_obj_t * hide_page) {lv_obj_clear_flag(show_page, LV_OBJ_FLAG_HIDDEN);lv_obj_add_flag(hide_page, LV_OBJ_FLAG_HIDDEN);
}// 切换到page2
switch_page(page2, page1);
优点:
- 实现简单
- 保留页面状态
- 内存占用固定
缺点:
- 所有页面常驻内存
- 不适合页面很多的情况
2. 使用lv_scr_load()切换屏幕(推荐常规使用)
// 创建两个屏幕
lv_obj_t * screen1 = lv_obj_create(NULL);
lv_obj_t * screen2 = lv_obj_create(NULL);// 在screen1上添加内容
lv_obj_t * label1 = lv_label_create(screen1);
lv_label_set_text(label1, "Screen 1");// 在screen2上添加内容
lv_obj_t * label2 = lv_label_create(screen2);
lv_label_set_text(label2, "Screen 2");// 加载第一个屏幕
lv_scr_load(screen1);// 切换到第二个屏幕
lv_scr_load(screen2);
优点:
- LVGL官方推荐方式
- 自动管理屏幕生命周期
- 内存效率较高
缺点:
- 切换时会销毁原屏幕(除非手动保存)
3. 使用lv_scr_load_anim()添加动画效果
// 创建两个屏幕
lv_obj_t * screen1 = lv_obj_create(NULL);
lv_obj_t * screen2 = lv_obj_create(NULL);// 添加内容...// 带动画的屏幕切换
lv_scr_load_anim(screen2, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false);
可用动画效果:
LV_SCR_LOAD_ANIM_NONE
- 无动画LV_SCR_LOAD_ANIM_OVER_LEFT
- 从右侧滑入LV_SCR_LOAD_ANIM_OVER_RIGHT
- 从左侧滑入LV_SCR_LOAD_ANIM_OVER_TOP
- 从底部滑入LV_SCR_LOAD_ANIM_OVER_BOTTOM
- 从顶部滑入LV_SCR_LOAD_ANIM_MOVE_LEFT
- 向左移动LV_SCR_LOAD_ANIM_MOVE_RIGHT
- 向右移动LV_SCR_LOAD_ANIM_MOVE_TOP
- 向上移动LV_SCR_LOAD_ANIM_MOVE_BOTTOM
- 向下移动LV_SCR_LOAD_ANIM_FADE_ON
- 淡入
4. 使用选项卡视图(lv_tabview)
适合需要底部/顶部导航栏的多页面应用:
// 创建选项卡视图
lv_obj_t * tabview = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, 50);// 添加3个选项卡
lv_obj_t * tab1 = lv_tabview_add_tab(tabview, "Tab 1");
lv_obj_t * tab2 = lv_tabview_add_tab(tabview, "Tab 2");
lv_obj_t * tab3 = lv_tabview_add_tab(tabview, "Tab 3");// 在每个选项卡中添加内容
lv_obj_t * label1 = lv_label_create(tab1);
lv_label_set_text(label1, "This is Tab 1");// 编程切换选项卡
lv_tabview_set_act(tabview, 1, LV_ANIM_ON); // 切换到第2个选项卡
5. 页面管理器
对于复杂应用,可以实现一个页面管理器:
typedef enum {PAGE_HOME,PAGE_SETTINGS,PAGE_ABOUT,PAGE_COUNT
} page_id_t;lv_obj_t * pages[PAGE_COUNT] = {NULL};void page_init() {for(int i = 0; i < PAGE_COUNT; i++) {pages[i] = lv_obj_create(NULL);// 初始化各页面内容...}
}void switch_to_page(page_id_t page) {static page_id_t current_page = PAGE_HOME;if(page >= 0 && page < PAGE_COUNT && page != current_page) {lv_scr_load_anim(pages[page], LV_SCR_LOAD_ANIM_FADE_ON, 200, 0, false);current_page = page;}
}
2. 使用lv_scr_load()切换屏幕(推荐常规使用)
// 创建两个屏幕
lv_obj_t * screen1 = lv_obj_create(NULL);
lv_obj_t * screen2 = lv_obj_create(NULL);// 在screen1上添加内容
lv_obj_t * label1 = lv_label_create(screen1);
lv_label_set_text(label1, "Screen 1");// 在screen2上添加内容
lv_obj_t * label2 = lv_label_create(screen2);
lv_label_set_text(label2, "Screen 2");// 加载第一个屏幕
lv_scr_load(screen1);// 切换到第二个屏幕
lv_scr_load(screen2);
关于lv_scr_load()
和lv_scr_load_anim()
-
lv_scr_load()
确实是直接切换页面(无动画) -
lv_scr_load_anim()
支持带动画效果的切换 -
// 示例:默认切换(前一个屏幕被自动释放) lv_obj_t *screen1 = lv_obj_create(NULL); // 创建屏幕1 lv_obj_t *screen2 = lv_obj_create(NULL); // 创建屏幕2 lv_scr_load(screen1); // 显示屏幕1 lv_scr_load(screen2); // 切换到屏幕2,screen1被自动删除
-
切换后会释放前一个屏幕的内存(除非手动保留)
-
方法1:使用
lv_obj_clear_flag(obj, LV_OBJ_FLAG_AUTO_DELETE)
lv_obj_t *screen1 = lv_obj_create(NULL); lv_obj_clear_flag(screen1, LV_OBJ_FLAG_AUTO_DELETE); // 禁止自动删除 lv_scr_load(screen1);// 切换到screen2时,screen1不会被删除 lv_obj_t *screen2 = lv_obj_create(NULL); lv_scr_load(screen2); // screen1仍保留在内存中
方法2:全局变量保存屏幕对象
lv_obj_t *screens[2]; // 全局保存void create_screens() {screens[0] = lv_obj_create(NULL); // 屏幕1screens[1] = lv_obj_create(NULL); // 屏幕2lv_scr_load(screens[0]); }void switch_screen(int id) {
-
需要补充的理解:
// 典型用法示例
lv_obj_t * screen1 = lv_obj_create(NULL); // 必须创建为独立屏幕
lv_obj_t * screen2 = lv_obj_create(NULL);// 直接切换
lv_scr_load(screen1);// 带动画切换(从右侧滑入,耗时300ms)
lv_scr_load_anim(screen2, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 300, 0, false);
Q:手动保留会导致内存泄漏吗?
A:不会,但需在应用退出时手动调用lv_obj_del()
释放所有屏幕。
Q:如何判断对象是否已被删除?
A:LVGL内部会标记已删除对象,访问已被删除的对象可能导致崩溃。
Q:保留多个屏幕会显著增加内存占用吗?
A:取决于页面复杂度,简单页面(如纯色背景+少量控件)通常占用较少内存。
父对象
1. 父对象的基本选择方式
创建对象时必须指定父对象,决定了对象的显示层级和生命周期:
// 创建父容器
lv_obj_t *parent = lv_obj_create(lv_scr_act()); // 创建子对象(按钮),父对象为parent
lv_obj_t *btn = lv_btn_create(parent);
2. LVGL父子关系核心特点
(1) 生命周期管理(与Qt对比)
特性 | LVGL | Qt |
---|---|---|
对象删除 | 删除父对象会自动删除所有子对象 | 同LVGL |
内存所有权 | 严格父子层级管理 | 可通过QPointer等弱引用打破 |
独立存活 | 子对象不能脱离父对象独立存在 | 子对象可重新设置父对象 |
(2) 关键行为
-
自动删除:当父对象被删除时,所有子对象递归删除
lv_obj_del(parent); // 会自动删除btn及其他子对象
-
层级限制:子对象永远显示在父对象的绘图区域内(裁剪效果)
3. 父对象选择策略
(1) 典型父对象类型
父对象类型 | 适用场景 | 示例 |
---|---|---|
屏幕对象(NULL ) | 顶级页面 | lv_obj_create(NULL) |
普通容器 | 分组相关控件 | 表单、工具条 |
滚动容器 | 需要滚动的区域 | lv_scroll_create(parent) |
窗口对象 | 弹出层/对话框 | lv_win_create(parent) |
(2) 选择建议
// ✅ 推荐做法:明确层级关系
lv_obj_t *main_screen = lv_obj_create(NULL);
lv_obj_t *toolbar = lv_obj_create(main_screen);
lv_obj_t *btn = lv_btn_create(toolbar);// ❌ 避免混乱的父子关系
lv_obj_t *btn1 = lv_btn_create(lv_scr_act());
lv_obj_t *btn2 = lv_btn_create(btn1); // 非常规用法!
4. 特殊管理技巧
(1) 临时解除父子关系
// 将子对象移动到新父对象
lv_obj_set_parent(child, new_parent);// 临时移出(需自行管理内存)
lv_obj_remove_from_parent(child);
lv_obj_del(child); // 必须手动删除
(2) 防止自动删除
lv_obj_t *child = lv_label_create(parent);
lv_obj_clear_flag(child, LV_OBJ_FLAG_AUTO_DELETE); // 禁用自动删除
lv_obj_del(parent); // 此时child不会被自动删除
5. 与Qt的关键差异
特性 | LVGL | Qt |
---|---|---|
对象树遍历 | 只能向下遍历(lv_obj_get_child ) | 支持双向遍历(QObject::parent() ) |
信号/槽绑定 | 无自动连接机制 | 有元对象系统自动管理 |
样式继承 | 部分属性继承 | 完整样式表继承 |
内存模型 | 严格父子所有权 | 支持多种所有权模型 |
6. 实际应用示例
(1) 动态界面管理
// 创建可复用的组件
lv_obj_t *create_button_group(lv_obj_t *parent) {lv_obj_t *cont = lv_obj_create(parent);lv_btn_create(cont);lv_btn_create(cont);return cont; // 返回容器对象
}// 使用时明确父子关系
lv_obj_t *main_page = lv_obj_create(NULL);
lv_obj_t *btn_group = create_button_group(main_page);
(2) 安全删除策略
void safe_remove(lv_obj_t *obj) {if(lv_obj_is_valid(obj)) { // 检查对象是否有效lv_obj_t *parent = lv_obj_get_parent(obj);if(parent) lv_obj_del(obj); // 正常删除else {lv_obj_remove_from_parent(obj);lv_obj_del(obj); // 处理无父对象情况}}
}
补充:嵌套容器
1. 两种方式的本质区别
方式 | 直接挂载到屏幕 | 嵌套容器 |
---|---|---|
代码示例 | lv_label_create(lv_scr_act()) | lv_label_create(container) |
适用场景 | 快速原型开发、简单界面 | 正式项目、复杂UI |
管理成本 | 需单独操作每个控件 | 可批量操作容器内所有子对象 |
渲染性能 | 较高(扁平结构) | 略低(需处理嵌套关系) |
内存占用 | 较低 | 稍高(多一个容器对象) |
2. 嵌套容器的核心价值
(1) 布局管理效率提升
// 统一调整容器内所有子对象
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW); // 自动排列所有子Label
lv_obj_set_style_pad_all(container, 10, 0); // 统一设置内边距
(2) 动态控制显隐
// 一键隐藏/显示整个功能模块
lv_obj_add_flag(settings_panel, LV_OBJ_FLAG_HIDDEN); // 隐藏所有设置项Label
(3) 样式继承优化
// 容器设置公共样式
lv_obj_set_style_text_font(container, &my_font, 0);
// 子Label自动继承字体,无需重复设置
lv_obj_t *label1 = lv_label_create(container);
lv_obj_t *label2 = lv_label_create(container);
3. 实际项目中的层级设计
(1) 推荐层级结构
屏幕 (lv_scr_act())
└── 主容器├── 标题栏容器│ ├── 标题Label│ └── 图标Button└── 内容区容器└── 表单容器├── 输入框Label└── 输入框TextArea
(2) 典型代码实现
// 1. 创建主容器
lv_obj_t *main_cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(main_cont, lv_pct(100), lv_pct(100));// 2. 创建标题栏子容器
lv_obj_t *header = lv_obj_create(main_cont);
lv_obj_set_size(header, lv_pct(100), 50);// 3. 在标题栏添加Label
lv_obj_t *title = lv_label_create(header);
lv_label_set_text(title, "系统设置");// 4. 内容区子容器
lv_obj_t *content = lv_obj_create(main_cont);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);// 5. 在内容区添加多个Label
for(int i=0; i<3; i++){lv_obj_t *item = lv_label_create(content);lv_label_set_text_fmt(item, "配置项 %d", i+1);
}
4. 何时选择直接挂载?
(1) 适用场景
- 快速验证UI效果
- 只有一个Label的简单界面
- 内存极度受限的嵌入式设备
(2) 示例代码
// 临时调试用的Label
lv_obj_t *debug_label = lv_label_create(lv_scr_act());
lv_label_set_text(debug_label, "传感器值: 25.6℃");
lv_obj_align(debug_label, LV_ALIGN_TOP_RIGHT, -10, 10);
5. 进阶技巧:动态父对象切换
// 将Label从容器A移动到容器B
lv_obj_set_parent(my_label, container_B);// 移动后自动保持相对位置
lv_obj_align(my_label, LV_ALIGN_CENTER, 0, 0);
窗口管理
1. 核心方案对比
维度 | 双屏幕方案 (lv_scr_load ) | 单屏幕+双窗口方案 (lv_win ) |
---|---|---|
内存占用 | 较高(同时保留两个屏幕) | 较低(窗口共享同一屏幕) |
渲染性能 | 切换时全屏重绘(开销大) | 局部更新(仅窗口内容变化) |
状态保留 | 需手动保存状态 | 窗口隐藏时自动保留状态 |
代码复杂度 | 简单直接 | 需处理窗口层叠关系 |
适用场景 | 全屏页面切换(如设置页→主页) | 局部弹窗/浮动窗口(如对话框+主界面) |
2. 内存占用实测数据
测试环境:STM32F429,LVGL v8.3,320x240分辨率
-
双屏幕方案:
lv_obj_t *scr1 = lv_obj_create(NULL); // 约150B lv_obj_t *scr2 = lv_obj_create(NULL); // 约150B
总内存:300B(不含内容)
-
单屏幕+双窗口:
lv_obj_t *scr = lv_scr_act(); lv_obj_t *win1 = lv_win_create(scr); // 约500B lv_obj_t *win2 = lv_win_create(scr); // 约500B lv_obj_add_flag(win2, LV_OBJ_FLAG_HIDDEN);
总内存:500B(但可动态释放隐藏窗口)
3. 性能关键指标
指标 | 双屏幕方案 | 单屏幕+双窗口 |
---|---|---|
切换时间(ms) | 15-25(全屏重绘) | 5-10(局部更新) |
输入延迟 | 较高(需重建输入焦点) | 较低(保持焦点链) |
动画流畅度 | 适合全屏动画 | 适合局部微交互 |
4. 具体场景推荐
(1) 必须用双屏幕的场景
-
全屏应用切换(如手机Launcher→设置)
// 初始化 lv_obj_t *home_scr = lv_obj_create(NULL); lv_obj_t *settings_scr = lv_obj_create(NULL);// 切换 lv_scr_load_anim(home_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false);
(2) 推荐单屏幕+窗口的场景
-
浮动UI系统(如HUD+弹窗)
// 主窗口 lv_obj_t *main_win = lv_win_create(lv_scr_act());// 弹窗(默认隐藏) lv_obj_t *dialog = lv_win_create(lv_scr_act()); lv_obj_add_flag(dialog, LV_OBJ_FLAG_HIDDEN);// 显示弹窗 lv_obj_clear_flag(dialog, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(dialog);
5. 混合方案示例
需求:主界面常驻,临时页面可快速切换
// 主屏幕(常驻)
lv_obj_t *main_scr = lv_obj_create(NULL);
lv_scr_load(main_scr);// 临时页面(动态创建/删除)
void show_temp_page() {lv_obj_t *temp_scr = lv_obj_create(NULL);lv_scr_load_anim(temp_scr, LV_SCR_LOAD_ANIM_FADE_IN, 200, 0, true);// 返回时自动删除
}// 或使用窗口方案
void show_temp_win() {lv_obj_t *win = lv_win_create(main_scr);lv_obj_set_size(win, 200, 150);lv_obj_center(win);
}
6. 决策流程图
7. 终极建议
- 内存<100KB:强制使用单屏幕+动态窗口(避免OOM)
- 内存>256KB:双屏幕方案(开发更简单)
- 高频切换:单屏幕+
lv_obj_swap()
隐藏/显示 - 触控响应优先:窗口方案(保持焦点链)
示例代码(最优性能方案):
// 预创建两个屏幕,但不同时激活
lv_obj_t *scr1 = lv_obj_create(NULL);
lv_obj_t *scr2 = lv_obj_create(NULL);
lv_obj_clear_flag(scr1, LV_OBJ_FLAG_AUTO_DELETE);// 快速切换(保留scr1状态)
void switch_to_scr2() {lv_scr_load_anim(scr2, LV_SCR_LOAD_ANIM_OVER_LEFT, 200, 0, false);lv_obj_del(scr1); // 手动控制删除
}
事件响应机制**(基于回调的事件分发模型**)
1. 事件系统核心架构
2. 事件处理全流程
(1) 事件触发阶段
- 输入设备(触摸屏/编码器/按键)产生原始数据
- LVGL驱动层 将原始数据转换为标准事件(如
LV_EVENT_PRESSED
) - 事件入队:事件被放入 异步事件队列(默认深度16)
(2) 事件分发阶段
- 命中测试:通过
lv_obj_hit_test()
确定目标对象 - 冒泡传递:事件沿父对象链向上传递(除非调用
lv_event_stop_bubbling()
) - 回调执行:依次执行对象上的注册回调
(3) 默认处理阶段
- 若用户未处理事件,LVGL执行 内置默认行为(如按钮按下状态切换)
3. 关键API及用法
(1) 事件回调注册
// 注册事件回调(支持LV_EVENT_ALL监听所有事件)
lv_obj_add_event_cb(obj, event_cb, LV_EVENT_CLICKED, user_data);// 回调函数原型
static void event_cb(lv_event_t * e) {lv_obj_t * target = lv_event_get_target(e); // 获取触发对象lv_event_code_t code = lv_event_get_code(e); // 获取事件类型void * user_data = lv_event_get_user_data(e);// 获取用户数据switch(code) {case LV_EVENT_CLICKED:// 处理点击事件break;case LV_EVENT_VALUE_CHANGED:// 处理值变化break;}
}
(2) 事件控制方法
API | 作用 | 示例 |
---|---|---|
lv_event_send(obj, code) | 手动发送事件 | 模拟点击:lv_event_send(btn, LV_EVENT_CLICKED) |
lv_event_stop_bubbling(e) | 停止事件冒泡 | 在回调中调用阻止父对象处理 |
lv_event_mark_deleted(e) | 标记对象待删除(安全删除) | 在回调中删除触发对象时使用 |
4. 事件类型详解
(1) 输入设备事件
事件类型 | 触发条件 | 典型应用场景 |
---|---|---|
LV_EVENT_PRESSED | 对象被按下 | 按钮按下态样式切换 |
LV_EVENT_RELEASED | 对象被释放 | 点击确认操作 |
LV_EVENT_LONG_PRESSED | 长按(默认1秒) | 弹出上下文菜单 |
LV_EVENT_GESTURE | 手势识别(滑动/缩放) | 图库浏览 |
(2) 控件特定事件
事件类型 | 对应控件 | 数据获取方法 |
---|---|---|
LV_EVENT_VALUE_CHANGED | 滑块/开关/下拉列表 | lv_slider_get_value(e->target) |
LV_EVENT_DRAW_PART_BEGIN | 自定义绘制控件 | 通过e->draw_part 修改绘制参数 |
LV_EVENT_SCROLL | 滚动容器 | lv_obj_get_scroll_top(e->target) |
5. 高级特性
(1) 事件过滤器
// 只处理特定事件(减少回调触发次数)
lv_obj_add_event_cb_filter(obj, [](lv_event_t * e) {return (e->code == LV_EVENT_CLICKED) ? LV_EVENT_RES_PASS : LV_EVENT_RES_IGNORE;
});
(2) 异步事件处理
// 在事件回调中延迟执行
lv_async_call([](void * data) {lv_obj_add_state(data, LV_STATE_CHECKED);
}, obj);
(3) 自定义事件
// 定义自定义事件码(>LV_EVENT_LAST)
#define MY_EVENT_CUSTOM (LV_EVENT_LAST + 1)// 发送自定义事件
lv_event_send(obj, MY_EVENT_CUSTOM, custom_data);
6. 性能优化技巧
(1) 事件回调优化
// 错误做法:频繁注册/删除回调
void update_ui() {lv_obj_remove_event_cb(btn, old_cb); // 避免!lv_obj_add_event_cb(btn, new_cb, ...);
}// 正确做法:重用回调
static void smart_cb(lv_event_t * e) {if(condition) handler_A(e);else handler_B(e);
}
(2) 事件数据共享
// 通过user_data传递结构体
typedef struct {lv_obj_t * slider;lv_obj_t * label;
} MyData;MyData * data = lv_mem_alloc(sizeof(MyData));
lv_obj_add_event_cb(btn, event_cb, LV_EVENT_ALL, data);
(3) 输入设备优化
// 减少不必要的事件上报
lv_indev_set_read_cb(indev, [](lv_indev_data_t * data) {static int last_pos;if(abs(data->point.x - last_pos) < 5) return false; // 忽略微小移动last_pos = data->point.x;return true;
});
7. 调试与问题排查
(1) 事件流监控
// 添加全局事件监视器
lv_obj_add_event_cb(lv_scr_act(), [](lv_event_t * e) {printf("Event %d on %p\n", e->code, e->target);
}, LV_EVENT_ALL, NULL);
(2) 常见问题解决
-
事件未触发:
- 检查对象是否设置
LV_OBJ_FLAG_CLICKABLE
- 确认父对象未设置
LV_OBJ_FLAG_HIDDEN
或LV_OBJ_FLAG_CLICK_FOCUSABLE
- 检查对象是否设置
-
内存泄漏:
在回调中动态分配的数据需通过LV_EVENT_DELETE
事件释放:if(e->code == LV_EVENT_DELETE) {lv_mem_free(e->user_data); }
拓展 LV_STATE_CHECKED
LV_STATE_CHECKED
是对象状态标志之一,用于表示控件被选中/激活/切换的状态1. 本质与作用
- 类型:
lv_state_t
枚举值(32位状态位掩码) - 二进制值:
0x00000002
(第2位) - 典型应用控件:
- 复选框(lv_checkbox)
- 开关(lv_switch)
- 单选按钮(lv_radio)
- 选项卡(lv_tabview)
- 自定义可选中控件
2. 状态管理API
方法 作用 示例 lv_obj_add_state(obj, state)
添加状态标志 lv_obj_add_state(btn, LV_STATE_CHECKED)
lv_obj_clear_state(obj, state)
清除状态标志 lv_obj_clear_state(sw, LV_STATE_CHECKED)
lv_obj_has_state(obj, state)
检查状态是否存在 if(lv_obj_has_state(cb, LV_STATE_CHECKED)) {...}
3. 视觉表现机制
当对象处于
LV_STATE_CHECKED
状态时,LVGL会自动应用对应的样式属性:/* 在样式表中定义选中状态样式 */ static lv_style_t style_checked; lv_style_init(&style_checked); lv_style_set_bg_color(&style_checked, lv_palette_main(LV_PALETTE_BLUE)); lv_style_set_text_color(&style_checked, lv_color_white());/* 将样式绑定到控件的选中状态 */ lv_obj_add_style(btn, &style_checked, LV_STATE_CHECKED);
典型样式属性变化:
- 背景色改变
- 文字/图标颜色变化
- 添加选中标记(如对勾图标)
- 控件位置微调(如开关滑块位移)
4. 与其他状态的关系
状态标志 常与CHECKED组合的场景 复合效果 LV_STATE_PRESSED
按钮在选中状态下被按下 深色背景+按压动画 LV_STATE_DISABLED
禁用已选中的控件 灰色外观+保留选中标记 LV_STATE_FOCUSED
键盘导航至已选中控件 焦点边框+选中样式 // 典型复合状态处理 if(lv_obj_has_state(obj, LV_STATE_CHECKED | LV_STATE_FOCUSED)) {// 处理既被选中又获得焦点的状态 }
5. 实际应用示例
(1) 自定义可选中按钮
lv_obj_t *btn = lv_btn_create(lv_scr_act()); lv_obj_add_event_cb(btn, [](lv_event_t *e) {lv_obj_t *obj = lv_event_get_target(e);if(lv_obj_has_state(obj, LV_STATE_CHECKED)) {lv_obj_clear_state(obj, LV_STATE_CHECKED);} else {lv_obj_add_state(obj, LV_STATE_CHECKED);} }, LV_EVENT_CLICKED, NULL);
(2) 动态样式更新
// 根据选中状态改变标签文本 lv_obj_t *label = lv_label_create(btn); lv_obj_add_event_cb(btn, [](lv_event_t *e) {lv_obj_t *label = lv_obj_get_child(e->target, 0);if(lv_obj_has_state(e->target, LV_STATE_CHECKED)) {lv_label_set_text(label, "ON");} else {lv_label_set_text(label, "OFF");} }, LV_EVENT_VALUE_CHANGED, NULL);
6. 内部实现原理
-
状态存储:每个
lv_obj_t
内部维护state
变量(uint32_t
) -
样式匹配:对象渲染时,LVGL按优先级匹配当前状态组合的样式:
匹配顺序: LV_STATE_CHECKED | LV_STATE_PRESSED → LV_STATE_CHECKED → LV_STATE_DEFAULT
-
事件触发:状态变化时会发送
LV_EVENT_VALUE_CHANGED
事件
7. 开发者注意事项
-
不要直接赋值状态:
// 错误做法 obj->state = LV_STATE_CHECKED; // 正确做法 lv_obj_add_state(obj, LV_STATE_CHECKED);
-
自定义控件需处理状态:
// 在draw事件中检查状态 if(lv_obj_has_state(obj, LV_STATE_CHECKED)) {// 绘制选中特效 }
-
性能优化:对频繁切换的对象,使用
lv_obj_clear_state
而非lv_obj_set_state
(避免冗余样式重算)
与其他GUI库的对比
特性 LVGL Qt Android 状态标志 位掩码组合 QStyle::State枚举 View.STATE_*常量 样式继承 部分属性继承 完全继承 通过Theme定义 状态持久性 显式添加/清除 通常由控件自动管理 由视图系统管理 - 类型:
定时器
一、LVGL 定时器类型
定时器类型 | 创建方式 | 触发机制 | 典型应用场景 |
---|---|---|---|
系统定时器 | lv_timer_create() | 基于系统滴答(tick)触发 | 通用周期性任务 |
动画定时器 | lv_anim_create() | 绑定到对象属性变化 | UI 动画效果 |
异步调用 | lv_async_call() | 下次主循环执行 | 跨线程安全操作 |
二、系统定时器 (lv_timer)
1. 基本使用
// 创建定时器(单位:毫秒)
lv_timer_t * timer = lv_timer_create(timer_cb, 500, NULL); // 回调函数
void timer_cb(lv_timer_t * timer) {static int count = 0;lv_label_set_text_fmt(label, "Count: %d", count++);// 运行10次后删除if(count >= 10) lv_timer_del(timer);
}
2. 关键API
API | 作用 |
---|---|
lv_timer_create(cb, period, user_data) | 创建定时器 |
lv_timer_del(timer) | 立即删除定时器 |
lv_timer_pause(timer) | 暂停(保留剩余周期) |
lv_timer_resume(timer) | 恢复运行 |
lv_timer_set_repeat_count(timer, n) | 设置重复次数(-1为无限) |
3. 内部机制
- 时间基准:依赖
lv_tick_inc()
函数更新的系统滴答 - 调度方式:在主循环 (
lv_task_handler
) 中检查触发 - 优先级:定时器按创建顺序执行,无优先级抢占
三、动画定时器 (lv_anim)
1. 基本使用
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_x); // 设置属性
lv_anim_set_var(&anim, obj); // 目标对象
lv_anim_set_values(&anim, 0, 100); // 起始/结束值
lv_anim_set_time(&anim, 1000); // 持续时间(ms)
lv_anim_set_path_cb(&anim, lv_anim_path_linear); // 动画曲线
lv_anim_start(&anim); // 启动
2. 动画曲线类型
曲线函数 | 效果 |
---|---|
lv_anim_path_linear | 线性变化 |
lv_anim_path_ease_in | 先慢后快 |
lv_anim_path_ease_out | 先快后慢 |
lv_anim_path_overshoot | 超过终点再回弹 |
3. 动画控制
// 暂停/继续特定对象的动画
lv_anim_pause(obj, anim_exec_cb);
lv_anim_resume(obj, anim_exec_cb);// 删除所有动画
lv_anim_delete(obj, NULL);
四、异步调用 (lv_async_call)
用于线程安全操作:
void async_task(void * data) {lv_obj_t * label = (lv_obj_t *)data;lv_label_set_text(label, "Updated from async");
}// 在其他线程/中断中调用
lv_async_call(async_task, label); // 下次主循环执行
五、定时器机制原理
-
时间管理:
- 底层依赖
lv_tick_inc(ms)
函数更新全局时间戳 - 需在硬件定时器中断中调用(如每1ms调用一次)
- 底层依赖
-
执行流程:
- 性能特征:
- 定时器回调在 主循环上下文 执行
- 单个回调阻塞会导致后续定时器延迟
- 默认最多支持 LV_TIMER_HANDLER_MAX_NUM(默认32)个定时器
六、最佳实践
-
资源管理:
// 定时器自动删除模式(运行1次) lv_timer_t * timer = lv_timer_create(cb, 1000, NULL); lv_timer_set_repeat_count(timer, 1);
-
高精度控制:
// 手动计算时间补偿 uint32_t last_run = lv_tick_get(); void timer_cb(lv_timer_t * t) {uint32_t now = lv_tick_get();uint32_t elapsed = now - last_run;last_run = now;// 使用elapsed做精确控制... }
-
错误处理:
if(!lv_timer_create(cb, period, data)) {LV_LOG_ERROR("Timer creation failed!"); }
七、常见问题解决方案
-
定时器不触发:
- 检查是否调用了
lv_tick_inc()
- 确认
lv_task_handler()
在主循环中执行
- 检查是否调用了
-
动画卡顿:
- 减少同时运行的动画数量
- 使用
lv_anim_set_early_apply(&anim, true)
立即应用首帧
-
内存泄漏:
-
在对象删除事件中清理关联定时器:
lv_obj_add_event_cb(obj, [](lv_event_t * e) {if(e->code == LV_EVENT_DELETE) {lv_anim_delete(e->target, NULL);} }, LV_EVENT_ALL, NULL);
-
- 性能特征:
动画
1. lv_anim_t
动画对象的本质
typedef struct _lv_anim_t {lv_anim_exec_xcb_t exec_cb; // 属性设置函数(如lv_obj_set_x)void * var; // 目标对象(通过lv_anim_set_var设置)int32_t start; // 起始值(lv_anim_set_values)int32_t end; // 结束值(lv_anim_set_values)uint32_t time; // 持续时间ms(lv_anim_set_time)/* 其他内部管理字段... */
} lv_anim_t;
2. 关键属性设置的作用
(1) 目标对象 lv_anim_set_var(&anim, obj)
-
功能:指定动画要作用的具体UI对象
-
底层影响:
-
-
典型值:
lv_obj_t*
类型指针(如按钮、标签等)
(2) 起止值 lv_anim_set_values(&anim, 0, 100)
- 功能:定义动画变化的数值范围
- 关键机制:
- 根据当前时间进度,通过动画路径函数(如
lv_anim_path_linear
)计算中间值 - 最终值通过
exec_cb
函数应用到目标对象
- 根据当前时间进度,通过动画路径函数(如
- 单位:与执行函数相关(如
lv_obj_set_x
的单位是像素)
(3) 持续时间 lv_anim_set_time(&anim, 1000)
-
功能:控制动画从开始到结束的总时间
-
内部处理:
// 伪代码:计算当前值 float progress = (current_time / total_time) * path_curve(time_ratio); int32_t current_val = start + (end - start) * progress;
-
性能影响:时间越长,每帧变化幅度越小,动画越平滑
3. 动画启动流程
您提到的三个属性设置后,还需要关键一步才会真正启动动画:
lv_anim_start(&anim); // 必须调用此函数动画才会生效
完整触发流程:
lv_anim_start
将动画添加到LVGL全局动画链表- 主循环 (
lv_task_handler
) 检测到动画需要更新 - 根据当前时间计算进度值(依赖
lv_tick_inc
提供的时间基准) - 调用
exec_cb
函数更新对象属性 - 触发对象重绘(自动调用
lv_obj_invalidate
)
4. 属性设置与视觉效果的关系
属性 | 视觉表现示例(移动动画) | 数值变化示例(1秒内) |
---|---|---|
var=obj, values=0→100 | 对象从x=0移动到x=100 | [0, 20, 40, 60, 80, 100] |
time=2000 | 移动速度减半 | [0, 10, 20,…,100] |
path_cb=ease_out | 先快后慢的移动 | [0, 50, 80, 95, 99, 100] |
5. 必须设置的四大属性
一个可运行的动画至少需要配置以下四项:
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_x); // 1.设置属性函数
lv_anim_set_var(&anim, obj); // 2.目标对象
lv_anim_set_values(&anim, 0, 100); // 3.起止值
lv_anim_set_time(&anim, 1000); // 4.持续时间
lv_anim_start(&anim); // 启动!
6. 完整动画生命周期
7. 常见问题解决方案
Q:设置参数后动画未启动?
- 确认调用了
lv_anim_start()
- 检查
lv_task_handler()
是否在主循环中执行 - 验证目标对象是否有效(未被删除)
Q:动画效果不符合预期?
- 检查
exec_cb
是否匹配属性(如用lv_obj_set_x
控制Y轴位置无效) - 确认时间单位是毫秒(
1000=1秒
) - 调试路径函数:改用
lv_anim_path_linear
测试基础功能
Q:如何暂停/继续动画?
// 暂停特定属性动画
lv_anim_pause(obj, (lv_anim_exec_xcb_t)lv_obj_set_x);// 继续播放
lv_anim_resume(obj, (lv_anim_exec_xcb_t)lv_obj_set_x);
拓展 lv_anim_path_linear
1. 本质与作用
-
功能:定义动画属性值的变化规律(如线性、缓入缓出等)
-
类型:
lv_anim_path_cb_t
函数指针 -
默认选项:
lv_anim_path_linear // 线性匀速 lv_anim_path_ease_in // 先慢后快(加速) lv_anim_path_ease_out // 先快后慢(减速) lv_anim_path_ease_in_out // 两头慢中间快 lv_anim_path_overshoot // 超过终点再回弹 lv_anim_path_bounce // 弹性反弹效果
2. 使用方法
(1) 设置线性动画
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_path_cb(&anim, lv_anim_path_linear); // 关键设置
lv_anim_start(&anim);
效果:属性值从起点到终点匀速变化(数学公式:value = start + (end - start) * (current_time / total_time)
)
(2) 设置非线性动画
lv_anim_set_path_cb(&anim, lv_anim_path_ease_out);
效果:动画开始时变化快,结束时逐渐变慢
3. 动态曲线对比
曲线类型 | 公式示意图 | 适用场景 |
---|---|---|
linear | ─────── | 进度条、机械运动 |
ease_in | ⠀⠀⠀\ | 自由落体、加速启动 |
ease_out | /⠀⠀⠀ | 刹车效果、平滑停止 |
ease_in_out | ⠀⠀/\ | 自然物体运动 |
overshoot | ───↗───↙── | 强调动作、弹窗出现 |
bounce | ─↑─↓─↑─ | 弹性控件、趣味交互 |
4. 底层实现原理
当调用 lv_anim_set_path_cb(&anim, lv_anim_path_linear)
时:
-
LVGL会将此函数指针存入动画对象
-
在动画每一帧计算时调用该函数:
// 伪代码示例 int32_t current_value = path_cb(start, end, time_ratio);
-
lv_anim_path_linear
的实现:int32_t lv_anim_path_linear(const lv_anim_t * anim) {return anim->start + (anim->end - anim->start) * anim->time_ratio; }
5. 自定义动画曲线
(1) 创建自定义路径函数
int32_t my_custom_path(const lv_anim_t * anim) {// 二次函数曲线float t = anim->time_ratio; // 当前进度[0.0-1.0]return anim->start + (anim->end - anim->start) * t * t;
}// 使用自定义曲线
lv_anim_set_path_cb(&anim, my_custom_path);
(2) 使用贝塞尔曲线
#include "lv_anim_bezier.h"int32_t bezier_path(const lv_anim_t * anim) {// 控制点:P0(0,0), P1(0.3,0.8), P2(1,1)float t = anim->time_ratio;float val = lv_bezier3(t, 0, 0.3, 0.8, 1); return anim->start + (anim->end - anim->start) * val;
}
6. 实际效果验证
可以通过以下代码实时观察不同曲线的值变化:
lv_obj_t * chart = lv_chart_create(lv_scr_act());
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, chart);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)anim_test_cb);
lv_anim_set_values(&anim, 0, 100);
lv_anim_set_time(&anim, 1000);
lv_anim_set_path_cb(&anim, lv_anim_path_ease_out); // 切换不同曲线观察
lv_anim_start(&anim);void anim_test_cb(lv_obj_t * chart, int32_t val) {lv_chart_set_next_value(chart, lv_chart_get_series_next(chart, NULL), val);
}
7. 性能注意事项
-
计算开销(从低到高):
linear < ease_in/out < overshoot < bounce
-
优化建议:
- 对大量同时运行的动画使用
linear
- 关键UI动画可使用复杂曲线
- 避免在性能受限的平台频繁使用
bounce
- 对大量同时运行的动画使用