应用

应用框架介绍

名词定义

  1. 内置应用: 编译时已集成到固件中的应用,随系统启动加载,无需额外推送即可运行,通常为设备核心功能(如主菜单、设置、平铺/表盘框架)以及一些固化的应用

  2. 外置应用: 也称动态加载的应用,可通过手机 APP 或串口推送至设备,无需重新编译固件即可新增或更新,支持灵活扩展功能(如第三方工具开发的应用等)

  3. 子页面ID: 应用内部细分页面的唯一标识,用于区分同一应用中的不同功能页面(如闹钟应用的 “设置页”“列表页”),便于框架调度和跳转

  4. 普通应用: 在主菜单中显示图标,可被用户主动点击启动的应用,具备可见的交互入口,是用户主要操作的功能模块

  5. 隐藏应用: 不在主菜单显示对应的应用图标,启动时需要主动调用入口程序,如主菜单以及AOD的应用

  6. 弹窗: 临时悬浮于当前页面的交互界面,用于消息提醒、事件提醒等

  7. AOD: 即 Always On Display(息屏显示),低功耗模式下显示常亮表盘或应用,每分钟(可配置)唤醒刷新一次

  8. 表盘: 智能穿戴设备(如手表)的主界面,通常显示时间、日期及个性化信息(如步数、天气),支持静态或动态样式,可通过内置或外置方式更换。

  9. 平铺: 应用或功能的一种排列展示方式,以网格或列表形式平铺呈现多个选项,用户可滑动切换查看,常见于多应用入口的主界面。

  10. 切换动画: 页面或应用跳转时的过渡效果(如 3D 翻转、滑动、缩放等),用于提升界面交互的流畅度和视觉体验,可根据场景开启或关闭。

如何创建一个内置应用及子页面

注册一个内置应用

内置应用的相关信息通过注册宏统一定义在专属的代码段(section)中,在软件编译阶段即被整合到固件中。开机时,框架会自动从该代码段(BuiltinApp…)加载所有应用,并全程管理其生命周期(包括加载、调度、释放),无需客户手动编写初始化逻辑。因此,添加内置应用的核心操作,就是通过注册宏将应用关联至该专属代码段。

1.注册一个普通应用

    APPLICATION_REGISTER(key_str, img, app_name, ptr_size)
    
    key_str     : 应用显示标题,多语言字符串,仅作显示用
    img         : 应用缩略图,在主菜单中使用
    app_name    : 应用名字,字符串,框架调度该应用时使用,需要保证唯一性
    ptr_size    : 应用全局内存大小,该内存由框架申请释放,页面可以直接使用

2.注册一个隐藏应用

  • 主菜单添加一个应用时是判断是否有相应的应用图标,没有图标则不在主菜单显示

  • 注册的隐藏应用可以通过运行命令直接启动

    APPLICATION_REGISTER_HIDDEN(key_str, app_name, ptr_size)

注册一个子页面

    APP_PAGE_REGISTER(app, subpage, ptr_size)
    app         :  应用名称
    subpage     :  子页面名称
    ptr_size    :  子页面全局内存大小,该内存由框架申请释放,页面可以直接使用

注册框架消息

    APP_MSG_HANDLER(on_start, on_resume, on_pause, on_stop)
    
    on_start    : 页面启动消息,可以在该消息中做一些数据初始化,以及背景页面创建
    on_resume   : 页面激活消息,将页面拉到前台处于活动态,start中未做完的工作在该事件中继续,如启动task,以及大内存申请
    on_pause    : 页面暂停消息,将页面置于隐藏,不可见,一些刷新操作可在此事件中暂停,以及大内存释放
    on_stop     : 页面销毁消息,在该事件中释放用户申请的内存,以及删除创建的task,建立在当前screen上面的obj不用主动删除,
                  框架在发送完该消息后,会直接把当前屏以及上面的控件全部删除.

