16

傻孩子(GorgonMeducer) · 2022年10月24日 · 北京市

【玩转Arm-2D】八、实战篇 - 手撸酷炫智能手表GUI

tips:文末有彩蛋哦       

这篇文章更__新了Scene模板的使用,大家赶快去文章中找找吧,还会有很多惊喜的(* ̄︶ ̄)

随着智能手机的普及,各种智能仪表也铺天盖地席卷而来,所谓智能,就是增加了人机交互,而人机交互最主要的就是一块彩色的屏幕。那么,在一款资源紧缺(Flash <= 64K、SRAM <= 16K)且带了彩屏的嵌入式产品中,想制作出类似智能手机界面的GUI可以吗?或者说GUI设计上有什么模板可遵循么?

答案是肯定的。

image.png

【基于面板的界面设计范式】

这就是一种基于面板的嵌入式界面设计模板(范式,如下图所示:

image.png

图中我们可以知道一个GUI主要由状态面板导航面板功能面板3大模块组成。其中:

状态面板(即待机界面,是用于显示状态信息的(比如温度、时间、产品Logo、产品当前状态等等);在导航面板或功能面板等待机一段时间后无用户操作则进入状态面板(即待机界面)。

导航面板(即菜单界面),一般以图标、列表或者按钮的形式存在,一般由状态面板按任意键进入;在导航面板中根据用户选择又可以进入次级导航面板,例如时间设置又可以分为年月日时分秒的设置。

功能面板也就是实现具体功能的界面,一般由导航面板进入,每个面板的功能都尽可能的单一,比如专门设置温度上下限、专门设置时间等等。在功能面板中,也可以设置快捷导航按键,比如在任何功能面板中通过快捷导航都可以进入微信聊天的界面。

好了,有了这个范式,是不是觉得嵌入式设备的GUI设计也变得简单了(* ̄︶ ̄)

【智能手表GUI设计】

那我们就用上面的设计范式,设计一款智能手表的GUI,包含状态面板导航面板功能面板,如下图所示:

状态面板中,设计了WiFi、蓝牙的图标,用来表示是否连接成功(连接成功显示蓝色,否则显示灰色)和充电状态的图标,用来表示是否正在充电,在状态面板的下面显示时间和温度。

导航面板中,设计了9个图标,分别点击不同的图标会进入不同的功能面板,怎么样,是不是感觉有点像智能手机的风格了(* ̄︶ ̄)

功能面板中,由于我们在导航面板中设计了9个图标,所以在功能面板模块中需要设计出9个不同的面板来与之一一对应。

到这里,大家是不是发现了,我们把GUI拆成了一个个面板来设计(有点像PPT的制作了),而每一个面板就相当于是Arm-2D的一个scene,面板中需要显示的内容(比如图标、时间、温度等)就是用scene中的绘制函数来绘制的。

那面板的切换怎么办呢,也就是scene间是怎么切换的呢?

这就要用到Scene Player(场景播放器)了,他负责场景间的切换、切换时的动画效果和切换需要持续的时间。好,那我们就简单介绍一下今天的主角Scene Player。

【场景播放器简介】

Scene Player(场景播放器)是Arm-2D为资源紧缺的MCU开发简易GUI专门提供的一个工具,有了他可以让“手撸GUI”变得非常简单,而且还提供了界面切换时的动画效果。

场景播放器的本质是一个针对场景(scene)的队列(FIFO),我们可以预先生成多个场景,并通过函数append_scenes压入队列中(在队列中排队准备显示),排在队列前面的(队列头)优先被显示。

image.png

我们可以在任意时刻通过场景切换函数来安全的触发场景切换(所谓的场景切换就是丢弃队列当前的头部场景——换成下一个)。

下面我们就来看看场景切换的函数,如下

arm_2d_scene_player_switch_to_next_scene(ptScene->ptPlayer);

说到切换,肯定少不了动画效果,Arm-2D也为我们提供了很多切换动效,如下所示:

/* valid switching visual effects begin */
ARM_2D_SCENE_SWITCH_CFG_NONE           = 0,//!< no switching visual effect
ARM_2D_SCENE_SWITCH_CFG_USER           = 1,//!< user defined switching visual effect
ARM_2D_SCENE_SWITCH_CFG_FADE_WHITE     = 2,//!< fade in fade out (white)
ARM_2D_SCENE_SWITCH_CFG_FADE_BLACK     = 3,//!< fade in fade out (black)
ARM_2D_SCENE_SWITCH_CFG_SLIDE_LEFT     = 4,//!< slide left
ARM_2D_SCENE_SWITCH_CFG_SLIDE_RIGHT,       //!< slide right
ARM_2D_SCENE_SWITCH_CFG_SLIDE_UP,          //!< slide up
ARM_2D_SCENE_SWITCH_CFG_SLIDE_DOWN,        //!< slide down
ARM_2D_SCENE_SWITCH_CFG_ERASE_LEFT     = 8,//!< erase to the right
ARM_2D_SCENE_SWITCH_CFG_ERASE_RIGHT,       //!< erase to the left
ARM_2D_SCENE_SWITCH_CFG_ERASE_UP,          //!< erase to the top
ARM_2D_SCENE_SWITCH_CFG_ERASE_DOWN,        //!< erase to the bottom
  • 第1个_ARM_2D_SCENE_SWITCH_CFG_NONE_就是没有切换动画,一般帧率比较低的时候就选择这个,不开启动画效果。
  • 第2个_ARM_2D_SCENE_SWITCH_CFG_USER_,是自定义动画效果,感兴趣的也可以实现自己的动画效果哦。
  • 剩下的就是Arm-2D提供的动画效果了,大家可以挨个试试(有淡入淡出、左移、右移、擦除等)。

设置动画效果的函数如下:


||设置切换特效为 淡入淡出(白色) 
arm_2d_scene_player_set_switching_mode( 
    &DISP0_ADAPTER,
    ARM_2D_SCENE_SWITCH_CFG_FADE_WHITE
); 

设置了动画效果,切换持续时间也要记得设置一下,如下


||设置切换持续时间为 3000ms 
arm_2d_scene_player_set_switching_period(
    &DISP0_ADAPTER, 
    3000);

对了,在切换之前,一定要先往场景队列中添加一个新场景(否则就只能切换个寂寞了,啥也看不到),需要调用如下函数:


void arm_2d_scene_player_append_scenes( 
      arm_2d_scene_player_t *ptThis, 
      arm_2d_scene_t *ptScenes,
      int_fast16_t hwCount)

接下来我们再看看场景绘制的接口回调函数,如下图所示:

image.png

图中我们也很容易发现,在绘制场景前会先绘制一次背景,因为背景一般只需要绘制一次就可以,不过Arm-2D也提供了更新背景的接口函数,如下:

arm_2d_scene_player_update_scene_background(ptScene->ptPlayer);

到这里,Scene Player我们就简单介绍完了,大家是不是也迫不及待想要制作自己的智能手表界面了,下面我们就开始制作。

Scene模板的使用

首先,我们先生成scene.c文件,打开 RTE,展开Acceleration后在Arm-2D Helper中找到 Scene,如下图
image.png
Scene的右边,我们可以通过“增加数值”的方式向工程中添加指定数量的场景。单击OK键,对应数量的场景模板便会加入到工程管理器中,如下图所示:
image.png
这样我们的3个scene就生成出来了,是不是很方便啊(* ̄︶ ̄)

那我们就打开生成的scene.c文件,看看里面都有什么,如下所示:


static void __on_scene1_depose(arm_2d_scene_t *ptScene)
{}
static void __on_scene1_background_start(arm_2d_scene_t *ptScene)
{}

static void __on_scene1_background_complete(arm_2d_scene_t *ptScene)
{}
static void __on_scene1_frame_start(arm_2d_scene_t *ptScene)
{}
static void __on_scene1_frame_complete(arm_2d_scene_t *ptScene)
{}
//背景绘制函数
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene1_background_handler)
{
    user_scene_1_t *ptThis = (user_scene_1_t *)pTarget;    
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    /*-----------------------draw back ground begin-----------------------*/
        
    /*-----------------------draw back ground end  -----------------------*/
    arm_2d_op_wait_async(NULL);
    return arm_fsm_rt_cpl;
}
//前景绘制函数
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene1_handler)
{
    user_scene_1_t *ptThis = (user_scene_1_t *)pTarget;
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    /*-----------------------draw the foreground begin-----------------------*/
    
    /*-----------------------draw the foreground end  -----------------------*/
    arm_2d_op_wait_async(NULL);

    return arm_fsm_rt_cpl;
}

