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()检查动画是否还在运行。

下面这张图,把整个动画生命周期串起来了:

LVGL动画状态机与生命周期 lv_anim_init() INIT(就绪) RUNNING(运行) END(结束) 设置三要素 lv_anim_start() 动画完成 / lv_anim_del() 关键回调时机 • start_cb:INIT→RUNNING时触发 • completed_cb:RUNNING→END时触发 • exec_cb:每帧调用,更新属性值 重复动画 • repeat_cnt > 0:自动回到RUNNING • repeat_delay:重复间隔

你看,状态机其实就三个状态、两条转换线。但实际项目中,动画的「生命周期管理」才是大头。我建议你养成一个习惯:每个动画都注册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。