动画可以说是 LVGL 中的特色之一,不过在使用动画前,请确保单片机具有足够的性能来维持足够的帧率。
transition:过渡动画当一个控件的状态发生改变时,可以让样式也发生变化以提醒用户。通过过渡动画(transition)可以让样式的改变更自然。例如,按钮在点击时,以及开关在切换时,都具有一小段的过渡动画。
过渡动画使用 lv_style_transition_dsc_t
结构描述。为了要设置过渡动画,需要提供以下信息:
- 哪些属性需要过渡
- 过渡前的延时
- 过渡持续的时间
- 过渡动画(以回调函数的形式提供)
这些信息和结构成员是一一对应的。除了直接给结构成员赋值外,也可以使用以下初始化函数一次性设置:
void lv_style_transition_dsc_init(
lv_style_transition_dsc_t* tr,
const lv_style_prop_t props[],
lv_anim_path_cb_t path_cb,
uint32_t time,
uint32_t delay,
void* user_data);
第一个参数需要提供被初始化的过渡动画结构,第二个参数数组和字符串一样需要以 0
结尾。例如,假设需要实现这样一个过渡效果:点击时背景颜色发生改变并拉长,那么相应的初始化过程为:
static lv_style_transition_dsc_t trans;
static const lv_style_prop_t trans_props[] = {
LV_STYLE_WIDTH, LV_STYLE_HEIGHT, LV_STYLE_BG_COLOR, 0,
};
lv_style_transition_dsc_init(&trans, trans_props,
lv_anim_path_ease_in_out, 500, 0, NULL);
这里使用的过渡函数为 lv_anim_path_ease_in_out()
,这是一个内置的过渡效果,与之类似的过渡lv_anim_path_ease_out函数可以参考下表:
lv_anim_path_linear
等速过渡
lv_anim_path_ease_in
先慢后快的过渡
lv_anim_path_ease_out
先快后慢的过渡
lv_anim_path_ease_in_out
先慢、后快、结尾再变慢的过渡
lv_anim_path_overshoot
幅度会稍微过头一些再弹回的过渡
lv_anim_path_bounce
和上一个类似,不过会比较快地多弹几次
lv_anim_path_step
一步到位,和没动画的区别在于多了个延时
过渡动画是控件样式的一部分,可以将初始化得到的过渡动画描述应用到样式上:
static lv_style_t style_trans;
lv_style_init(&style_trans);
lv_style_set_transition(&style_trans, &trans);
过渡动画只有在两种样式切换时才会发生。例如,如果让以上样式应用在按下状态下:
lv_style_set_bg_color(&style_trans, lv_palette_main(LV_PALETTE_RED));
lv_style_set_width(&style_trans, 150);
lv_style_set_height(&style_trans, 60);
lv_obj_add_style(obj, &style_trans, LV_STATE_PRESSED);
那么只有在从其它状态变为按下时才会发生过渡:
注意松开时样式是突然转变的。如果要给这部分也添加一个过渡效果,可以给默认状态下的控件添加一个包含过渡的样式。
animate:通用动画过渡只有在状态改变时才会发生,而动画可以在任意时刻进行。除此之外,两者的区别还有:过渡只是样式的一部分,而动画和样式之间是独立的。
实际上,过渡的底层也使用的是动画。
创建动画为了创建动画,需要像样式一样声明一个动画类型并初始化:
lv_anim_t anim;
lv_anim_init(&anim);
由于动画是立即执行的,因此可以使用自动变量存储。然后,需要明确该动画将作用于哪一个控件:
lv_anim_set_var(&anim, obj);
接下来,可以设置动画的各种轨迹,包括:
- 动画需要改变什么属性
- 这些属性改变的范围
- 动画效果
- 延时和持续时间
动画的这些属性和过渡是类似的。例如,假设想做一个控件下落的动画,那么需要提供一个改变 y 坐标值的回调函数,这个函数可以直接使用 lv_obj_set_y()
,然后设定改变的始末值和运动轨迹,对应的代码为:
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_y);
lv_anim_set_values(&anim, -100, 100);
lv_anim_set_path_cb(&anim, lv_anim_path_bounce);
lv_anim_set_time(&anim, 1000);
lv_anim_set_delay(&anim, 1000);
然后,可以在必要的时候执行动画:
lv_anim_start(&anim);
效果为:
更复杂的动画关于延迟渲染
之前说过,样式是延迟渲染的,因此样式变量需要使用static
存储类型修饰符;而动画不是,动画从创建到执行是立即发生的。这也很好理解:样式在创建的过程中可能发生多次修改,因此需要确定最终的表现结果如何,再着手绘制,否则整个控件可能会重绘多次,占用大量无效的资源。
这种特点可能会带来许多意想不到的问题。例如,假设在lv_anim_set_values()
函数中去获取一个控件的位置、宽度等信息,由于它们都属于样式的一部分,此时还没有实际计算,因此得到的可能是默认值,造成动画始末效果偏离预期轨迹。
要解决这个问题,要么手动设置具体的值,要么让动画等到实际渲染发生了再执行,例如将其作为事件回调函数中的一部分。
以上创建的动画是单次不重复的,LVGL 提供了许多函数,可以为动画设置更复杂的属性。
这里介绍一个控件 bar ,它实质上就是没有 knob 部分的滑块,可以借用该控件来创建一个进度条(progress bar)动画。以下创建一个 bar 并将它的模式设定为 LV_BAR_MODE_RANGE
,这样就可以同时修改 indicator 两端的位置了:
lv_obj_t* bar = lv_bar_create(lv_scr_act());
lv_bar_set_mode(bar, LV_BAR_MODE_RANGE);
这里使用官方文档中提供的一个样式来使外观更好看,具体细节就无需解释了:
static lv_style_t style_bg;
static lv_style_t style_indic;
lv_style_init(&style_bg);
lv_style_set_border_color(&style_bg, lv_palette_main(LV_PALETTE_BLUE));
lv_style_set_border_width(&style_bg, 2);
lv_style_set_pad_all(&style_bg, 6);
lv_style_set_radius(&style_bg, 6);
lv_style_set_anim_time(&style_bg, 1000);
lv_style_init(&style_indic);
lv_style_set_bg_opa(&style_indic, LV_OPA_COVER);
lv_style_set_bg_color(&style_indic, lv_palette_main(LV_PALETTE_BLUE));
lv_style_set_radius(&style_indic, 3);
lv_obj_remove_style_all(bar);
lv_obj_add_style(bar, &style_bg, 0);
lv_obj_add_style(bar, &style_indic, LV_PART_INDICATOR);
lv_obj_set_size(bar, 200, 20);
然后就可以确定动画效果了。例如,这里期望的动画效果为:
那么首先可以编写一个改变属性的回调函数,例如改变 indicator 的范围:
static void anim_progress_load(void* obj, int32_t v) {
lv_bar_set_start_value(obj, v, LV_ANIM_ON);
lv_bar_set_value(obj, 20 + v, LV_ANIM_ON);
}
这些值在 0~80 范围内等速改变,持续时间 1.5 秒,无延时,对应的代码为:
lv_anim_set_exec_cb(&anim, anim_progress_load);
lv_anim_set_values(&anim, 0, 80);
lv_anim_set_path_cb(&anim, lv_anim_path_linear);
lv_anim_set_time(&anim, 1500);
lv_anim_set_delay(&anim, 0);
然后这里为其添加一个倒退和重复效果,这样动画就能来回播放了:
lv_anim_set_playback_time(&anim, 1500);
lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE);
实现的进度条动画就像以上 gif 展示的一样。除此之外,还可以修改更多动画的细节,例如:
lv_anim_set_start_cb(anim, start_cb)
在延时后、开始前执行一个函数
lv_anim_set_playback_delay(anim, delay)
设置动画倒退前的延时
lv_anim_set_repeat_delay(anim, delay)
设置动画重复前的延时
lv_anim_set_early_apply(&a, bool)
是否将起始值应用到动画开始前,使动画执行时不会太突兀
更多的细节可以参考官方文档。
组合动画效果有时候需要同时播放较多动画,此时如果逐个播放的话,需要逐个为动画设计延时,不方便安排。此时,可以使用 LVGL 提供的时间线(timeline)统一安排各个动画。
时间线的创建非常简单。首先,创建一系列动画,但先不调用 lv_anim_start()
让动画开始。
其次,创建一个时间线并将各个动画添加到时间线的某一时刻处:
lv_anim_timeline_t* anim_timeline = lv_anim_timeline_create();
lv_anim_timeline_add(anim_timeline, 0, &anim_axis);
lv_anim_timeline_add(anim_timeline, 100, &anim_obj_01);
lv_anim_timeline_add(anim_timeline, 1100, &anim_obj_02);
lv_anim_timeline_add(anim_timeline, 2100, &anim_obj_03);
lv_anim_timeline_add(anim_timeline, 300, &anim_label_01);
lv_anim_timeline_add(anim_timeline, 1300, &anim_label_02);
lv_anim_timeline_add(anim_timeline, 2300, &anim_label_03);
使用时间线时,无需为动画设计延时,只需要关注动画会在什么时刻播放,延时便会自动计算。
添加完毕后,再调用时间线的执行函数就可以了:
lv_anim_timeline_start(anim_timeline);
这样就可以创建很复杂的组合动画效果了:
使用时间线可以方便管理所有动画,可以将时间线上包含的所有动画停播、倒放、跳转等。以下列出了一些常用的时间线控制函数:
lv_anim_timeline_stop(timeline)
暂停播放当前的所有动画
lv_anim_timeline_set_reverse(timeline, bool)
设置接下来的播放方向
lv_anim_timeline_set_progress(timeline, progress)
跳转到播放进度
如果需要倒放,在设置了播放方向后还需要调用 lv_anim_timeline_start()
重新播放,并且会从当前位置倒放。
滚动也是常见的一种动画效果。如果一个容器的尺寸不足以容纳它包含的控件,那么它就可以通过滚动来展示包含控件的所有部分。
为了使一个控件是可滚动的,它需要拥有标志 LV_OBJ_FLAG_SCROLLABLE
。清除该标志可以隐藏子控件的溢出部分。
滚动是可以冒泡的,如果一个控件已经滚动到底,再次对其尝试滚动将使滚动事件传播到父容器上。可以通过清除 LV_OBJ_FLAG_SCROLL_CHAIN
标志位去除这个性质。
可以通过 lv_obj_set_scroll_dir()
限制滚动的方向。例如:
lv_obj_set_scroll_dir(obj, LV_DIR_RIGHT);
那么就只能向右滚动到底,不能向左折回。
还可以通过以下几个函数利用代码执行滚动:
lv_obj_scroll_to(obj, x, y, anim_en);
lv_obj_scroll_by(obj, x, y, anim_en);
lv_obj_scroll_to_view(child, anim_en);
注意前两个函数的区别:前者是滚动到相应的位置,多次调用只有第一次实际有效;后者是模拟滚动的操作,实际滚动方向是相反的,并且多次调用效果可以叠加。除此之外,后者甚至可以滚动到超出子控件的范围之外。最后一个函数自动滚动到合适的位置,确保子控件可视。
这几个函数都不受滚动方向的约束。它们都具有第三个参数,用于指定滚动时是否提供滚动动画。
滚动动画滚动是有动画的,默认情况下,滚动动画的特点表现在以下几点:
- 滚动是具有惯性的,意思是当输入设备停止交互时,控件还会继续向前滚动一小段距离。可以通过清除
LV_OBJ_FLAG_SCROLL_MOMENTUM
标志位取消这个特征 - 滚动是具有弹性的,当滚动到底时,继续尝试滚动会使控件超出一定范围,松开后回弹。可以通过清除
LV_OBJ_FLAG_SCROLL_ELASTIC
标志位取消这个特征 - 除此之外,以上介绍的两个代码实现滚动的函数,如果在第三个参数中应用滚动,那么会发生一小段 easy-out 的切换动画
还可以设置一种特殊的滚动效果 snap ,它使滚动时可以自动对齐。为了启用这种效果,需要添加 LV_OBJ_FLAG_SNAPPABLE
标志位,然后设置对齐的方式:
lv_obj_set_scroll_snap_x(cont, LV_SCROLL_SNAP_START);
这样便可以按开始位置对齐了:
还可以配合 LV_OBJ_FLAG_SCROLL_ONE
标志位一次只滚过最多一个控件的位置。
在滚动时,会触发 LV_EVENT_SCROLL
事件,可以通过在该事件回调函数中对包含的子控件做变换,实现更复杂的滚动效果。
例如,以下在事件回调函数内,根据每个子控件当前位置的纵坐标对横坐标做一些变换:
static scrool_widget_cb(lv_event_t* e) {
lv_obj_t* cont = lv_event_get_target(e);
uint32_t child_cnt = lv_obj_get_child_cnt(cont);
for (uint8_t i = 0; i < child_cnt; i++) {
lv_obj_t* child = lv_obj_get_child(cont, i);
lv_obj_set_style_translate_x(child, child->coords.y1 * 0.5 - 60, 0);
}
}
然后让每次滚动时都做以上变换:
lv_obj_add_event_cb(cont, scrool_widget_cb, LV_EVENT_SCROLL, NULL);
这样就能实现斜方向的滚动效果了:
这里由于仅在事件中才修改按钮的水平位置,因此一开始控件的摆放不是倾斜的。要解决这个问题,可以添加以下代码:
lv_obj_scroll_to_view(lv_obj_get_child(cont, 0), LV_ANIM_OFF);
lv_event_send(cont, LV_EVENT_SCROLL, NULL);
前者使各个控件的坐标被计算,后者手动触发事件回调函数,利用计算出的坐标执行位置变换。
LVGL 的官方文档还给出了一个示例,可以实现类似圆形的旋转滚动,效果非常不错,不过涉及的计算较多,感兴趣的可以自行阅读官方文档。
滚动条如果一个控件可以发生滚动,那么它就具有滚动条(scrollbar)。可以通过 lv_obj_set_scrollbar_mode()
函数修改滚动条的模式。例如,使用 LV_SCROLLBAR_MODE_OFF
模式可以使滚动条完全消失,就像上一张 gif 显示的那样。
滚动条是一个控件的 LV_PART_SCROLLBAR
部分,可以通过选择器给滚动条加上不同的样式。
首发于:http://frozencandles.fun/archives/425
参考资料/延伸阅读https://docs.lvgl.io/master/overview/animation.html
https://docs.lvgl.io/master/overview/scroll.html