3、属性动画实战:移动、旋转、缩放、透明度动画的配置与组合

属性动画,说白了就是让控件从A状态平滑过渡到B状态。移动、旋转、缩放、透明度——这四个基础操作,几乎能覆盖90%的UI动效需求。我刚开始用LVGL做动画时,总觉得要写很多代码,后来发现核心就几个API来回用。

这一章,咱们直接上手实战。我会把每个属性的配置拆开讲,最后再组合成一个完整的动画效果。

3.1 移动动画:让控件动起来

移动动画是最基础的。你想想看,一个按钮从左边滑到右边,或者一个弹窗从底部升上来,都是移动。

LVGL里移动动画的核心是 lv_anim_set_var 和 lv_anim_set_exec_cb。前者指定控件,后者指定回调函数。移动的回调函数用 lv_obj_set_x 或 lv_obj_set_y。

/* 创建一个按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 50);

/* 配置移动动画 */
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);    /* 从x=0移动到x=200 */
lv_anim_set_time(&a, 1000);        /* 持续1秒 */
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);  /* 缓出效果 */
lv_anim_start(&a);

这里有个细节:lv_anim_set_values 的起始值和结束值,单位是像素。我习惯把起始值设成控件当前的位置,这样动画不会跳变。

小技巧:如果你想让控件从屏幕外滑入,可以把起始值设成负的。比如 lv_anim_set_values(&a, -100, 0),控件就会从左边滑进来。

3.2 旋转动画:转起来才有科技感

旋转动画在仪表盘、加载图标里很常见。LVGL的旋转是通过 lv_anim_set_exec_cb 配合 lv_img_set_angle 实现的。

注意,角度单位是0.1度。也就是说,lv_img_set_angle(img, 3600) 才是转一圈。

/* 创建一个图片对象 */
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_img_set_src(img, &my_image);

/* 配置旋转动画 */
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, img);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_img_set_angle);
lv_anim_set_values(&a, 0, 3600);   /* 从0度转到360度 */
lv_anim_set_time(&a, 2000);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);  /* 无限循环 */
lv_anim_start(&a);

我曾经在做一个智能家居面板时,风扇图标需要一直旋转。一开始没设 repeat_count,转一圈就停了。后来加上 LV_ANIM_REPEAT_INFINITE,才搞定。

注意:旋转动画只对图片对象(lv_img)有效。如果你要对普通控件(比如按钮)做旋转,需要先设置 lv_obj_set_style_transform_angle,再用 lv_anim_set_exec_cb 配合 lv_obj_set_style_transform_angle。

3.3 缩放动画:放大缩小有讲究

缩放动画常用于按钮点击反馈、弹窗出现效果。LVGL里缩放是通过 lv_anim_set_exec_cb 配合 lv_obj_set_style_transform_scale 实现的。

缩放值的单位是256倍。也就是说,lv_obj_set_style_transform_scale(btn, 256) 是原始大小,512 是放大一倍,128 是缩小一半。

/* 创建一个按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());

/* 配置缩放动画 */
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_style_transform_scale);
lv_anim_set_values(&a, 256, 300);  /* 从原始大小放大到1.17倍 */
lv_anim_set_time(&a, 300);
lv_anim_set_playback_time(&a, 300);  /* 回放时间,实现弹跳效果 */
lv_anim_start(&a);

这里我用了 playback_time,意思是动画播放完后,再按原路返回。这样按钮就会先变大再恢复,看起来像弹了一下。

核心要点:缩放动画一定要配合 lv_obj_set_style_transform_pivot_x 和 lv_obj_set_style_transform_pivot_y 设置旋转中心点。默认中心点在左上角,缩放效果会很奇怪。我一般设成控件中心:lv_obj_set_style_transform_pivot_x(btn, 50, 0) 和 lv_obj_set_style_transform_pivot_y(btn, 25, 0)。

3.4 透明度动画:淡入淡出最常用

