1. LVGL动画基础:动画三要素与状态机
做嵌入式GUI开发,动画这块儿是绕不开的坎儿。我记得刚接触LVGL那会儿,总觉得动画就是让控件动起来,简单。结果真到项目里,帧率上不去、动画卡顿、内存泄漏……各种问题全来了。后来我才明白,搞懂动画的底层逻辑,比背API重要得多。
这一章,咱们就把LVGL动画的根儿刨清楚。说白了,就是三样东西:时间、路径、回调。再加上一个动画状态机,你就掌握了LVGL动画的全部套路。
1.1 动画三要素:时间、路径、回调
LVGL里任何一个动画,都离不开这三个要素。我习惯把它们叫做「动画铁三角」。
| 要素 | 作用 | 典型参数/函数 |
|---|---|---|
| 时间 | 控制动画持续时长、延迟、重复次数 | anim.set_duration(500)、anim.set_repeat_count(3) |
| 路径 | 定义动画值随时间变化的曲线 | lv_anim_path_linear、lv_anim_path_ease_in_out |
| 回调 | 设置动画起始/结束值、执行自定义操作 | anim.set_values(0, 100)、anim.set_custom_exec_cb() |
核心理解: 时间决定「多久」,路径决定「怎么变」,回调决定「变什么」。三者缺一不可。
举个例子。你想让一个按钮在500ms内从透明度0变成255,用线性变化。代码是这样的:
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, btn); // 动画对象
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_opa); // 回调:设置透明度
lv_anim_set_values(&anim, 0, 255); // 起始值→结束值
lv_anim_set_time(&anim, 500); // 时长500ms
lv_anim_set_path_cb(&anim, lv_anim_path_linear); // 线性路径
lv_anim_start(&anim); // 启动
你看,三要素清清楚楚。我在项目里经常看到新手把set_values和set_exec_cb搞混——前者是数值范围,后者是把这个数值应用到哪个属性上。嗯,这里要注意。
1.2 lv_anim_t结构体详解
lv_anim_t是LVGL动画的「本体」。我刚开始看源码时,觉得这结构体字段太多,后来发现其实就几类:
- 目标对象相关:
var——你要让谁动 - 时间控制:
start_time、act_time、duration、repeat_cnt - 值控制:
start_value、current_value、end_value - 路径与回调:
path_cb、exec_cb、start_cb、completed_cb - 状态标记:
state——这个后面讲状态机时会细说
我的习惯: 每次创建动画前,先用lv_anim_init(&anim)清零结构体。别小看这一步,我曾经因为没初始化,残留的旧状态导致动画行为诡异,查了整整一下午。
这里有个关键点——exec_cb的类型是lv_anim_exec_xcb_t。说白了就是一个函数指针,接收动画对象和当前值。LVGL内置了很多现成的执行回调,比如lv_obj_set_x、lv_obj_set_y、lv_obj_set_opa等等。你也可以自己写:
static void my_exec_cb(void *var, int32_t v) {
lv_obj_t *obj = (lv_obj_t *)var;
lv_obj_set_width(obj, v);
lv_obj_set_height(obj, v / 2);
}
这样就能实现宽度和高度按不同比例变化的动画。你想想看,是不是比分开写两个动画要灵活得多?
1.3 动画状态机
LVGL的动画状态机,其实没那么玄乎。每个lv_anim_t实例都有一个state字段,取值就三种:
| 状态 | 枚举值 | 含义 |
|---|---|---|
| 就绪 | LV_ANIM_STATE_INIT |
动画已初始化,等待启动 |
| 运行 | LV_ANIM_STATE_RUNNING |
动画正在执行中 |
| 结束 | LV_ANIM_STATE_END |
动画执行完毕(或被删除) |
状态流转很简单:INIT → RUNNING → END。调用lv_anim_start()时,状态从INIT切到RUNNING;动画自然结束或调用lv_anim_del()时,切到END。
避坑指南: 我曾经在动画回调里直接删除动画对象,结果状态机混乱,导致野指针崩溃。正确的做法是:在completed_cb里用lv_anim_del_all()统一清理,或者用lv_anim_get()检查动画是否还在运行。
下面这张图,把整个动画生命周期串起来了:
你看,状态机其实就三个状态、两条转换线。但实际项目中,动画的「生命周期管理」才是大头。我建议你养成一个习惯:每个动画都注册completed_cb,在里面做资源清理。这样即使动画被提前删除,也能保证回调链完整。
1.4 实战:写一个完整的淡入动画
光说不练假把式。咱们写一个完整的淡入动画,把刚才的知识点串起来:
static void fade_in_completed_cb(lv_anim_t *a) {
LV_LOG_USER("动画结束,对象已完全显示");
// 这里可以释放资源或触发下一个动画
}
void my_fade_in_animation(lv_obj_t *target) {
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, target);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_opa);
lv_anim_set_values(&anim, 0, 255);
lv_anim_set_time(&anim, 800);
lv_anim_set_delay(&anim, 100); // 延迟100ms启动
lv_anim_set_path_cb(&anim, lv_anim_path_ease_out);
lv_anim_set_repeat_count(&anim, 0); // 不重复
lv_anim_set_completed_cb(&anim, fade_in_completed_cb);
lv_anim_start(&anim);
}
小技巧: 路径函数lv_anim_path_ease_out会让动画「先快后慢」,视觉上更自然。我一般做界面入场动画都用这个,比线性路径舒服多了。
这段代码里,三要素全了:时间(800ms + 100ms延迟)、路径(ease_out)、回调(exec_cb + completed_cb)。状态机从INIT开始,调用lv_anim_start后进入RUNNING,800ms后自动切到END,触发fade_in_completed_cb。