注册应用入口

    APPLICATION_MAIN(app_id, ptr_size)  
    app_name    : 应用名字
    ptr_size    : 应用全局内存大小

注册流程说明

1.为简化注册流程,框架消息注册以及入口注册均包含在应用注册接口以及页面注册接口内部,无需用户额外处理

2.每个应用注册时会默认注册一个’root’的子页面,以保证每个应用至少有一个子页面(子页面运行层级与注册的先后顺序无关)

3.运行注册的子页面仅有下面一种接口,其他接口参见"gui_app_fwk.h"

    gui_app_run_subpage(const char *app_id, const char *sub_id, void *user_data)
    app_id      : 应用名称
    sub_id      : 子页面名称
    user_data   : 自定义数据,可以通过'APP_GET_PAGE_USERDATA_PTR'接口获取,需注意该接口仅在'msg_handle'中有效

示例

注册一个闹钟应用

    static void on_start(void)
    {   
        /* 获取框架申请的内存,全局指针所对应的结构体大小一定要和申请的大小一致                                            */
        p_alarm = (app_alarm_gui_t *)APP_GET_PAGE_MEM_PTR;
        RT_ASSERT(p_alarm);

        /* 在当前screen上面建立背景页面,包括基础数据读取以及页面标题栏                                                    */
        p_alarm->bg_page = app_alarm_gui_init(lv_scr_act());

        /* 创建一个刷新timer,定时查询状态,亦可使用数据订阅                                                                */
        p_alarm->timer = lv_timer_create(alarm_task_cb, 1000, (void *)0);

        /* 将timer设置pause状态,因为初始页面不需要刷新,暂时关闭timer                                                      */
        lv_timer_pause(p_alarm->timer);
    }

    static void on_resume(void)
    {
        /* 收到resume消息后,将该页面的所有内容创建完成                                                                    */
        app_alarm_main_bg_page(p_alarm->bg_page);
        
        /* 页面创建完成后启动刷新timer                                                                                    */
        lv_timer_resume(p_alarm->timer);
    }

    static void on_pause(void)
    {
        /* 页面进入隐藏不可见,关闭刷新timer                                                                               */
        lv_timer_pause(p_alarm->timer);
    }

    static void on_stop(void)
    {
        /* 页面即将销毁,先删除刷新timer,因为该timer不属于当前screen的子控件,销毁当前页面时不会自动销毁                    */
        lv_timer_del(p_alarm->timer);

        /* 将全局指针变量置空,防止其他地方使用时非空判断出现异常,该指针指向的内存会在框架执行完stop 消息后释放           */
        p_alarm_main = NULL;
    }

    /* 注册Alarm应用,并使用框架申请的大小为'sizeof(app_alarm_gui_t)' 的内存                                              */
    APPLICATION_REGISTER(app_get_strid(key_alarm, "Alarm"), img_alarm, "Alarm", sizeof(app_alarm_gui_t));

如何创建一个外置应用(动态应用)

外置应用也称动态应用或动态加载应用,可以通过外置应用功能实现应用市场。动态应用具体介绍

注册一个外置应用

1,代码第一行定义DYN_APP宏,申明此APP是外置app,资源通过外置方式获取

    #define DYN_APP

2,注册函数定义,函数类型为

    typedef void(*module_init_func_t)(void);

如注册函数名register_func,函数的实现必须调用外置app的注册接口,将注册信息记录到链表,

    dynamic_app_register(const char*id, const char* title, gui_app_entry_func_ptr_t entry, const void* thumbnail)
    id          : 应用名称
    title       : 应用标题,用以显示用
    entry       : 应用的入口函数固定为'app_main'
    thumbnail   : 应用图标固定为'APP_GET_THUM(tn)'

3,模块初始化和清除回调 将注册函数插入模块初始化中,模块的清除回调典型为NULL

    MODULE_INIT_DEF(register_func)
    MODULE_CLEANUP_DEF(NULL)