原来在此文件中实现了Scene Player的回调函数,有了这个scene模板,剩下的就是填空了(* ̄︶ ̄)

对了,这个模板还帮我们生成了一个叫做arm_2d_scenes.h的头文件,这个头文件就把所有通过rte加入的scene都囊括了,在需要用到场景的地方包涵这个头文件就可以了,是不是很贴心(* ̄︶ ̄)

状态面板的制作

接下来,我们就以状态面板为例,来进行填空。

我们要实现的状态面板如下:

image.png

只需要在___pfb_draw_scene1_handler_函数中绘制就可以了,程序如下


static
IMPL_PFB_ON_DRAW(__pfb_draw_scene1_handler)
{
    user_scene_1_t *ptThis = (user_scene_1_t *)pTarget;
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    /*-----------------------draw the foreground begin-----------------------*/
    do{
        arm_2d_region_t my_region = {
            .tLocation = {.iX = 20,.iY = 20, },
            .tSize = {.iWidth = 200,.iHeight = 40,},   
        };                        

                                           
        //! 绘制白色的工具栏     
        draw_round_corner_box(
              ptTile,//
              &my_region,
              GLCD_COLOR_WHITE,
              64,
              bIsNewFrame);
       //! 绘制工具栏上一个 40*40 的图标
        my_region.tLocation.iX = 40;
        my_region.tLocation.iY = 20;
        my_region.tSize.iWidth = 40;
        my_region.tSize.iHeight = 40;
        arm_2d_fill_colour_with_mask_and_opacity(  
                ptTile,
                &my_region,
                &c_tilewifiGRAY8,//&c_tilewifiGRAY8,34  29
                (__arm_2d_color_t){GLCD_COLOR_BLUE},
                200);  
       ...               
     }
    /*-----------------------draw the foreground end  -----------------------*/
    arm_2d_op_wait_async(NULL);

    return arm_fsm_rt_cpl;
}
  • 这个界面也很简单,图标就是颜色填充,时钟的制作就是图片加旋转(旋转之前讲过,具体程序就没有贴),值的一说的就是我们使用了圆角矩形函数draw_round_corner_box绘制工具栏,这个也是Arm-2D为我们提供的,这个圆角矩形不仅颜色可以改变,而且还可以设置透明度哦,最重要的是他支持任意大小,在界面设计中是非常实用的。

