動畫可以說是 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()
重新播放,並且會從當前位置倒放。
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);
這樣便可以按開始位置對齊了:
還可以配合 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