示例

#define DYN_APP
#include "global.h"
#include "dynamic_app.h"

#define _MODULE_NAME_   'dyn_plane' /*外置应用在模拟器中使用时需要定义'_MODULE_NAME_',值必须和动态应用APP_ID相同*/
#include "app_module.h"             /*资源使用接口的头文件包含*/

/**
user logic
**/

static void on_start(void)
{
    /*user logic*/
}

static void on_resume(void)
{
    /*user logic*/
}

static void on_pause(void)
{
    /*user logic*/
}

static void on_stop(void)
{
    /*user logic*/
}

/*仅为模拟器调试使用的注册接口*/
APPLICATION_REGISTER(app_get_strid(key_plane, "Plane"), img_game_plane, "dyn_plane", 0);

void game_plane_register(void)
{
    dynamic_app_register("dyn_plane", app_get_str(key_plane, "Plane"), app_main, APP_GET_THUM(tn));   
}

MODULE_INIT_DEF(game_plane_register)
MODULE_CLEANUP_DEF(NULL)

注册外置的子页面

1,代码第一行定义DYN_APP宏,申明此页面是外置页面,资源通过外置方式获取

    #define DYN_APP

2,消息处理函数定义

    APP_MSG_HANDLER(on_start, on_resume, on_pause, on_stop)

3,外置应用的子页面典型使用方式为显示调用子页面调用函数,函数内部通过注册接口注册子页面,注册函数如下:

    gui_app_create_page_for_app_ext(const char* app_id, const char* page_id, gui_app_msg_cb_t handler, void* user_data, uint32_t mem_size)
    app_id      : 子页面对应的应用ID
    page_id     : 子页面ID
    handler     : 子页面消息处理函数, 固定为'msg_handler'
    user_data   : 自定义数据,可以通过'APP_GET_PAGE_MEM_PTR'接口获取,需注意该接口仅在'handler'中有效
    mem_size    : 子页面全局内存大小页面可以通过'APP_GET_PAGE_MEM_PTR'获取使用需注意该接口仅在'handler'中有效

示例

#define DYN_APP
#include "global.h"
#include "dynamic_app.h"

#define _MODULE_NAME_   'dyn_plane' /*外置页面在模拟器中使用时需要定义'_MODULE_NAME_',值必须和动态应用APP_ID相同*/
#include "app_module.h"             /*资源使用接口的头文件包含*/
#define PAGE_ID  "setting"
/**
user logic
**/

static void on_start(void)
{
    /*user logic*/
}

static void on_resume(void)
{
    /*user logic*/
}

static void on_pause(void)
{
    /*user logic*/
}

static void on_stop(void)
{
    /*user logic*/
}

APP_MSG_HANDLER(on_start, on_resume, on_pause, on_stop)

/*显示调用此函数创建子页面*/
int dyn_plane_subpage_create(void)
{
    /*user_data 和 ptr_size根据实际情形填写*/
    gui_app_create_page_for_app_ext("dyn_plane", PAGE_ID, msg_handler, NULL, 0)
}

注册外置弹窗

1,代码第一行定义DYN_APP宏,申明此弹窗是外置弹窗,资源通过外置方式获取

    #define DYN_APP

2,通过以下接口注册,目前一个app只能注册一个弹窗,如需要多弹窗功能,可在弹窗逻辑中区分,实现多弹窗功能

    POPUP_REGISTER(id, priority, time, ptr_size)
    id                  : 弹窗名称, 需要和应用ID一致
    priority            : 弹窗优先级,数字越大,优先级越高
    time                : 弹窗生命周期
    ptr_size            : 弹窗全局内存大小,该内存由框架申请释放,页面可以通过'POPUP_GET_NODE_MEM_PTR'使用

示例

#define DYN_APP
#include "global.h"
#include "popup_fwk.h"

