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_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 动画组合的流程结构
下面这张图展示了组合动画的完整流程:
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就能活起来。