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 动画基本就掌握了一半。