【說在前面的話】
在前面的文章《【喂到嘴邊了的模塊】準備徒手擼GUI?用Arm-2D三分鐘就夠了》中,我們介紹了如何借助 cmsis-pack 快速的在 MDK 中部署 arm-2d。
在過去的一段時間內,想必很多人都完成了部署,看到了下面的畫面吧?
為了避免讓大家產生疑惑,這里我們需要再次明確一下我們所要面對的開發(fā)環(huán)境:
- 資源相對緊張的MCU,無法負擔起傳統(tǒng)的嵌入式GUI(比如以體積“小巧”著稱的LVGL):
- Flash <= 64K,或者
- 應用本身已經占用了大量Flash空間,留給GUI的空間非常有限
- SRAM <= 16K
- 需要實現的GUI界面較為簡單(這點在隨后會詳細介紹)
-
幀率要求較低(傳說中的8幀不卡、9幀流暢、10幀電競)
【基于面板的界面設計】
從用戶的角度來說,如果一個嵌入式產品帶了彩屏,很自然的就會希望它能提供像智能手機(或平板設備)一樣的操作體驗——但從開發(fā)者的角度來說,用戶的這一期望往往會被錯誤的理解為:用戶希望嵌入式產品上的圖形界面能像手機那樣支持“這樣或那樣”的滑動、滾動效果——如果能做到當然最好,但其實這并不是這些“類智能手機界面”設計的核心。
讓我把話挑明了吧——流暢的滑動只是添料,甚至是可以完全丟棄的——真正核心的是一套與傳統(tǒng)Windows圖形界面設計完全不同的理念。關于這套設計理念,有一套叫做“人本界面”的設計方法論作為支撐,感興趣的小伙伴可以在豆瓣上搜索同名的圖書。
就本文要討論的內容來說,我們可以簡單的關注以下的一些要點:
- 智能設備的界面強調“簡潔”、并希望“讓用戶的注意力一次只集中在一件簡單的事物上”。
- 與Windows不同,智能設備的界面很少(或者極力避免)窗口重疊
- 界面的基本單位不是“窗體(Window)”,而是以整個屏幕為基本單位的“面板(Panel)”
- 每個面板的內容都盡可能簡單、通過留白的方式強調那些需要用戶注意的內容;
- 每個面板的功能都盡可能單一:
- 一般避免在同一個面板中擠進多個不太相關的功能;
- 相關的內容,如果能夠放得下,且美觀,則可以有主次的布置在同一個面板中以減少用戶切換面板帶來的不便;
- 如果相關的內容如果無法在同一個面板中展示,則一定會添加快捷方式方便用戶快速進行面板的切換;
- 面板間的切換方式以大家熟悉的PPT頁面切換方式類似
- 對滑動切換來說,要么不做,要做就要“絲滑”(差不多30FPS),否則會給用戶帶來“卡頓”的不適感
-
完全沒有動畫的切換往往會給用戶“設備反應迅速”的錯覺,對負擔不起高幀率的嵌入式設備來說,反而是最好的選擇
仔細回想一下,身邊的智能設備,是不是都基本滿足上述特點?——其實我們熟悉的手機和平板也是如此。
基于上述原則,我們甚至可以總結出一套簡單有效的“嵌入式界面設計八股”:
- 用戶界面分成三個部分:狀態(tài)面板、導航面板和功能面板
- 狀態(tài)面板:又叫待機面板,用于顯示狀態(tài)信息(比如溫度、時間、產品Logo、產品當前狀態(tài)等等)。
- 通常在待機界面上按下任意鍵(或者進行任意觸摸)進入導航面板
- 一般用戶超過一段時間沒有與界面進行交互后會自動進入狀態(tài)面板,所以狀態(tài)面板有時候又叫待機面板
- 導航面板:也就是大家常說的菜單。
- 一般導航面板以圖標、列表或者按鈕的形式存在,
- 一般避免超出屏幕范圍的內容,最好做到讓用戶對所有選項“盡收眼底”
- 導航面板可以通過子面板的形式實現多級菜單,從而簡化開發(fā)
- 功能面板:實現具體功能的面板,一般由導航面板進入
- 每個面板的功能都盡可能單一,比如專門設置溫度、專門設置時間等等
-
相關的導航面板之間可以通過類似左右箭頭(或者底部導航快捷按鈕)的機制進行快捷切換
【什么是場景(scene)】
“場景(scene)”是 arm-2d為“手擼GUI”的用戶引入的一個概念,通過配套的“場景播放器(scene player)”,極大的簡化了基于面板的界面開發(fā)。
一般來說,一個簡單的面板用一個場景就可以搞定;而稍微復雜點的面板則可以通過多個場景(以及基于狀態(tài)機的場景切換)來搞定——總的原則就是,無論多復雜的面板,都可以拆分成一個個簡單的場景來分而治之。
也許你已經注意到了:原本面板本身就已經很簡單了,那么所謂“復雜的面板”根據狀態(tài)機拆分成多個場景后是不是更加簡單了?——是的,每個場景的功能都是極其單一和簡單的——極大的簡化了每個場景的實現難度。
【場景(scene)的數據結構和構成】
場景在 arm-2d 中以類 arm_2d_scene_t 來描述:
/*! * \brief a class for describing scenes which are the combination of a * background and a foreground with a dirty-region-list support * */typedef struct arm_2d_scene_t arm_2d_scene_t;struct arm_2d_scene_t { arm_2d_scene_t *ptNext; //!< next scene arm_2d_scene_player_t *ptPlayer; //!< points to the host scene player arm_2d_region_list_item_t *ptDirtyRegion; //!< dirty region list for the foreground arm_2d_helper_draw_handler_t *fnBackground; //!< the function pointer for the background arm_2d_helper_draw_handler_t *fnScene; //!< the function pointer for the foreground void (*fnOnBGStart)(arm_2d_scene_t *ptThis); //!< on-start-drawing-background event handler void (*fnOnBGComplete)(arm_2d_scene_t *ptThis); //!< on-complete-drawing-background event handler void (*fnOnFrameStart)(arm_2d_scene_t *ptThis); //!< on-frame-start event handler void (*fnOnFrameCPL)(arm_2d_scene_t *ptThis); //!< on-frame-complete event handler /*! * \note We use fnDepose to free the resources */ void (*fnDepose)(arm_2d_scene_t *ptThis); //!< on-scene-depose event handler struct { uint8_t bOnSwitchingIgnoreBG : 1; //!< ignore background during switching period uint8_t bOnSwitchingIgnoreScene : 1; //!< ignore forground during switching period };};
其數據結構并不復雜。
數據結構的主體是這兩個指針:
-
fnScene:指向一個由用戶提供的繪圖函數:
-
繪制一個場景中所有的內容;或者
-
當場景中存在“不會變化且不會被覆蓋的背景”和“少數”內容會發(fā)生變化的前景時,專門用于繪制前景——此時就需要通過ptDirtyRegion來指向描述前景變化區(qū)域的臟矩陣(Dirty Region List)。
-
fnBackground:指向一個由用戶提供的繪圖函數,專門繪制一個場景中那些“只需要繪制一次”且“未來不會被前景覆蓋或者變化”的內容,最典型的就是繪制場景中的背景圖片;
需要特別說明的是:
-
fnBackground 只會在繪制每個場景的第一幀時調用;
-
隨后的每一幀就只會調用 fnScene;
-
fnBackground 會繪制整個屏幕;
-
臟矩陣(ptDirtyRegion)只對 fnScene 有效;
-
當ptDirtyRegion 為 NULL時,fnScene也是繪制整個屏幕。
-
這意味著,當 ptDirtyRegion為NULL時,fnBackground 繪制的內容會 100% 被覆蓋掉——也就是說完全沒用。這意味著:
-
當且僅當我們指定了有效的臟矩陣時,fnBackground 才是實際有意義的。
如果你對“背景”和“前景”的分工感到似懂非懂,不妨看下面這個例子:
在這個場景中:
-
作為背景的狗頭實際上不會發(fā)生變化,因此我們只需在 fnBackground 所指向的繪圖函數中繪制即可;
-
動態(tài)進度條由于其內容一直在變化,因此需要在 fnScene所指向的繪圖函數中“配合臟矩陣”進行重復繪制。
可以看到,這里的事件處理順序并不復雜,大家可以根據實際的應用需求各取所需。
【場景播放器(scene player)的本質是什么】
場景播放器的本質是一個針對場景(scene)的隊列(FIFO):
- 用戶可以預先生成多個場景,并通過函數arm_2d_scene_player_append_scenes壓入隊列中;
- 隊列的頭部就是當前生效的場景;
- 用戶可以在任意時刻通過函數arm_2d_scene_player_switch_to_next_scene來安全的觸發(fā)場景切換,
- 所謂的場景切換就是丟棄隊列當前的頭部場景——換成下一個;
- 場景切換后,被丟棄的場景會調用 fnDepose ,用戶可以利用這個函數為對應場景“擦屁股”
- 比如,假設一個場景(arm_2d_scene_t)對象本身就是動態(tài)分配的(從 malloc中分配),那么就可以通過 fnDepose 方法來將內存釋放掉(比如調用 free函數)。
- 場景播放器提供了 arm_2d_scene_player_flush_fifo 方法,它會清空整個隊列。
- 被清空出去的場景都會被依次調用 fnDepose,因此不用擔心內存泄露的問題。
- 場景切換是支持特效的,比如:淡入淡出、滑動和擦除等等
【用場景開發(fā)也太簡單了8!】
前面洋洋灑灑的做了這么多理論鋪墊,也許會讓你對 scene 的使用產生了“非常復雜”的錯覺或者擔憂,但實際情況卻相反:借助cmsis-pack和RTE的幫助,創(chuàng)建 scene 幾乎只要點幾下鼠標就可以搞定,而且立即就可以使用。
假設你已經根據《【喂到嘴邊了的模塊】準備徒手擼GUI?用Arm-2D三分鐘就夠了》的描述,完成了 arm-2d 的部署,并且成功的加入了一個 Display Adapter,此時我們應該能看到這樣的效果:
此時,打開 RTE,展開Acceleration后在Arm-2D Helper中找到 Scene:
如果你的界面中找不到 Scene,說明你的 arm-2d cmsis-pack 版本較老,可以關注公眾號【裸機思維】后,發(fā)送關鍵字 arm-2d 后獲取最新版本的網盤鏈接。
在Scene的右邊,我們可以通過“增加數值”的方式向工程中添加指定數量的場景。單擊確定后,對應數量的場景模板會加入到工程管理器中:
這里的 arm_2d_scene_0.h 和 arm_2d_scene_0.c 分別對應我們新加入的場景的頭文件和源代碼。
其實,所謂的 Display Adapter 就是場景播放器(arm_2d_scene_player_t):
ARM_NOINITexternarm_2d_scene_player_t DISP0_ADAPTER;
在初始化完 Display Adapter 后,我們調用場景的初始化函數arm_2d_scene0_init()——將它們加入指定的場景播放器隊列中:
...int main (void) { arm_irq_safe { arm_2d_init(); } disp_adapter0_init(); arm_2d_scene0_init(&DISP0_ADAPTER); while(1) { disp_adapter0_task(); } }
調用函數 arm_2d_scene_player_switch_to_next_scene() 來切換到我們新加入的場景中:
...int main (void) { arm_irq_safe { arm_2d_init(); } disp_adapter0_init(); arm_2d_scene0_init(&DISP0_ADAPTER); arm_2d_scene_player_switch_to_next_scene(&DISP0_ADAPTER); while(1) { disp_adapter0_task(); } }
為了方便觀察效果,不妨設置一個場景切換效果:
-
...int main (void) { arm_irq_safe { arm_2d_init(); } disp_adapter0_init(); /* 初始化場景 scene0,并將其加入到場景播放器 DISP0_ADAPTER 中 */ arm_2d_scene0_init(&DISP0_ADAPTER); /* 設置切換特效為 淡入淡出(白色) */ arm_2d_scene_player_set_switching_mode( &DISP0_ADAPTER, ARM_2D_SCENE_SWITCH_MODE_FADE_WHITE); /* 設置切換持續(xù)時間為 3000ms */ arm_2d_scene_player_set_switching_period( &DISP0_ADAPTER, 3000); /* 申請切換到新加入的場景中 */ arm_2d_scene_player_switch_to_next_scene(&DISP0_ADAPTER); while(1) { disp_adapter0_task(); } }
編譯后運行,可以看到類似如下的效果:
可以看到,場景播放器從默認的“轉圈圈”界面以“漸明漸暗”的形式切換到了我們的新場景 scene0 中。
細心的小伙伴可能很快就注意到了一個奇怪的地方:為啥很快 scene0 又消失在白屏中了呢?要解答這一疑問不妨打開