#define _MODULE_NAME_   'dyn_plane' /*外置弹窗在模拟器中使用时需要定义'_MODULE_NAME_',值必须和动态应用APP_ID相同*/
#include "app_module.h"             /*资源使用接口的头文件包含*/

/**
user logic
**/

static void on_start(void* param)
{
    /*user logic*/
}

static void on_refr(void* param)
{
    /*user logic*/
}

static void on_stop(void* param)
{
    /*user logic*/
}

POPUP_REGISTER("dyn_plane", 5, 3000, 0);

如何调整开机后默认启动的应用

以门锁产品为例, 上电后 UI 部分需要直接启动至对应的门锁主应用界面 (类似于单应用形态产品, 不需要进入 Main 主菜单界面)

基于目前 Solution mod 启动流程修改方案参考如下

  1. 将默认 APP 从 Main 主菜单调整为自定义应用 (如以已有的文件管理器 filemanage 为例)

    • 在 Func: gui_app_init / File: gui_app_fwk.c 中通过 uint32_t app_schedule_change_main_app_id(const char *id) 将默认应用从原来的 Main 主菜单设置为新的应用 (如 filemanage 文件管理器)

--- "a/sdk/middleware/app_fwk/gui_app_fwk.c"
+++ "b/sdk/middleware/app_fwk/gui_app_fwk.c"
@@ -938,7 +938,7 @@ void gui_app_init(uint16_t style)
         app_trans_animation_init();
-        app_schedule_change_main_app_id("Main");
+        app_schedule_change_main_app_id("filemanage");
 #ifdef GUI_MAX_RUNNING_APPS
         app_schedule_max_running_apps(GUI_MAX_RUNNING_APPS);
 #endif /* GUI_MAX_RUNNING_APPS */
  1. 将开机后自动启动的 APP 从 Main 主菜单调整为自定义应用 (如以已有的文件管理器 filemanage 为例)

    • 在 Func: app_power_on_process / File: app_pm_custom.c 中通过 int gui_app_run(const char *cmd) 将默认启动的 Main 主菜单设置为新的应用 (如 filemanage 文件管理器)

--- "a/Solution/components/pm/app_pm_custom.c"
+++ "b/Solution/components/pm/app_pm_custom.c"
@@ -157,7 +157,7 @@ void app_power_on_process(void)
 #endif
     {
 #ifdef GUI_APP_FRAMEWORK
-        gui_app_run("Main");
+        gui_app_run("filemanage");
 #endif
     }
  1. 对应单应用形态, 配置同时运行的 APP 数量为 1

    • 通过 Menuconfig 修改 GUI_MAX_RUNNING_APPS1

  2. 精简其他不相关服务以加速上电开机速度等

    • 如关闭 MESSAGEALARM 相关功能, 可以通过 Menuconfig 关闭对应的宏控即可

      • APP_MESSAGE_USED

      • APP_ALARM_USED

如何控制临时关闭动画换页效果?

当应用页面切换的动画效果(如 3D 翻页、滑动等)已开启时,默认会在页面跳转时展示动效。但部分场景下(例如连续启动两个页面,需跳过中间页面的过渡动画),可通过以下方式临时关闭动画:

操作方法

  • 在调用页面创建接口(如 gui_app_create_pagegui_app_create_page_for_app)后,立即调用 void gui_app_exec_now(void) 函数,即可取消当前页面切换的动画过程。

实现原理

  • 页面创建接口(gui_app_create_page 等)默认采用异步方式实现,通过 send_msg_to_gui_app_task 发送消息触发页面创建,因此会自然触发预设的切换动画

  • gui_app_exec_now 函数会强制同步执行页面创建流程,并在内部调用 app_schedule_enable_trans_anim(false) 临时关闭动画开关,从而跳过中间过渡效果

此方法适用于多级页面连续跳转场景,既能保留正常场景下的动画体验,又能在需要时避免多余动效,提升操作流畅度。