透明度动画,说白了就是让控件慢慢出现或消失。这个在页面切换、弹窗显示里用得最多。

LVGL里透明度动画用 lv_anim_set_exec_cb 配合 lv_obj_set_style_opa。透明度值范围是0到255,0完全透明,255完全不透明。

/* 创建一个标签 */
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello LVGL");

/* 配置透明度动画 */
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, label);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);
lv_anim_set_values(&a, 0, 255);    /* 从完全透明到完全不透明 */
lv_anim_set_time(&a, 500);
lv_anim_start(&a);

嗯,这里要注意:如果你想让控件消失后隐藏,可以在动画结束时调用 lv_obj_add_flag 加上 LV_OBJ_FLAG_HIDDEN。否则控件虽然透明了,但还是会占用触摸事件。

3.5 组合动画:让效果更丰富

单个动画太单调了。实际项目中,我们经常需要同时移动、旋转、缩放、改变透明度。比如一个弹窗出现时,从中间放大并淡入。

LVGL支持多个动画同时运行。你只需要为每个属性创建一个 lv_anim_t,然后分别启动。

/* 创建一个容器作为弹窗 */
lv_obj_t *popup = lv_obj_create(lv_scr_act());
lv_obj_set_size(popup, 200, 150);
lv_obj_center(popup);

/* 动画1:缩放(从0.5倍到1倍) */
lv_anim_t a_scale;
lv_anim_init(&a_scale);
lv_anim_set_var(&a_scale, popup);
lv_anim_set_exec_cb(&a_scale, (lv_anim_exec_xcb_t)lv_obj_set_style_transform_scale);
lv_anim_set_values(&a_scale, 128, 256);
lv_anim_set_time(&a_scale, 400);
lv_anim_set_path_cb(&a_scale, lv_anim_path_ease_out);

/* 动画2:透明度(从0到255) */
lv_anim_t a_opa;
lv_anim_init(&a_opa);
lv_anim_set_var(&a_opa, popup);
lv_anim_set_exec_cb(&a_opa, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);
lv_anim_set_values(&a_opa, 0, 255);
lv_anim_set_time(&a_opa, 400);
lv_anim_set_path_cb(&a_opa, lv_anim_path_ease_out);

/* 同时启动两个动画 */
lv_anim_start(&a_scale);
lv_anim_start(&a_opa);

你看,代码结构很清晰。每个动画独立配置,最后一起启动。LVGL内部会并行处理它们。

经验之谈:组合动画的时间最好保持一致,否则效果会不协调。我一般把所有动画的 time 设成相同值,然后通过 delay 来控制先后顺序。比如先透明度后缩放,可以给缩放动画加一个 lv_anim_set_delay(&a_scale, 100)。

3.6 动画组合的完整示例

下面是一个完整的示例,展示了一个按钮点击后,弹窗从屏幕中心放大并淡入,同时背景变暗。