开机显示状态面板

我们的状态面板绘制很快就填完了,那怎么让他开机就显示呢?

首先,需要在arm_2d_disp_adapter_0.h文件中把默认显示的sence设置成Disable,如下图所示:

image.png

然后在main函数中添加我们自己的scence,如下


#include "arm_2d_scenes.h"

int main(void) 
{
    system_init();
    ...
    // 初始化 Display Adapter 0
    disp_adapter0_init();
    
    //初始化我们自己的scence
    arm_2d_scene1_init( &DISP0_ADAPTER, NULL);
    ...
    while (true) {       
        // 执行 Display Adapter 的刷新任务
        disp_adapter0_task();       
    } 
}

在_arm_2d_scene1_init()_实际上是函数__arm_2d_scene1_init()的封装*,其中调用了_arm_2d_scene_player_append_scenes_函数来往DISP0_ADAPTER*队列中添加我们自己的scene,这样就可以显示我们自己的状态面板了。

  • 当第2个参数传为NULL时,会调用malloc函数分配空间,所以要在fnDepose回调函数中进行释放。当然也可以自己定义成静态变量传进去,就可以不使用malloc函数了。

__arm_2d_scene1_init函数(这个函数也是模板生成的哈)如下所示:


user_scene_1_t *__arm_2d_scene1_init(   
    arm_2d_scene_player_t *ptDispAdapter, 
    user_scene_1_t *ptScene)
{
    bool bUserAllocated = false;
    assert(NULL != ptDispAdapter);
   //定义脏矩阵
    IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)

        /* a dirty region to be specified at runtime*/
        ADD_REGION_TO_LIST(s_tDirtyRegions,0  ),
        
        ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
            .tLocation = {.iX = 20,.iY = 20,},
            .tSize = { .iWidth = 200, .iHeight = 200,},  ), 
        END_IMPL_ARM_2D_REGION_LIST()            
    //分配空间
    if (NULL == ptScene) {
        ptScene = (user_scene_1_t *)malloc(sizeof(user_scene_1_t));
        assert(NULL != ptScene);
        if (NULL == ptScene) {
            return NULL;
        }
        bUserAllocated = true;
    } else {
        memset(ptScene, 0, sizeof(user_scene_1_t));
    }
    //添加自己的回调函数
    *ptScene = (user_scene_1_t){
        .use_as__arm_2d_scene_t = {
        /* Please uncommon the callbacks if you need them
         */
        //.fnBackground   = &__pfb_draw_scene1_background_handler,
        .fnScene        = &__pfb_draw_scene1_handler,
        //.ptDirtyRegion  = (arm_2d_region_list_item_t *)s_tDirtyRegions,
        //.fnOnBGStart    = &__on_scene1_background_start,
        //.fnOnBGComplete = &__on_scene1_background_complete,
        //.fnOnFrameStart = &__on_scene1_frame_start,
        .fnOnFrameCPL   = &__on_scene1_frame_complete,
        .fnDepose       = &__on_scene1_depose,
        },
        .bUserAllocated = bUserAllocated,
    };
    //往队列中添加sence
    arm_2d_scene_player_append_scenes(  
        ptDispAdapter, 
        &ptScene->use_as__arm_2d_scene_t, 
        1);
    return ptScene;
}  
  • 此函数中一共做了4件事情:

    1、定义刷新用的脏矩阵(根据具体的面板进行修改);

    2、给ptScene分配空间(一般不需要修改);

    3、把分配好的ptScene添加自己的回调函数(一般不需要修改);

    4、最后在把ptScene添加到队列中进行显示(一般不需要修改)。

  • 脏矩阵的修改只需要定义出界面需要改变的区域就可以(也就是前景的区域),不是整个屏幕的区域,如下图所示
  • 图片

