【例說(shuō)Arm-2D界面設(shè)計(jì)】“手?jǐn)]GUI”的利器——場(chǎng)景播放器
掃描二維碼
隨時(shí)隨地手機(jī)看文章
【說(shuō)在前面的話】
在前面的文章《【喂到嘴邊了的模塊】準(zhǔn)備徒手?jǐn)]GUI?用Arm-2D三分鐘就夠了》中,我們介紹了如何借助 cmsis-pack 快速的在 MDK 中部署 arm-2d。
在過(guò)去的一段時(shí)間內(nèi),想必很多人都完成了部署,看到了下面的畫(huà)面吧?

為了避免讓大家產(chǎn)生疑惑,這里我們需要再次明確一下我們所要面對(duì)的開(kāi)發(fā)環(huán)境:
- 資源相對(duì)緊張的MCU,無(wú)法負(fù)擔(dān)起傳統(tǒng)的嵌入式GUI(比如以體積“小巧”著稱(chēng)的LVGL):
- Flash <= 64K,或者
- 應(yīng)用本身已經(jīng)占用了大量Flash空間,留給GUI的空間非常有限
- SRAM <= 16K
- 需要實(shí)現(xiàn)的GUI界面較為簡(jiǎn)單(這點(diǎn)在隨后會(huì)詳細(xì)介紹)
-
幀率要求較低(傳說(shuō)中的8幀不卡、9幀流暢、10幀電競(jìng))
【基于面板的界面設(shè)計(jì)】
從用戶的角度來(lái)說(shuō),如果一個(gè)嵌入式產(chǎn)品帶了彩屏,很自然的就會(huì)希望它能提供像智能手機(jī)(或平板設(shè)備)一樣的操作體驗(yàn)——但從開(kāi)發(fā)者的角度來(lái)說(shuō),用戶的這一期望往往會(huì)被錯(cuò)誤的理解為:用戶希望嵌入式產(chǎn)品上的圖形界面能像手機(jī)那樣支持“這樣或那樣”的滑動(dòng)、滾動(dòng)效果——如果能做到當(dāng)然最好,但其實(shí)這并不是這些“類(lèi)智能手機(jī)界面”設(shè)計(jì)的核心。
讓我把話挑明了吧——流暢的滑動(dòng)只是添料,甚至是可以完全丟棄的——真正核心的是一套與傳統(tǒng)Windows圖形界面設(shè)計(jì)完全不同的理念。關(guān)于這套設(shè)計(jì)理念,有一套叫做“人本界面”的設(shè)計(jì)方法論作為支撐,感興趣的小伙伴可以在豆瓣上搜索同名的圖書(shū)。
就本文要討論的內(nèi)容來(lái)說(shuō),我們可以簡(jiǎn)單的關(guān)注以下的一些要點(diǎn):
- 智能設(shè)備的界面強(qiáng)調(diào)“簡(jiǎn)潔”、并希望“讓用戶的注意力一次只集中在一件簡(jiǎn)單的事物上”。
- 與Windows不同,智能設(shè)備的界面很少(或者極力避免)窗口重疊
- 界面的基本單位不是“窗體(Window)”,而是以整個(gè)屏幕為基本單位的“面板(Panel)”
- 每個(gè)面板的內(nèi)容都盡可能簡(jiǎn)單、通過(guò)留白的方式強(qiáng)調(diào)那些需要用戶注意的內(nèi)容;
- 每個(gè)面板的功能都盡可能單一:
- 一般避免在同一個(gè)面板中擠進(jìn)多個(gè)不太相關(guān)的功能;
- 相關(guān)的內(nèi)容,如果能夠放得下,且美觀,則可以有主次的布置在同一個(gè)面板中以減少用戶切換面板帶來(lái)的不便;
- 如果相關(guān)的內(nèi)容如果無(wú)法在同一個(gè)面板中展示,則一定會(huì)添加快捷方式方便用戶快速進(jìn)行面板的切換;
- 面板間的切換方式以大家熟悉的PPT頁(yè)面切換方式類(lèi)似
- 對(duì)滑動(dòng)切換來(lái)說(shuō),要么不做,要做就要“絲滑”(差不多30FPS),否則會(huì)給用戶帶來(lái)“卡頓”的不適感
-
完全沒(méi)有動(dòng)畫(huà)的切換往往會(huì)給用戶“設(shè)備反應(yīng)迅速”的錯(cuò)覺(jué),對(duì)負(fù)擔(dān)不起高幀率的嵌入式設(shè)備來(lái)說(shuō),反而是最好的選擇
仔細(xì)回想一下,身邊的智能設(shè)備,是不是都基本滿足上述特點(diǎn)?——其實(shí)我們熟悉的手機(jī)和平板也是如此。
基于上述原則,我們甚至可以總結(jié)出一套簡(jiǎn)單有效的“嵌入式界面設(shè)計(jì)八股”:
- 用戶界面分成三個(gè)部分:狀態(tài)面板、導(dǎo)航面板和功能面板
- 狀態(tài)面板:又叫待機(jī)面板,用于顯示狀態(tài)信息(比如溫度、時(shí)間、產(chǎn)品Logo、產(chǎn)品當(dāng)前狀態(tài)等等)。
- 通常在待機(jī)界面上按下任意鍵(或者進(jìn)行任意觸摸)進(jìn)入導(dǎo)航面板
- 一般用戶超過(guò)一段時(shí)間沒(méi)有與界面進(jìn)行交互后會(huì)自動(dòng)進(jìn)入狀態(tài)面板,所以狀態(tài)面板有時(shí)候又叫待機(jī)面板
- 導(dǎo)航面板:也就是大家常說(shuō)的菜單。
- 一般導(dǎo)航面板以圖標(biāo)、列表或者按鈕的形式存在,
- 一般避免超出屏幕范圍的內(nèi)容,最好做到讓用戶對(duì)所有選項(xiàng)“盡收眼底”
- 導(dǎo)航面板可以通過(guò)子面板的形式實(shí)現(xiàn)多級(jí)菜單,從而簡(jiǎn)化開(kāi)發(fā)
- 功能面板:實(shí)現(xiàn)具體功能的面板,一般由導(dǎo)航面板進(jìn)入
- 每個(gè)面板的功能都盡可能單一,比如專(zhuān)門(mén)設(shè)置溫度、專(zhuān)門(mén)設(shè)置時(shí)間等等
-
相關(guān)的導(dǎo)航面板之間可以通過(guò)類(lèi)似左右箭頭(或者底部導(dǎo)航快捷按鈕)的機(jī)制進(jìn)行快捷切換
【什么是場(chǎng)景(scene)】
“場(chǎng)景(scene)”是 arm-2d為“手?jǐn)]GUI”的用戶引入的一個(gè)概念,通過(guò)配套的“場(chǎng)景播放器(scene player)”,極大的簡(jiǎn)化了基于面板的界面開(kāi)發(fā)。
一般來(lái)說(shuō),一個(gè)簡(jiǎn)單的面板用一個(gè)場(chǎng)景就可以搞定;而稍微復(fù)雜點(diǎn)的面板則可以通過(guò)多個(gè)場(chǎng)景(以及基于狀態(tài)機(jī)的場(chǎng)景切換)來(lái)搞定——總的原則就是,無(wú)論多復(fù)雜的面板,都可以拆分成一個(gè)個(gè)簡(jiǎn)單的場(chǎng)景來(lái)分而治之。
也許你已經(jīng)注意到了:原本面板本身就已經(jīng)很簡(jiǎn)單了,那么所謂“復(fù)雜的面板”根據(jù)狀態(tài)機(jī)拆分成多個(gè)場(chǎng)景后是不是更加簡(jiǎn)單了?——是的,每個(gè)場(chǎng)景的功能都是極其單一和簡(jiǎn)單的——極大的簡(jiǎn)化了每個(gè)場(chǎng)景的實(shí)現(xiàn)難度。
【場(chǎng)景(scene)的數(shù)據(jù)結(jié)構(gòu)和構(gòu)成】
場(chǎng)景在 arm-2d 中以類(lèi) arm_2d_scene_t 來(lái)描述:
/*! * \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 };};
其數(shù)據(jù)結(jié)構(gòu)并不復(fù)雜。
數(shù)據(jù)結(jié)構(gòu)的主體是這兩個(gè)指針:
-
fnScene:指向一個(gè)由用戶提供的繪圖函數(shù):
-
繪制一個(gè)場(chǎng)景中所有的內(nèi)容;或者
-
當(dāng)場(chǎng)景中存在“不會(huì)變化且不會(huì)被覆蓋的背景”和“少數(shù)”內(nèi)容會(huì)發(fā)生變化的前景時(shí),專(zhuān)門(mén)用于繪制前景——此時(shí)就需要通過(guò)ptDirtyRegion來(lái)指向描述前景變化區(qū)域的臟矩陣(Dirty Region List)。
-
fnBackground:指向一個(gè)由用戶提供的繪圖函數(shù),專(zhuān)門(mén)繪制一個(gè)場(chǎng)景中那些“只需要繪制一次”且“未來(lái)不會(huì)被前景覆蓋或者變化”的內(nèi)容,最典型的就是繪制場(chǎng)景中的背景圖片;
需要特別說(shuō)明的是:
-
fnBackground 只會(huì)在繪制每個(gè)場(chǎng)景的第一幀時(shí)調(diào)用;
-
隨后的每一幀就只會(huì)調(diào)用 fnScene;
-
fnBackground 會(huì)繪制整個(gè)屏幕;
-
臟矩陣(ptDirtyRegion)只對(duì) fnScene 有效;
-
當(dāng)ptDirtyRegion 為 NULL時(shí),fnScene也是繪制整個(gè)屏幕。
-
這意味著,當(dāng) ptDirtyRegion為NULL時(shí),fnBackground 繪制的內(nèi)容會(huì) 100% 被覆蓋掉——也就是說(shuō)完全沒(méi)用。這意味著:
-
當(dāng)且僅當(dāng)我們指定了有效的臟矩陣時(shí),fnBackground 才是實(shí)際有意義的。
如果你對(duì)“背景”和“前景”的分工感到似懂非懂,不妨看下面這個(gè)例子:
在這個(gè)場(chǎng)景中:
-
作為背景的狗頭實(shí)際上不會(huì)發(fā)生變化,因此我們只需在 fnBackground 所指向的繪圖函數(shù)中繪制即可;
-
動(dòng)態(tài)進(jìn)度條由于其內(nèi)容一直在變化,因此需要在 fnScene所指向的繪圖函數(shù)中“配合臟矩陣”進(jìn)行重復(fù)繪制。
可以看到,這里的事件處理順序并不復(fù)雜,大家可以根據(jù)實(shí)際的應(yīng)用需求各取所需。
【場(chǎng)景播放器(scene player)的本質(zhì)是什么】
場(chǎng)景播放器的本質(zhì)是一個(gè)針對(duì)場(chǎng)景(scene)的隊(duì)列(FIFO):

- 用戶可以預(yù)先生成多個(gè)場(chǎng)景,并通過(guò)函數(shù)arm_2d_scene_player_append_scenes壓入隊(duì)列中;
- 隊(duì)列的頭部就是當(dāng)前生效的場(chǎng)景;
- 用戶可以在任意時(shí)刻通過(guò)函數(shù)arm_2d_scene_player_switch_to_next_scene來(lái)安全的觸發(fā)場(chǎng)景切換,
- 所謂的場(chǎng)景切換就是丟棄隊(duì)列當(dāng)前的頭部場(chǎng)景——換成下一個(gè);
- 場(chǎng)景切換后,被丟棄的場(chǎng)景會(huì)調(diào)用 fnDepose ,用戶可以利用這個(gè)函數(shù)為對(duì)應(yīng)場(chǎng)景“擦屁股”
- 比如,假設(shè)一個(gè)場(chǎng)景(arm_2d_scene_t)對(duì)象本身就是動(dòng)態(tài)分配的(從 malloc中分配),那么就可以通過(guò) fnDepose 方法來(lái)將內(nèi)存釋放掉(比如調(diào)用 free函數(shù))。
- 場(chǎng)景播放器提供了 arm_2d_scene_player_flush_fifo 方法,它會(huì)清空整個(gè)隊(duì)列。
- 被清空出去的場(chǎng)景都會(huì)被依次調(diào)用 fnDepose,因此不用擔(dān)心內(nèi)存泄露的問(wèn)題。
- 場(chǎng)景切換是支持特效的,比如:淡入淡出、滑動(dòng)和擦除等等
【用場(chǎng)景開(kāi)發(fā)也太簡(jiǎn)單了8!】
前面洋洋灑灑的做了這么多理論鋪墊,也許會(huì)讓你對(duì) scene 的使用產(chǎn)生了“非常復(fù)雜”的錯(cuò)覺(jué)或者擔(dān)憂,但實(shí)際情況卻相反:借助cmsis-pack和RTE的幫助,創(chuàng)建 scene 幾乎只要點(diǎn)幾下鼠標(biāo)就可以搞定,而且立即就可以使用。
假設(shè)你已經(jīng)根據(jù)《【喂到嘴邊了的模塊】準(zhǔn)備徒手?jǐn)]GUI?用Arm-2D三分鐘就夠了》的描述,完成了 arm-2d 的部署,并且成功的加入了一個(gè) Display Adapter,此時(shí)我們應(yīng)該能看到這樣的效果:

此時(shí),打開(kāi) RTE,展開(kāi)Acceleration后在Arm-2D Helper中找到 Scene:
如果你的界面中找不到 Scene,說(shuō)明你的 arm-2d cmsis-pack 版本較老,可以關(guān)注公眾號(hào)【裸機(jī)思維】后,發(fā)送關(guān)鍵字 arm-2d 后獲取最新版本的網(wǎng)盤(pán)鏈接。
在Scene的右邊,我們可以通過(guò)“增加數(shù)值”的方式向工程中添加指定數(shù)量的場(chǎng)景。單擊確定后,對(duì)應(yīng)數(shù)量的場(chǎng)景模板會(huì)加入到工程管理器中:
這里的 arm_2d_scene_0.h 和 arm_2d_scene_0.c 分別對(duì)應(yīng)我們新加入的場(chǎng)景的頭文件和源代碼。
其實(shí),所謂的 Display Adapter 就是場(chǎng)景播放器(arm_2d_scene_player_t):
ARM_NOINITexternarm_2d_scene_player_t DISP0_ADAPTER;
在初始化完 Display Adapter 后,我們調(diào)用場(chǎng)景的初始化函數(shù)arm_2d_scene0_init()——將它們加入指定的場(chǎng)景播放器隊(duì)列中:
...int main (void) { arm_irq_safe { arm_2d_init(); } disp_adapter0_init(); arm_2d_scene0_init(&DISP0_ADAPTER); while(1) { disp_adapter0_task(); } }
調(diào)用函數(shù) arm_2d_scene_player_switch_to_next_scene() 來(lái)切換到我們新加入的場(chǎng)景中:
...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(); } }
為了方便觀察效果,不妨設(shè)置一個(gè)場(chǎng)景切換效果:
-
...int main (void) { arm_irq_safe { arm_2d_init(); } disp_adapter0_init(); /* 初始化場(chǎng)景 scene0,并將其加入到場(chǎng)景播放器 DISP0_ADAPTER 中 */ arm_2d_scene0_init(&DISP0_ADAPTER); /* 設(shè)置切換特效為 淡入淡出(白色) */ arm_2d_scene_player_set_switching_mode( &DISP0_ADAPTER, ARM_2D_SCENE_SWITCH_MODE_FADE_WHITE); /* 設(shè)置切換持續(xù)時(shí)間為 3000ms */ arm_2d_scene_player_set_switching_period( &DISP0_ADAPTER, 3000); /* 申請(qǐng)切換到新加入的場(chǎng)景中 */ arm_2d_scene_player_switch_to_next_scene(&DISP0_ADAPTER); while(1) { disp_adapter0_task(); } }
編譯后運(yùn)行,可以看到類(lèi)似如下的效果:
可以看到,場(chǎng)景播放器從默認(rèn)的“轉(zhuǎn)圈圈”界面以“漸明漸暗”的形式切換到了我們的新場(chǎng)景 scene0 中。
細(xì)心的小伙伴可能很快就注意到了一個(gè)奇怪的地方:為啥很快 scene0 又消失在白屏中了呢?要解答這一疑問(wèn)不妨打開(kāi)