2. 动画核心结构体:lv_anim_t 结构体详解与参数配置
好,咱们直接进入正题。动画在嵌入式 UI 里,说白了就是「让控件动起来」。LVGL 里所有动画的起点,都藏在一个结构体里——lv_anim_t。我刚开始接触 LVGL 时,觉得动画不就是调个函数吗?后来踩了坑才发现,不理解这个结构体,你连动画怎么停、怎么变速都控制不了。
2.1 lv_anim_t 结构体长什么样?
先看代码,心里有个底。这个结构体定义在 lv_anim.h 里,我挑最核心的字段给你看:
typedef struct {
void * var; // 要动画的对象(比如一个按钮、一个标签)
lv_anim_exec_xcb_t exec_cb; // 执行回调,每帧调用,更新属性
lv_anim_start_cb_t start_cb;// 动画启动时的回调
lv_anim_ready_cb_t ready_cb;// 动画结束时的回调
lv_anim_path_cb_t path_cb; // 路径函数,控制动画速度曲线
int32_t start; // 起始值
int32_t end; // 结束值
uint32_t duration; // 动画时长,单位毫秒
int32_t act_time; // 当前已执行时间(内部用,别乱改)
uint16_t repeat_cnt; // 重复次数,0 表示无限
uint8_t playback : 1; // 是否反向播放
uint8_t start_cb_called : 1; // 内部标记,别碰
} lv_anim_t;
嗯,这里要注意:var 是个 void *,意味着你可以对任何对象做动画。我见过有人把 lv_obj_t * 传进去,也有人传自定义结构体。灵活,但别传野指针。
2.2 动画参数配置:时长、延迟、路径
配置动画,其实就是填这个结构体。LVGL 提供了一组 lv_anim_set_xxx 的宏或函数,我个人习惯用链式调用,代码更清爽。
2.2.1 时长(duration)与延迟(delay)
duration 决定动画跑多久,单位毫秒。比如 500 就是半秒。delay 是动画开始前的等待时间。我曾经在项目里做一个弹窗动画,忘了设 delay,结果多个动画同时触发,界面像抽风一样。后来我强制每个动画加 50ms 的错开延迟,效果立马顺滑了。
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, btn);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x);
lv_anim_set_values(&a, 0, 200);
lv_anim_set_time(&a, 800); // 800ms 跑完
lv_anim_set_delay(&a, 100); // 延迟 100ms 再开始
lv_anim_start(&a);
你想想看,如果延迟设成负数会怎样?LVGL 会把它当作「已经执行了这么久」,动画会从中间开始。这个技巧我用来做「续播」效果,挺有意思。
2.2.2 路径(path_cb)—— 动画的灵魂
路径函数决定了值的变化曲线。说白了,就是「从起点到终点,中间怎么走」。LVGL 内置了好几种路径:
| 路径函数 | 效果 | 适用场景 |
|---|---|---|
lv_anim_path_linear |
匀速直线 | 进度条、滚动条 |
lv_anim_path_ease_in |
先慢后快 | 物体掉落、弹出 |
lv_anim_path_ease_out |
先快后慢 | 按钮按下回弹、淡出 |
lv_anim_path_ease_in_out |
两头慢中间快 | 页面切换、平滑移动 |
lv_anim_path_bounce |
终点处弹跳几下 | 通知弹窗、小球落地 |
lv_anim_path_step |
瞬间跳变 | 开关切换、状态突变 |
我建议你动手试一下 lv_anim_path_bounce。记得有一次我做智能家居面板,温度调节滑块用 bounce 路径,用户拖到目标温度后滑块会轻轻弹一下,反馈感特别好。用户反馈说「这界面有灵魂」。
lv_anim_set_path_cb(&a, lv_anim_path_bounce);
就这么一行,效果天差地别。
2.2.3 重复与反向播放
有些动画需要一直循环,比如加载中的转圈。设置 repeat_cnt 为 0 就是无限循环。配合 playback 可以做到「正着跑完,再反着跑回来」。
lv_anim_set_repeat_count(&a, 3); // 重复 3 次
lv_anim_set_playback(&a, true); // 启用反向
lv_anim_set_playback_delay(&a, 50); // 反向前的延迟
这里有个坑:repeat_cnt 是「额外重复次数」。比如你设了 3,动画总共会跑 4 次(1 次原始 + 3 次重复)。我曾经在这里算错过,导致动画多跑了一轮,排查了半天才发现。
2.3 自定义路径函数
内置路径不够用?那就自己写。路径函数的原型很简单:
int32_t my_path(const lv_anim_t *a, int32_t start, int32_t end, int32_t current_time, uint32_t total_time);
你只需要根据当前时间,算出一个介于 start 和 end 之间的值。比如我想做一个「先冲过头再回弹」的效果:
int32_t my_overshoot_path(const lv_anim_t *a, int32_t start, int32_t end, int32_t current_time, uint32_t total_time) {
float progress = (float)current_time / total_time;
// 简单的 overshoot 公式
float overshoot = 1.0f + 0.3f * sinf(progress * 3.14159f * 3.0f) * (1.0f - progress);
return start + (int32_t)((end - start) * progress * overshoot);
}
嗯,数学不好的同学别慌。你只需要记住:输入是时间进度,输出是值进度。剩下的交给想象力。
2.4 动画的生命周期回调
有时候你想在动画开始或结束时做点事。比如动画结束了,解锁某个按钮。LVGL 提供了两个回调:
start_cb:动画启动时调用(包括重复启动)ready_cb:动画完成时调用(包括反向完成)
void on_anim_ready(lv_anim_t *a) {
lv_obj_t *obj = (lv_obj_t *)a->var;
lv_obj_set_style_bg_color(obj, lv_color_hex(0x00FF00), 0);
LV_LOG_USER("动画结束,变绿了!");
}
lv_anim_set_ready_cb(&a, on_anim_ready);
我曾经在 ready_cb 里忘记判断对象是否有效,结果动画结束后对象被删了,回调里访问野指针,直接 hardfault。所以建议你在回调里加个 lv_obj_is_valid() 检查。
2.5 避坑指南与个人经验
⚠️ 常见坑:动画未初始化就启动
我曾经在项目里直接声明 lv_anim_t a; 然后调用 lv_anim_start(&a),结果动画乱跳。原因是没有调用 lv_anim_init(&a),结构体里的字段全是随机值。记住:任何动画变量,必须先 init,再 set,最后 start。
💡 个人小技巧:动画池复用
如果你频繁创建和删除动画,建议用 lv_anim_t *a = lv_anim_create(NULL); 动态分配。这样 LVGL 内部会管理内存,你只需要在动画结束后调用 lv_anim_del(a, NULL); 即可。我习惯在页面切换时统一清理所有动画,避免内存泄漏。
🔑 核心总结
lv_anim_t是动画的「身份证」,所有参数都写在这里- 时长、延迟、路径是三大核心参数,路径决定「感觉」
- 自定义路径函数可以做出独一无二的动画效果
- 生命周期回调要小心野指针,记得做有效性检查
好了,这一章的内容就到这里。下一章我会带你手写一个完整的动画流程,从创建到销毁,把今天讲的结构体用起来。到时候你会发现,理解了 lv_anim_t,LVGL 动画基本就掌握了一半。