由于我们使用了malloc函数进行分配,所以要在_depose_函数中进行释放,不过这个模板也帮我们生成了,是不是很贴心(* ̄︶ ̄)如下

static void __on_scene1_depose(arm_2d_scene_t *ptScene)
{
    user_scene_1_t *ptThis = (user_scene_1_t *)ptScene;
    ARM_2D_UNUSED(ptThis);    
    ptScene->ptPlayer = NULL;    
    /* reset timestamp */
    this.lTimestamp = 0;
    if (this.bUserAllocated) {
        free(ptScene);
    }
}

到此,我们的第一个面板就制作完成了,怎么样赶快动手试试吧。

导航面板的制作

下面我们就来实现一个以图标为按钮的导航面板,如下图所示:

image.png

这个界面的制作也很简单,主要就是对图标进行贴图,界面绘制函数如下


static
IMPL_PFB_ON_DRAW(__pfb_draw_scene2_handler)
{
    user_scene_2_t *ptThis = (user_scene_2_t *)pTarget;
    ARM_2D_UNUSED(ptTile);
    ARM_2D_UNUSED(bIsNewFrame);
    //填充背景颜色    
    arm_2d_fill_colour(ptTile, NULL, bgColour[s_bg_flag & 0x7]);    
    do{
        char i = 0, j = 0;
        arm_2d_region_t my_region = {
            .tLocation = {.iX = 30,.iY = 30, },
            .tSize = {.iWidth = 48, .iHeight = 48, },  };   
        //绘制9个图标           
        for(i = 0; i < 3; i++){
            for(j = 0; j < 3; j++){
                my_region.tLocation.iX   = 30 + 68 * j; 
                my_region.tLocation.iY   = 30 + 68 * i;;    
                arm_2d_rgb565_tile_copy_with_src_mask( 
                      icons[j+i*3].ptTile,      //source tile address // 
                      icons[j+i*3].ptMaskTile,  //source mask address // 
                      ptTile,                   //target tile address // 
                      &my_region,               //region address //      
                      ARM_2D_CP_MODE_COPY );    //copy mode //                  
            }
        }           
    }while(0);
    /*-----------------------draw the foreground end  -----------------------*/
    arm_2d_op_wait_async(NULL);

    return arm_fsm_rt_cpl;
}
  • 使用了tile_copy_with_src_mask函数来绘制图标,这样在切换背景时效果也非常好。
  • 大家也看到了,背景色的填充也在pfb_draw_scene2_handler中绘制的,而不是在背景函数中fnBackground绘制的,这是怎么回事呢?