/* 点击按钮的回调 */
static void btn_event_cb(lv_event_t *e)
{
    /* 创建遮罩层 */
    lv_obj_t *mask = lv_obj_create(lv_scr_act());
    lv_obj_set_size(mask, LV_HOR_RES, LV_VER_RES);
    lv_obj_set_style_bg_color(mask, lv_color_black(), 0);
    lv_obj_set_style_bg_opa(mask, 0, 0);  /* 初始透明 */

    /* 创建弹窗 */
    lv_obj_t *popup = lv_obj_create(lv_scr_act());
    lv_obj_set_size(popup, 250, 180);
    lv_obj_center(popup);
    lv_obj_set_style_transform_scale(popup, 128, 0);  /* 初始0.5倍 */
    lv_obj_set_style_opa(popup, 0, 0);  /* 初始透明 */

    /* 遮罩层透明度动画 */
    lv_anim_t a_mask;
    lv_anim_init(&a_mask);
    lv_anim_set_var(&a_mask, mask);
    lv_anim_set_exec_cb(&a_mask, (lv_anim_exec_xcb_t)lv_obj_set_style_bg_opa);
    lv_anim_set_values(&a_mask, 0, 128);  /* 半透明黑色 */
    lv_anim_set_time(&a_mask, 300);

    /* 弹窗缩放动画 */
    lv_anim_t a_scale;
    lv_anim_init(&a_scale);
    lv_anim_set_var(&a_scale, popup);
    lv_anim_set_exec_cb(&a_scale, (lv_anim_exec_xcb_t)lv_obj_set_style_transform_scale);
    lv_anim_set_values(&a_scale, 128, 256);
    lv_anim_set_time(&a_scale, 300);
    lv_anim_set_path_cb(&a_scale, lv_anim_path_ease_out);

    /* 弹窗透明度动画 */
    lv_anim_t a_opa;
    lv_anim_init(&a_opa);
    lv_anim_set_var(&a_opa, popup);
    lv_anim_set_exec_cb(&a_opa, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);
    lv_anim_set_values(&a_opa, 0, 255);
    lv_anim_set_time(&a_opa, 300);
    lv_anim_set_path_cb(&a_opa, lv_anim_path_ease_out);

    /* 启动所有动画 */
    lv_anim_start(&a_mask);
    lv_anim_start(&a_scale);
    lv_anim_start(&a_opa);
}

这个示例里,遮罩层从透明变成半透明,弹窗从0.5倍放大到1倍,同时透明度从0变成255。三个动画同时运行,效果非常流畅。

3.7 动画组合的流程结构

下面这张图展示了组合动画的完整流程:

组合动画执行流程 用户触发事件 移动动画 旋转动画 缩放动画 lv_anim_start() lv_anim_start() lv_anim_start() 动画同时完成 每个动画独立配置时间、路径、延迟,LVGL内部并行调度

3.8 常见问题与避坑

做属性动画时,有几个坑我踩过,分享给你:

  • 动画卡顿:如果动画一卡一卡的,先检查是不是在动画执行期间频繁调用了 lv_anim_start。我曾经在定时器里每10ms启动一次动画,结果界面直接卡死。解决办法是加一个标志位,动画运行时不再重复启动。
  • 动画跳变:动画开始或结束时,控件位置突然跳一下。这通常是因为起始值或结束值没设对。我习惯在动画启动前,先打印一下控件的当前值,确保和动画起始值一致。
  • 内存泄漏:每个 lv_anim_t 结构体在动画结束后会自动释放,但如果你在动画运行期间修改了 lv_anim_t 的指针,可能会导致内存泄漏。建议用局部变量,不要用全局的 lv_anim_t 指针。
  • 透明度动画后控件仍可点击:透明度为0的控件,虽然看不见,但触摸事件仍然有效。记得在动画结束时加上 lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN) 或 lv_obj_set_style_pointer_events(obj, LV_STYLE_PROP_INV, 0)。
重要提醒:不要在 lv_anim_set_exec_cb 的回调函数里做耗时操作。这个回调是在LVGL的主循环里调用的,如果卡住了,整个UI都会卡。我曾经在回调里做了图片解码,结果动画一卡一卡的。后来把解码放到任务里,回调只做位置更新。

3.9 性能优化建议

属性动画虽然好用,但也不能滥用。我总结了几条优化建议:

场景 建议 原因
同时运行多个动画 不超过10个 LVGL的动画调度器是单线程的,太多动画会占用CPU
动画对象数量 不超过50个 每个动画对象都会占用内存和调度资源
动画时间 100ms ~ 2000ms 太短看不出效果,太长用户会等得不耐烦
路径函数 优先用内置的 自定义路径函数如果计算复杂,会影响帧率

嗯,这些数字是我在实际项目中测试出来的。如果你的硬件性能好,可以适当放宽。但嵌入式设备资源有限,还是保守一点好。

属性动画的配置其实不难,难的是组合出流畅自然的效果。多试几次,找到合适的参数组合,你的UI就能活起来。