LVGL库入门教程06-动画

动画可以说是 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);

那么只有在从其它状态变为按下时才会发生过渡:

image

注意松开时样式是突然转变的。如果要给这部分也添加一个过渡效果,可以给默认状态下的控件添加一个包含过渡的样式。

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

效果为:

image

关于延迟渲染 之前说过,样式是延迟渲染的,因此样式变量需要使用 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);

然后就可以确定动画效果了。例如,这里期望的动画效果为:

image

那么首先可以编写一个改变属性的回调函数,例如改变 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);

这样就可以创建很复杂的组合动画效果了:

image

使用时间线可以方便管理所有动画,可以将时间线上包含的所有动画停播、倒放、跳转等。以下列出了一些常用的时间线控制函数:

函数 用途
lv_anim_timeline_stop(timeline) 暂停播放当前的所有动画
lv_anim_timeline_set_reverse(timeline, bool) 设置接下来的播放方向
lv_anim_timeline_set_progress(timeline, progress) 跳转到播放进度

如果需要倒放,在设置了播放方向后还需要调用 lv_anim_timeline_start() 重新播放,并且会从当前位置倒放。

scroll:滚动动画

滚动的特点

滚动也是常见的一种动画效果。如果一个容器的尺寸不足以容纳它包含的控件,那么它就可以通过滚动来展示包含控件的所有部分。

为了使一个控件是可滚动的,它需要拥有标志 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);

这样便可以按开始位置对齐了:

image

还可以配合 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);

这样就能实现斜方向的滚动效果了:

image

这里由于仅在事件中才修改按钮的水平位置,因此一开始控件的摆放不是倾斜的。要解决这个问题,可以添加以下代码:

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 部分,可以通过选择器给滚动条加上不同的样式。

参考资料/延伸阅读

https://docs.lvgl.io/master/overview/animation.html

https://docs.lvgl.io/master/overview/scroll.html

京ICP备2021034974号
contact me by hello@frozencandles.fun