这里有一个小技巧需要说明一下:

image.png

很多情况下,背景和前景绘制【应该】是一样的,只不过背景是完整版本,包含那些不会改变的,而前景是配合脏矩阵,只更新那些会变化的。所以,我们只提供fnScene脏矩阵,把fnBackground设置为NULL就可以了,这样当你绘制背景的时候,就会完整绘制fnScene了。

什么,你还没听懂?

那简单点说,就是你编写绘制函数的时候根本不用考虑前景和背景,只需要在初始化scene的时候用脏矩阵标记那些会变化的部分就行了,是不是感到了无脑的便利(* ̄︶ ̄)

那背景又是怎么更新的呢?

当然还是调用那个api,在需要更新的时候调用就可以了。

调用更新背景的api我放到了场景绘制完成的回调函数中,如下所示:

static void __on_scene2_frame_complete(arm_2d_scene_t *ptScene)
{
    user_scene_2_t *ptThis = (user_scene_2_t *)ptScene;
    ARM_2D_UNUSED(ptThis);
    if(s_bg_flag < 7){
    //1000ms更新一次
         if (arm_2d_helper_is_time_out(1000, &this.lTimestamp)) {
            s_bg_flag ++;
            arm_2d_scene_player_update_scene_background(ptScene->ptPlayer);
         }
     } 
}

小结:背景的绘制大家记住这两点就可以了:

image.png

然后,又该圆角矩形上场了。我们用圆角矩形选择对应的图标,再点击按钮进入相应的功能面板就可以了。程序如下:


do{
  char i,j;
  j = s_select % 3;
  i = s_select / 3;
  my_region.tLocation.iX   = 30 + 68 * j-3; 
  my_region.tLocation.iY   = 30 + 68 * i-3;
  my_region.tSize.iWidth = 48+6;
  my_region.tSize.iHeight = 48+6;
  draw_round_corner_box(
            ptTile,
            &my_region,
            GLCD_COLOR_WHITE,
            128,
            bIsNewFrame);
}while(0);
  • 提示:我们可以调整圆角矩形的填充颜色和透明度来使选择的图标进行高亮显示

到此,导航面板就绘制完成了,那怎么从状态面板切换到导航面板呢

这里就需要调用switch_to_next_scene函数了,如下:


static void __on_scene1_frame_complete(arm_2d_scene_t *ptScene)
{
    user_scene_1_t *ptThis = (user_scene_1_t *)ptScene;
    ARM_2D_UNUSED(ptThis);
    
    /* switch to next scene after 3s */
    if (arm_2d_helper_is_time_out(3000, &this.lTimestamp)) { 
       // 我们自己添加的 scene初始化        
        __arm_2d_scene2_init(&DISP0_ADAPTER,  NULL);
        //模板生成的
        arm_2d_scene_player_switch_to_next_scene(ptScene->ptPlayer);            
     }       
}
  • 在状态面板绘制完成的回调函数中进行切换,在切换前记得调用导航面板的初始化函数就可以了,当然你也可以设置切换动效哦。
  • 我们还使用了arm_2d_helper_is_time_out函数,也就是定时3000毫秒进行切换。
  • 小提示:这个定时切换也是 scene模板帮我们生成的,它会自动在里面加一个3秒后切换到下一个场景的代码,这样,我们甚至可以在main函数中无脑添加很多 scene初始化,他们会自己顺次切换(相当于PPT的播放),这可真是名副其实的Player了。哈哈,这个的好处主要是方便测试。

功能面板的制作

功能面板大家可以根据具体的功能进行设计,我也简单的实现了一个微信聊天的界面,如下图:

image.png

这个面板界面也很简单,代码我们就不贴了。其中使用了4个圆角矩形,我在图中进行了高亮显示,是不是觉得这个圆角矩形在界面设计中非常好用,大家赶快动手试试吧!

制作完成的界面视频演示效果如下:

image.png
大家也可以参考官方的代码来实现自己的scene,地址如下:

https://github.com/ARM-software/Arm-2D/blob/developing/examples/%5Btemplate%5D%5Bbare-metal%5D%5Bpfb%5D/project/mdk/RTE/Acceleration/arm_2d_scene_0.c

下面是今天的彩蛋环节

在视频中我们发现每一个面板页面都有一个【玩转Arm-2D】的水印LOGO,他是怎么制作的呢?难道是要在每一个面板界面中都加入一个LOGO的绘制吗?这样是可以,不过非常麻烦,而且还不好维护,因为很容易在某个面板中遗漏掉。

此时,如果有一个这样的接口,可以一直保持在所有面板图层的上面进行绘制,这样我们就只需要设计水印本身,而不用在每个场景里都去绘制一次水印了。哈哈。。。这个想法是真的好(* ̄︶ ̄)

其实Arm-2D早就为我们设计好了,这个就是Arm-2D为我们提供的navigation layer的绘制。

image.png

水印LOGO的制作

有了Navigation Layer,水印Logo就跟场景解藕了,它可以一直保持在所有面板图层的上面进行绘制。这样,就可以在每个面板界面看到我们的水印Logo了。

好,接下来,就开始制作水印Logo,首先准备一个Logo图标,如下

image.png

接着,我们就在__pfb_draw_navigation函数中进行绘制就可以了,此函数在arm_2d_disp_adapter_0.c文件中,如下

static
IMPL_PFB_ON_DRAW(__pfb_draw_navigation)
{
    do{
        arm_2d_region_t my_region = {
            .tLocation = {.iX = 20,.iY = 15, },
            .tSize = {.iWidth = 76,.iHeight = 34,},  };               
        arm_2d_fill_colour_with_mask_and_opacity(  
              ptTile,
              &my_region,
              &c_tilemy_logo_01Mask,
              (__arm_2d_color_t){GLCD_COLOR_LOGO_BLUE},
              35);                                      
    }while(0);

    return arm_fsm_rt_cpl;
}
  • 程序中使用了mask填充函数fill_colour_with_mask_and_opacity,这样不仅可以改变填充颜色还可以设置透明度,一举两得(* ̄︶ ̄)
  • 不过Navigation layer一般是用来做悬浮导航条和显示光标箭头之类的,因为它不会受到场景切换的影响(也就是永远在最上面,遮不住)。

这样我们的水印logo就制作完了,还是很简单的。

最后的最后我们在总结一下:

image.png

至此用Arm-2D的Scene Player(场景播放器)“手撸GUI”就讲完了,是不是很简单,大家赶快动手试试吧。

原文:嵌入式小书虫
作者:FledgingSu 支离苏

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1479
内容数
116
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息