LVGL庫入門教程 - 動畫

冰封殘燭 發表於 2022-06-29

動畫可以說是 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);

那麼只有在從其它狀態變為按下時才會發生過渡:

LVGL庫入門教程 - 動畫

注意鬆開時樣式是突然轉變的。如果要給這部分也新增一個過渡效果,可以給預設狀態下的控制元件新增一個包含過渡的樣式。

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

效果為:

LVGL庫入門教程 - 動畫

關於延遲渲染
之前說過,樣式是延遲渲染的,因此樣式變數需要使用 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);

然後就可以確定動畫效果了。例如,這裡期望的動畫效果為:

LVGL庫入門教程 - 動畫

那麼首先可以編寫一個改變屬性的回撥函式,例如改變 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);

這樣就可以建立很複雜的組合動畫效果了:

LVGL庫入門教程 - 動畫

使用時間線可以方便管理所有動畫,可以將時間線上包含的所有動畫停播、倒放、跳轉等。以下列出了一些常用的時間線控制函式:

函式 用途
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);

這樣便可以按開始位置對齊了:

LVGL庫入門教程 - 動畫

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

這樣就能實現斜方向的滾動效果了:

LVGL庫入門教程 - 動畫

這裡由於僅在事件中才修改按鈕的水平位置,因此一開始控制元件的擺放不是傾斜的。要解決這個問題,可以新增以下程式碼:

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