4、动画回调函数:设置回调函数、自定义动画逻辑、动画完成回调
动画回调函数,说白了就是给动画挂上一些「钩子」。
你让一个按钮从A点移到B点,这是基础操作。但如果你想在移动过程中改变它的颜色,或者在移动结束时弹个提示框呢?这时候就需要回调函数登场了。
我个人习惯把回调函数理解为动画的「监听器」。它不会干扰动画本身的运行,但能在关键时刻插一脚,执行你自定义的逻辑。
4.1 设置回调函数:给动画绑定事件
LVGL里,回调函数是通过 lv_anim_set_* 系列函数来绑定的。常用的有三个:
lv_anim_set_start_cb— 动画开始时触发lv_anim_set_get_value_cb— 每次更新值时触发(自定义动画逻辑的核心)lv_anim_set_completed_cb— 动画完成时触发
先看一个最简单的例子:
// 创建一个动画对象
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, 1000);
// 绑定开始回调
lv_anim_set_start_cb(&a, my_start_cb);
// 启动动画
lv_anim_start(&a);
对应的回调函数:
static void my_start_cb(lv_anim_t *a) {
lv_obj_t *obj = (lv_obj_t *)a->var;
lv_obj_set_style_bg_color(obj, lv_color_hex(0xFF0000), 0);
LV_LOG_USER("动画开始了,按钮变红");
}
嗯,这里要注意:回调函数的参数是 lv_anim_t * 指针。你可以通过 a->var 拿到正在动画的对象。我在项目中经常用这个特性,在动画开始时先重置一下对象的状态,避免出现「上一次动画没跑完,这次又接着跑」的混乱情况。
4.2 自定义动画逻辑:别只盯着exec_cb
大部分教程只教你用 lv_anim_set_exec_cb 来驱动属性变化。但如果你想要更复杂的动画逻辑,比如根据进度动态计算多个属性,那就得用 lv_anim_set_get_value_cb 了。
这个回调函数的作用是:每次动画更新时,由你决定当前进度对应的值是多少。
看代码:
static int32_t my_get_value_cb(lv_anim_t *a) {
// a->act_time 是当前已过去的时间(毫秒)
// a->time 是总时长
int32_t progress = (a->act_time * 100) / a->time;
// 自定义逻辑:前50%时间只走20%路程,后50%走80%
if (progress < 50) {
return a->start_value + (a->end_value - a->start_value) * progress / 100 * 0.2;
} else {
return a->start_value + (a->end_value - a->start_value) * (0.1 + (progress - 50) * 0.8 / 50);
}
}
// 使用时
lv_anim_set_get_value_cb(&a, my_get_value_cb);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x);
你想想看,这相当于你完全接管了「时间到值」的映射关系。默认的线性插值、缓动函数都不用了,你自己说了算。
4.3 动画完成回调:收尾工作别忘掉
动画跑完了,你以为就结束了?
很多时候,动画完成后需要做一些清理工作。比如:
- 释放临时分配的内存
- 改变按钮的可点击状态
- 触发下一个动画
- 记录日志或统计数据
这些都可以放在 lv_anim_set_completed_cb 里:
static void my_completed_cb(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_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE);
LV_LOG_USER("动画完成,按钮变绿,可点击");
// 如果需要,可以在这里启动下一个动画
// start_next_animation();
}
// 绑定
lv_anim_set_completed_cb(&a, my_completed_cb);
4.4 综合示例:一个带回调的完整动画
把上面几个知识点串起来,写一个完整的例子:
static void anim_start_cb(lv_anim_t *a) {
lv_obj_t *obj = (lv_obj_t *)a->var;
lv_obj_set_style_shadow_opa(obj, LV_OPA_50, 0);
LV_LOG_USER("动画开始,阴影出现");
}
static void anim_completed_cb(lv_anim_t *a) {
lv_obj_t *obj = (lv_obj_t *)a->var;
lv_obj_set_style_shadow_opa(obj, LV_OPA_0, 0);
LV_LOG_USER("动画完成,阴影消失");
}
void my_animation_example(void) {
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 50);
lv_obj_center(btn);
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_y);
lv_anim_set_values(&a, 0, 300);
lv_anim_set_time(&a, 2000);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
// 绑定回调
lv_anim_set_start_cb(&a, anim_start_cb);
lv_anim_set_completed_cb(&a, anim_completed_cb);
lv_anim_start(&a);
}
这个例子中,按钮从y=0移动到y=300。开始时阴影出现,结束时阴影消失。整个过程流畅自然,回调函数帮你做了「入场」和「退场」的装饰工作。
4.5 回调函数的执行顺序
搞清楚回调的执行顺序,能帮你避免很多奇怪的bug。我整理了一张表:
| 回调函数 | 触发时机 | 典型用途 |
|---|---|---|
start_cb |
动画开始前(第一帧之前) | 初始化状态、显示提示、记录开始时间 |
get_value_cb |
每一帧更新时 | 自定义值映射、分段动画、动态调整 |
completed_cb |
动画完全结束后 | 清理资源、触发后续动作、更新UI状态 |
注意:start_cb 是在动画开始前调用的,这时候第一帧还没执行。如果你在 start_cb 里修改了对象的属性,这些修改会作为动画的初始状态。
我个人建议,每个动画至少绑定一个 completed_cb。哪怕里面只写一行日志,也能帮你快速定位问题。我在调试复杂动画时,经常靠完成回调里的日志来判断动画是否真的跑完了,还是被意外中断了。
好了,回调函数就讲到这里。下一章我们会聊聊「动画的暂停、恢复与删除」,这些操作在实际项目中非常常用,尤其是当用户快速点击时,如何优雅地处理动画队列。