【说在前面的话】
在前面的文章《【玩转Arm-2D】入门和移植从未如此简单》中,我们介绍了如何借助 cmsis-pack 快速的在 MDK 中部署 arm-2d。
在过去的一段时间内,想必很多人都完成了部署,看到了下面的画面吧?
如果还没有,推荐先跟着上一篇文章的手把手图文教程——完成基本的部署吧。本文将在此基础上继续为您介绍如何使用arm-2d来简化我们手撸GUI的过程。
为了避免让大家产生疑惑,这里我们需要再次明确一下我们所要面对的开发环境:
- 资源相对紧张的MCU,无法负担起传统的嵌入式GUI(比如以体积“小巧”著称的LVGL):
- Flash <= 64K,或者
- 应用本身已经占用了大量Flash空间,留给GUI的空间非常有限
- SRAM <= 16K
- 需要实现的GUI界面较为简单(这点在随后会详细介绍)
- 帧率要求较低(传说中的8帧不卡、9帧流畅、10帧电竞)
【基于面板的界面设计】
从用户的角度来说,如果一个嵌入式产品带了彩屏,很自然的就会希望它能提供像智能手机(或平板设备)一样的操作体验——但从开发者的角度来说,用户的这一期望往往会被错误的理解为:用户希望嵌入式产品上的图形界面能像手机那样支持“这样或那样”的滑动、滚动效果——如果能做到当然最好,但其实这并不是这些“类智能手机界面”设计的核心。
让我把话挑明了吧——流畅的滑动只是添料,甚至是可以完全丢弃的——真正核心的是一套与传统Windows图形界面设计完全不同的理念。关于这套设计理念,有一套叫做“人本界面”的设计方法论作为支撑,感兴趣的小伙伴可以在豆瓣上搜索同名的图书。
就本文要讨论的内容来说,我们可以简单的关注以下的一些要点:
- 智能设备的界面强调“简洁”、并希望“让用户的注意力一次只集中在一件简单的事物上”。
基于这一原则,又派生出了如下的特点:
- 与Windows不同,智能设备的界面很少(或者极力避免)窗口重叠
- 界面的基本单位不是“窗体(Window)”,而是以整个屏幕为基本单位的“面板(Panel)”
- 每个面板的内容都尽可能简单、通过留白的方式强调那些需要用户注意的内容;
- 每个面板的功能都尽可能单一:
- 一般避免在同一个面板中挤进多个不太相关的功能;
- 相关的内容,如果能够放得下,且美观,则可以有主次的布置在同一个面板中以减少用户切换面板带来的不便;
- 如果相关的内容如果无法在同一个面板中展示,则一定会添加快捷方式方便用户快速进行面板的切换;
- 面板间的切换方式以大家熟悉的PPT页面切换方式类似
- 对滑动切换来说,要么不做,要做就要“丝滑”(差不多30FPS),否则会给用户带来“卡顿”的不适感
- 完全没有动画的切换往往会给用户“设备反应迅速”的错觉,对负担不起高帧率的嵌入式设备来说,反而是最好的选择
仔细回想一下,身边的智能设备,是不是都基本满足上述特点?——其实我们熟悉的手机和平板也是如此。
基于上述原则,我们甚至可以总结出一套简单有效的“嵌入式界面设计八股”:
- 用户界面分成三个部分:状态面板、导航面板和功能面板
- 状态面板:又叫待机面板,用于显示状态信息(比如温度、时间、产品Logo、产品当前状态等等)。
- 通常在待机界面上按下任意键(或者进行任意触摸)进入导航面板
- 一般用户超过一段时间没有与界面进行交互后会自动进入状态面板,所以状态面板有时候又叫待机面板
- 导航面板:也就是大家常说的菜单。
- 一般导航面板以图标、列表或者按钮的形式存在,
- 一般避免超出屏幕范围的内容,最好做到让用户对所有选项“尽收眼底”
- 导航面板可以通过子面板的形式实现多级菜单,从而简化开发
- 功能面板:实现具体功能的面板,一般由导航面板进入
- 每个面板的功能都尽可能单一,比如专门设置温度、专门设置时间等等
- 相关的导航面板之间可以通过类似左右箭头(或者底部导航快捷按钮)的机制进行快捷切换
【什么是场景(scene)】
“场景(scene)”是 arm-2d为“手撸GUI”的用户引入的一个概念,通过配套的“场景播放器(scene player)”,极大的简化了基于面板的界面开发。
一般来说,一个简单的面板用一个场景就可以搞定;而稍微复杂点的面板则可以通过多个场景(以及基于状态机的场景切换)来搞定——总的原则就是,无论多复杂的面板,都可以拆分成一个个简单的场景来分而治之。
也许你已经注意到了:原本面板本身就已经很简单了,那么所谓“复杂的面板”根据状态机拆分成多个场景后是不是更加简单了?——是的,每个场景的功能都是极其单一和简单的——极大的简化了每个场景的实现难度。
举个例子:有个面板的功能是设置温度,当超过某一特定值后,需要弹出一个窗口提醒用户当前设置值有某些注意事项。这样的面板在设计时就可以拆分成两个场景:1)一个正常的数值设置场景,实现一个类似滑条的功能让用户设置温度;2)一个专门的场景来提示用户注意事项——通过这样的安排,每个场景都可以非常单一。
再比如:某个面板的用来设置多个相关的选项,并且当用户开启某个开关后,会出现一些隐藏选项(或者原本不可设置的选项变成可选)。此时,就可以根据这个开关的状态,引入两个场景:一个对应开关关闭时的面板,一个对应开关开启时的面板——总之,面板拆的越细致,每个场景的设计就越简单。
【场景(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 can use this event to initialize/generate the new(next) scene
*/
void (*fnBeforeSwitchOut)(arm_2d_scene_t *ptThis); //!< before-scene-switch-out 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:指向一个由用户提供的绘图函数:
- 绘制一个场景中所有的内容;或者
- 当场景中存在“不会变化且不会被覆盖的背景”和“少数”内容会发生变化的前景时,专门用于绘制前景——此时就需要通过ptDirtyRegion来指向描述前景变化区域的脏矩阵(Dirty Region List)。
fnBackground:不要使用(Deprecated)
- ptDirtyRegion:指向一个可选的脏矩阵列表,通过这个脏矩阵列表,我们可以告诉场景播放器“只刷新某些我们指定的区域”
需要特别说明的是:
- 脏矩阵(ptDirtyRegion)只对 fnScene 有效;
- 当ptDirtyRegion 为 NULL时,fnScene也是绘制整个屏幕。
- 当ptDirtyRegion不为NULL时,第一次刷新场景(fnScene)时场景播放器会无视脏矩阵(也就是绘制整个屏幕),而在随后的场景刷新时,使用脏矩阵进行局部刷新。
- 当我们需要更新背景时,可以调用函数arm\_2d\_scene\_player\_update\_scene\_background() 来申请一次全屏幕刷新。
如果你对“背景”和“前景”的分工感到似懂非懂,不妨看下面这个例子:
在这个场景中:
- 作为背景的狗头实际上不会发生变化,因此只需要绘制一次即可
- 动态进度条由于其内容一直在变化,因此需要在 fnScene所指向的绘图函数中“配合脏矩阵”进行重复绘制。
为了方便应用开发,arm\_2d\_scene\_t 提供了一系列事件处理程序接口(回调函数),它们与背景、场景的绘制关系如下:
可以看到,这里的事件处理顺序并不复杂,大家可以根据实际的应用需求各取所需。
【场景播放器(scene player)的本质是什么】
场景播放器的本质是一个针对场景(scene)的队列(FIFO):
- 用户可以预先生成多个场景,并通过函数arm\_2d\_scene\_player\_append\_scenes压入队列中;
- 队列的头部就是当前生效的场景;
- 用户可以在任意时刻通过函数arm\_2d\_scene\_player\_switch\_to\_next\_scene来安全的触发场景切换,
- 所谓的场景切换就是丢弃队列当前的头部场景——换成下一个;
- 当用户申请了场景切换后,在即将开始进行场景切换前,即将被丢弃的场景会调用fnBeforeSwitchingOut,用户甚至可以在这里决定下一个场景是谁(创建新场景、通过构造函数完成初始化并加入队列)。
- 除了每个场景有一个fnBeforeSwitchingOut事件处理以外,其实场景播放器自己也有一个公共的fnBeforeSwitchingOut,用户完全可以利用它来实现场景的调度,而不是由具体某个场景决定自己的后续。
- 场景切换后,被丢弃的场景会调用 fnDepose ,用户可以利用这个函数为对应场景“擦屁股”
- 比如,假设一个场景(arm\_2d\_scene\_t)对象本身就是动态分配的(从 malloc中分配),那么就可以通过 fnDepose 方法来将内存释放掉(比如调用 free函数)。
- 场景播放器提供了 arm\_2d\_scene\_player\_flush\_fifo 方法,它会清空整个队列。
- 被清空出去的场景都会被依次调用 fnDepose,因此不用担心内存泄露的问题。
- 场景切换是支持特效的,比如:淡入淡出、滑动和擦除等等
【用场景开发也太简单了8!】
前面洋洋洒洒的做了这么多理论铺垫,也许会让你对 scene 的使用产生了“非常复杂”的错觉或者担忧,但实际情况却相反:借助cmsis-pack和RTE的帮助,创建 scene 几乎只要点几下鼠标就可以搞定,而且立即就可以使用。
假设你已经根据《【玩转Arm-2D】入门和移植从未如此简单》的描述,完成了 arm-2d 的部署,并且成功的加入了一个 Display Adapter,此时我们应该能看到这样的效果:
此时,打开 RTE,展开Acceleration后在Arm-2D Helper中找到 Scene:
如果你的界面中找不到 Scene,说明你的 arm-2d cmsis-pack 版本较老,可以关注公众号【裸机思维】后,发送关键字 arm-2d 后获取最新版本的网盘链接。
在Scene的右边,我们可以通过“增加数值”的方式向工程中添加指定数量的场景。单击确定后,对应数量的场景模板会加入到工程管理器中:
这里的 arm\_2d\_scene\_0.h 和 arm\_2d\_scene\_0.c 分别对应我们新加入的场景的头文件和源代码。
打开 main.c,加入对场景的头文件引用:
#include "arm_2d_scenes.h"
其实,所谓的 Display Adapter 就是场景播放器(arm\_2d\_scene\_player\_t):
ARM_NOINIT
extern
arm_2d_scene_player_t DISP0_ADAPTER;
在初始化完 Display Adapter 后,我们调用场景的初始化函数arm\_2d\_scene0\_init()——将它们加入指定的场景播放器队列中:
#include "arm_2d_scenes.h"
...
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() 来切换到我们新加入的场景中:
#include "arm_2d_scenes.h"
...
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();
}
}
为了方便观察效果,不妨设置一个场景切换效果:
#include "arm_2d_scenes.h"
...
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);
/* 设置切换持续时间为 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 又消失在白屏中了呢?要解答这一疑问不妨打开 arm\_2d\_scene\_0.c 一探究竟。注意到时间处理函数 \_\_on\_scene0\_frame\_complete:
static void __on_scene0_frame_complete(arm_2d_scene_t *ptScene)
{
ARM_2D_UNUSED(ptScene);
/* switch to next scene after 3s */
if (arm_2d_helper_is_time_out(3000)) {
arm_2d_scene_player_switch_to_next_scene(ptScene->ptPlayer);
}
}
结合初始化代码:
void arm_2d_scene0_init(arm_2d_scene_player_t *ptDispAdapter)
{
...
arm_2d_scene_t *ptScene = (arm_2d_scene_t *)malloc(sizeof(arm_2d_scene_t));
assert(NULL != ptScene);
*ptScene = (arm_2d_scene_t){
.fnBackground = NULL,
.fnScene = &__pfb_draw_scene0_handler,
.ptDirtyRegion = (arm_2d_region_list_item_t *)s_tDirtyRegions,
/* Please uncommon the callbacks if you need them
*/
//.fnOnBGStart = &__on_scene0_background_start,
//.fnOnBGComplete = &__on_scene0_background_complete,
//.fnOnFrameStart = &__on_scene0_frame_start,
.fnOnFrameCPL = &__on_scene0_frame_complete,
.fnDepose = &__on_scene0_depose,
};
arm_2d_scene_player_append_scenes( ptDispAdapter, ptScene, 1);
}
根据前面事件调用关系流程图,我们容易发现:
- \_\_on\_scene0\_frame\_complete 在场景的每一帧绘制完成后都被调用一次;
- 函数 arm\_2d\_helper\_is\_time\_out() 会在超过3000毫秒后返回 true,并申请场景切换
- 由于场景播放器的队列中已经没有下一个场景,因此以白屏告终。
破案了!!!
此外,观察 arm\_2d\_scene\_0.c 可以发现,该模板已经为我们打好了所有的基础,并添加了所有基础的代码:
- 我们在 \_\_pfb\_draw\_scene0\_handler 中绘制场景,且其中已经包含了一个用 Mask 打印 cmsis-logo 和左上角红色字符串"scene0"的例子代码:
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene0_handler)
{
ARM_2D_UNUSED(pTarget);
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
/*-----------------------draw the foreground begin-----------------------*/
/* following code is just a demo, you can remove them */
arm_2d_fill_colour(ptTile, NULL, GLCD_COLOR_WHITE);
#if 0
/* draw the cmsis logo in the centre of the screen */
arm_2d_align_centre(ptTile->tRegion, c_tileCMSISLogo.tRegion.tSize) {
arm_2d_tile_copy_with_src_mask( &c_tileCMSISLogo,
&c_tileCMSISLogoMask,
ptTile,
&__centre_region,
ARM_2D_CP_MODE_COPY);
}
#else
/* draw the cmsis logo using mask in the centre of the screen */
arm_2d_align_centre(ptTile->tRegion, c_tileCMSISLogo.tRegion.tSize) {
arm_2d_fill_colour_with_mask_and_opacity(
ptTile,
&__centre_region,
&c_tileCMSISLogoMask,
(__arm_2d_color_t){GLCD_COLOR_BLACK},
64);
}
#endif
/* draw text at the top-left corner */
arm_lcd_text_set_target_framebuffer((arm_2d_tile_t *)ptTile);
arm_lcd_text_set_colour(GLCD_COLOR_RED, GLCD_COLOR_WHITE);
arm_lcd_text_location(0,0);
arm_lcd_puts("Scene 0");
/*-----------------------draw the foreground end -----------------------*/
arm_2d_op_wait_async(NULL);
return arm_fsm_rt_cpl;
}
- 初始化代码中已经包含了脏矩阵的范例和如何开启其它事件处理程序的方法:
void arm_2d_scene0_init(arm_2d_scene_player_t *ptDispAdapter,
user_scene_0_t *ptThis)
{
assert(NULL != ptDispAdapter);
/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)
/* a dirty region to be specified at runtime*/
ADD_REGION_TO_LIST(s_tDirtyRegions,
0 /* initialize at runtime later */
),
/* add the last region:
* it is the top left corner for text display
*/
ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
.tLocation = {
.iX = 0,
.iY = 0,
},
.tSize = {
.iWidth = 320,
.iHeight = 8,
},
),
END_IMPL_ARM_2D_REGION_LIST()
/* get the screen region */
arm_2d_region_t tScreen
= arm_2d_helper_pfb_get_display_area(
&ptDispAdapter->use_as__arm_2d_helper_pfb_t);
/* initialise dirty region 0 at runtime
* this demo shows that we create a region in the centre of a screen(320*240)
* for a image stored in the tile c_tileCMSISLogoMask
*/
arm_2d_align_centre(tScreen, c_tileCMSISLogoMask.tRegion.tSize) {
s_tDirtyRegions[0].tRegion = __centre_region;
}
if (NULL == ptThis) {
ptThis = (user_scene_0_t *)malloc(sizeof(user_scene_0_t));
assert(NULL != ptThis);
if (NULL == ptThis) {
return NULL;
}
} else {
bUserAllocated = true;
}
memset(ptThis, 0, sizeof(user_scene_0_t));
*ptThis = (user_scene_0_t){
.use_as__arm_2d_scene_t = {
/* Please uncommon the callbacks if you need them
*/
.fnScene = &__pfb_draw_scene0_handler,
.ptDirtyRegion = (arm_2d_region_list_item_t *)s_tDirtyRegions,
//.fnOnBGStart = &__on_scene0_background_start,
//.fnOnBGComplete = &__on_scene0_background_complete,
.fnOnFrameStart = &__on_scene0_frame_start,
//.fnBeforeSwitchOut = &__before_scene0_switching_out,
.fnOnFrameCPL = &__on_scene0_frame_complete,
.fnDepose = &__on_scene0_depose,
},
.bUserAllocated = bUserAllocated,
};
arm_2d_scene_player_append_scenes( ptDispAdapter, ptScene, 1);
}
是不是非常贴心呢?
【通过代码模板创建新场景】
除了上面介绍的通过RTE来添加新场景的方式,cmsis-pack还未我们在MDK中提供了另外一种选择——通过代码模板来添加。具体步骤为:
1、在工程管理器中选中你想添加代码模板的Group,单击右键,弹出菜单:
2、选择Add New Item to Group。
3、在窗口中找到User Code Template。展开Acceleration,选中Arm-2D:Core的User Scene Template。这里,我们可以在Location中设置代码模板存放的位置。
4、在编辑器中打开新加入的 arm\_2d\_scene\_template.c 和 arm\_2d\_scene\_template.h 。通过文本替换功能
i. 将所有的“<NAME>”替换为你新场景的名称,比如 MY\_SCENE,注意要大写
ii. 将所有的“<name>”替换为你新场景的名称,比如 my\_scene,注意要小写
注意:替换时,请一定要将“Match whole word” 选项去掉,并勾选“Match case”
5、建议根据场景的名称修改arm\_2d\_scene\_template.c 和arm\_2d\_scene\_template.h 的文件名:比如我们的场景叫my scene,因此对应的文件名称为 arm\_2d\_scene\_my\_scene.c 和 arm\_2d\_scene\_my\_scene.h 。
6、将修改名称后的.c和.h加入工程中参与编译。
7、需要使用新场景时,别忘记通过 #include 加入场景的头文件,并调用对应的初始化函数,例如:
#include "arm_2d_scene_my_scene.h"
...
arm_2d_scene_my_scene_init(&DISP0_ADAPTER);
...
【一些值得注意的细节】
细节一:模板中使用了动态的方式来生成场景
虽然不是必须的,但场景的模板中使用了动态的方式来生成场景:
/*!
* \brief initalize scene0 and add it to a user specified scene player
* \param[in] __DISP_ADAPTER_PTR the target display adatper (i.e. scene player)
* \param[in] ... this is an optional parameter. When it is NULL, a new
* user_scene_0_t will be allocated from HEAP and freed on
* the deposing event. When it is non-NULL, the life-cycle is managed
* by user.
* \return user_scene_0_t* the user_scene_0_t instance
*/
#define arm_2d_scene0_init(__DISP_ADAPTER_PTR, ...) \
__arm_2d_scene0_init((__DISP_ADAPTER_PTR), (NULL, ##__VA_ARGS__))
user_scene_0_t *__arm_2d_scene0_init(arm_2d_scene_player_t *ptDispAdapter,
user_scene_0_t *ptThis)
{
assert(NULL != ptDispAdapter);
...
arm_2d_scene_t *ptScene = (arm_2d_scene_t *)malloc(sizeof(arm_2d_scene_t));
assert(NULL != ptScene);
*ptScene = (arm_2d_scene_t){
...
.fnDepose = &__on_scene0_depose,
};
arm_2d_scene_player_append_scenes( ptDispAdapter, ptScene, 1);
}
并在场景的 \_\_on\_scene0\_depose 函数中(也就是场景废弃事件处理程序 fnDepose里)进行了释放:
static void __on_scene0_depose(arm_2d_scene_t *ptScene)
{
ptScene->ptPlayer = NULL;
free(ptScene);
}
当我们初始化一个场景时,我们通常直接调用它的构造函数:
arm_2d_scene0_init(&DISP0_ADAPTER);
此时,该场景就是使用堆来分配对象的。但其实我们可以通过别的方式事先为场景创建变量,并将其指针传递给构造函数:
static user_scene_0_t my_scene0;
arm_2d_scene0_init(&DISP0_ADAPTER, &my_scene0);
此时,该场景将不再“自动使用”堆来创建对象,改为直接初始化我们所提供的场景变量:
user_scene_0_t *__arm_2d_scene0_init(
arm_2d_scene_player_t *ptDispAdapter,
user_scene_0_t *ptThis)
{
...
if (NULL == ptThis) {
ptThis = (user_scene_0_t *)malloc(sizeof(user_scene_0_t));
assert(NULL != ptThis);
if (NULL == ptThis) {
return NULL;
}
} else {
bUserAllocated = true;
}
memset(ptThis, 0, sizeof(user_scene_0_t));
...
}
容易发现,当用户传递的第二个参数 ptThis 不为NULL时,变量 bUserAllocated 将被设置为 true——对应的,在场景释放时,模板中的 \_\_on\_scene0\_depose() 也会通过检测 bUserAllocated发现不需要调用 free() 函数来释放资源:
#undef this
#define this (*ptThis)
static void __on_scene0_depose(arm_2d_scene_t *ptScene)
{
user_scene_0_t *ptThis = (user_scene_0_t *)ptScene;
ARM_2D_UNUSED(ptThis);
...
if (!this.bUserAllocated) {
free(ptScene);
}
}
细节二:场景切换的多种模式
现阶段,场景播放器为用户提供多种切换特效:
typedef enum {
/* valid switching visual effects begin */
ARM_2D_SCENE_SWITCH_MODE_NONE = 0, //!< no switching visual effect
ARM_2D_SCENE_SWITCH_MODE_USER = 1, //!< user defined switching visual effect
ARM_2D_SCENE_SWITCH_MODE_FADE_WHITE = 2, //!< fade in fade out (white)
ARM_2D_SCENE_SWITCH_MODE_FADE_BLACK = 3, //!< fade in fade out (black)
ARM_2D_SCENE_SWITCH_MODE_SLIDE_LEFT = 4, //!< slide left
ARM_2D_SCENE_SWITCH_MODE_SLIDE_RIGHT, //!< slide right
ARM_2D_SCENE_SWITCH_MODE_SLIDE_UP, //!< slide up
ARM_2D_SCENE_SWITCH_MODE_SLIDE_DOWN, //!< slide down
ARM_2D_SCENE_SWITCH_MODE_ERASE_LEFT = 8, //!< erase to the right
ARM_2D_SCENE_SWITCH_MODE_ERASE_RIGHT, //!< erase to the left
ARM_2D_SCENE_SWITCH_MODE_ERASE_UP, //!< erase to the top
ARM_2D_SCENE_SWITCH_MODE_ERASE_DOWN, //!< erase to the bottom
...
};
这其中就包含了大家常见的:
- 淡入淡出:
1 以白色为过渡:ARM\_2D\_SCENE\_SWITCH\_MODE\_FADE\_WHITE
2 以黑色为过渡:ARM\_2D\_SCENE\_SWITCH\_MODE\_FADE\_BLACK
- 滑动效果:LEFT / RIGHT / UP / DOWN
- 擦除效果:LEFT / RIGHT / UP / DOWN
用户可以通过函数 arm\_2d\_scene\_player\_set\_switching\_mode() 来指定切换模式。
此外,用户还可以通过函数arm\_2d\_scene\_player\_set\_switching\_period() 来指定“整个切换过程需要多长时间内完成”——这意味着,如果具体的硬件带宽不足(刷新率不够),场景播放器会以跳帧的方式来满足时间要求。
细节三:模板中使用了动态的方式来生成场景
每个Display Adapter都携带了一个默认的场景,也就是我们移植完毕后所看到的“转圈圈”界面:
它存在的目的主要是帮助我们完成移植时观察现象,并测算基本的带宽信息(测算LCD Latency)。进行实际应用开发时,往往并不希望将其作为用户看到的第一个场景——因此,我们可以通过对应Display Adapter的配置界面将其关闭:即,勾选 Disable the default scene(如下图所示)。
需要特别注意的是:
- 关闭 Display Adapter 的默认场景后,Display Adapter的场景队列实际上是空的
- 用户初始化并加入的第一个场景就成为了场景队列中的第一个场景,也就是当前直接显示在屏幕上的场景——因此无需调用场景切换函数 arm\_2d\_scene\_player\_switch\_to\_next\_scene() 使其生效——这与未关闭默认场景时的情况是不同的。
- 如果你仍然调用了场景切换,且没有后续场景加入场景播放器,那么根据切换特效的不同,你看到的结果可能是白屏、黑屏或者是前一个场景的静止画面。
- 继续向Display Adapter加入场景后,会解除上述现象。
细节四:如何在多个场景中自由切换
场景播放器内部维护的是一个场景的FIFO,其逻辑就是:以用户入队的顺序来顺次播放场景。但实际应用中,场景与场景之间的关系是网状的,而不是一根筋的线性关系,这该如何处理呢?
其实,我们不该被场景播放器的队列迷惑——它只是方便我们事先缓冲一些场景而已:比如将“数据载入的Loading界面”和“下一个工作界面”都加入缓冲等等。
我们需要做的是根据需要往队列里加入少量目标场景即可。这里有两种解决思路:
- 由具体场景决定自己的后续:此时,只需要在对应场景的 \_\_before\_sceneN\_switching\_out() 事件处理程序中初始化“后继”场景即可,比如:
static void __before_scene0_switching_out(arm_2d_scene_t *ptScene)
{
user_scene_0_t *ptThis = (user_scene_0_t *)ptScene;
ARM_2D_UNUSED(ptThis);
/* 加入一个loading页面(当然这个scene是用户事先设计好的) */
arm_2d_scene_loading_page(
this.use_as__arm_2d_scene_t.ptPlayer);
/* 在loading页面后紧随的实际工作场景 */
arm_2d_scene_example_scene(
this.use_as__arm_2d_scene_t.ptPlayer);
}
- 由用户创建的中心调度逻辑来决定。每个Display Adapter(内置了一个场景播放器)都有一个公共的 before\_scene\_switching 事件,我们可以按照下面的方法来注册自己的事件处理程序:
/* load scene one by one */
void before_scene_switching_handler(void *pTarget,
arm_2d_scene_player_t *ptPlayer,
arm_2d_scene_t *ptScene)
{
...
}
int main (void)
{
arm_irq_safe {
arm_2d_init();
}
disp_adapter0_init();
arm_2d_scene_player_register_before_switching_event_handler(
&DISP0_ADAPTER,
before_scene_switching_handler);
...
while (1) {
disp_adapter0_task();
}
}
随后,每当场景播放器(按照用户的申请)即将进行场景切换的前夕,我们注册的 before\_scene\_switching 事件处理程序都会被调用,此时,我们就可以根据自己编写的中心调度策略来决定谁是下一个场景。
比如,这里我们就编写了一个非常简单的场景顺次切换中心调度,仅供娱乐:
void scene0_loader(void)
{
arm_2d_scene0_init(&DISP0_ADAPTER);
}
void scene1_loader(void)
{
arm_2d_scene1_init(&DISP0_ADAPTER);
}
void scene2_loader(void)
{
arm_2d_scene2_init(&DISP0_ADAPTER);
}
void scene3_loader(void)
{
arm_2d_scene3_init(&DISP0_ADAPTER);
}
void scene4_loader(void)
{
arm_2d_scene4_init(&DISP0_ADAPTER);
}
typedef void scene_loader_t(void);
static scene_loader_t * const c_SceneLoaders[] = {
scene0_loader,
scene1_loader,
scene3_loader,
scene4_loader,
scene2_loader,
};
/* load scene one by one */
void before_scene_switching_handler(void *pTarget,
arm_2d_scene_player_t *ptPlayer,
arm_2d_scene_t *ptScene)
{
static uint_fast8_t s_chIndex = 0;
if (s_chIndex >= dimof(c_SceneLoaders)) {
s_chIndex = 0;
}
/* call loader */
c_SceneLoaders[s_chIndex]();
s_chIndex++;
}
实际应用中,中心调度器的逻辑还是要看大家自己“八仙过海各显神通”了。
【说在后面的话】
我们在文章的开头简单介绍了现代嵌入式GUI设计以“面板”为基本单位的设计模式,并以此引入了场景(scene)这个概念。
相信借助 arm-2d 场景播放器(scene player),尤其是在“滑动场景切换特效”的帮助下,在资源受限的环境中,“手撸GUI”的难度将大大降低。
Arm-2D 作为 Cortex-M 处理器的“显卡驱动”,不仅能为已有的GUI协议栈(比如LVGL)提供底层加速,还为资源受限的MCU实现GUI提供了一种“基于面板(Panel)开发”的解决方案。
在使用Arm-2D直接进行应用开发的过程中,场景是基本单位,也就是说我们所有的界面绘制工作都是在具体的场景中进行的。在本文中,我们已经学会了如何创建新的场景,并介绍了场景切换的基本方式。这就好比我们已经拥有了一个基本的舞台。
在下一篇文章中,我们将着重介绍使用 Arm-2D 进行简单GUI开发的一些基本步骤和对应的API函数。
原文:裸机思维
作者:GorgonMeducer 傻孩子
专栏推荐文章
- 【玩转Arm-2D】入门和移植从未如此简单
- 【玩转Arm-2D】十一、酷炫汽车仪表盘是怎么实现的
- 【例说Arm-2D界面设计】还在手算坐标?试试Layout Assistant吧!
- 【喂到嘴边了的模块】不服?跑个分看看!——Coremark篇
- 【喂到嘴边了的模块】害怕追新?LVGL8发布稳定性更新(附部署教程)
如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。