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);

你想想看,这相当于你完全接管了「时间到值」的映射关系。默认的线性插值、缓动函数都不用了,你自己说了算。

我的经验:自定义get_value_cb最适合做「分段动画」。比如一个进度条,前80%快速填充,后20%慢速微调,给人一种「快到了,但最后一点要精确」的感觉。我在做仪表盘项目时就用过这个技巧,用户体验反馈非常好。

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);
注意:完成回调里不要做耗时操作。LVGL的动画系统是在主循环里跑的,如果你在回调里执行了延时或大量计算,整个UI都会卡住。我曾经犯过这个错——在完成回调里写了个while循环等待硬件响应,结果界面直接冻住了。后来改成用异步任务才解决。

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 里修改了对象的属性,这些修改会作为动画的初始状态。

核心要点:回调函数是动画的「生命线」。start_cb管出生,get_value_cb管成长,completed_cb管死亡。用好这三个钩子,你的动画就不再是简单的位移,而是有「灵魂」的交互体验。

我个人建议,每个动画至少绑定一个 completed_cb。哪怕里面只写一行日志,也能帮你快速定位问题。我在调试复杂动画时,经常靠完成回调里的日志来判断动画是否真的跑完了,还是被意外中断了。

好了,回调函数就讲到这里。下一章我们会聊聊「动画的暂停、恢复与删除」,这些操作在实际项目中非常常用,尤其是当用户快速点击时,如何优雅地处理动画队列。