OpenHarmony 小鸿 AI 开发实战 07:用 LVGL 构建 240×240 语音伙伴界面

发布时间:2026/7/24 18:43:19
OpenHarmony 小鸿 AI 开发实战 07:用 LVGL 构建 240×240 语音伙伴界面 240×240 小屏不是把手机页面缩小。当前小鸿界面要同时容纳状态栏、语音焦点、状态提示、对话文本、音量反馈和配网/升级页面还要在 WS63 的 SRAM 与 LiteOS-M 任务模型下稳定刷新。界面代码因此被拆成布局常量、对象创建、视觉状态、事件消费和长文本分页五个部分而不是在一个循环里不断清屏重画。本文依据当前lcd_st7789_driver.h、lvgl_ui_layout.h/.c、lvgl_task.c和显示事件枚举整理。所有尺寸、缓冲区大小、颜色、页面时长和状态文案都来自实际源码。最新 UI 固件包已存在并记录哈希但本轮没有重新烧录后拍摄实机屏幕所以文中使用的是根据代码确定性渲染的结构图不把设计示意图冒充设备照片源码验证与实机视觉验收仍然分开。显示尺寸由 ST7789 驱动向布局层传递面板驱动头文件定义 240×240lvgl_ui_layout.h再把LCD_PANEL_PIXEL_X/Y映射成DISP_WIDTH/HEIGHT。如果驱动没有给出尺寸预处理直接报错不允许布局悄悄使用另一个默认值。这样布局常量和 LVGL display 创建共享同一尺寸来源。#if defined(LCD_PANEL_PIXEL_X) #define DISP_WIDTH LCD_PANEL_PIXEL_X #else #error LCD_PANEL_PIXEL_X unknown #endif #if defined(LCD_PANEL_PIXEL_Y) #define DISP_HEIGHT LCD_PANEL_PIXEL_Y #else #error LCD_PANEL_PIXEL_Y unknown #endif当前 UI 留出 8 像素安全边界状态栏从 y4 开始高 22焦点环位于 64、36尺寸 112中心核心为 90×90。提示文字位于 y149对话摘要从 y179 开始。这些位置围绕 240 像素高度手工组织目标是优先让用户看清“设备现在在做什么”。常态页面由状态栏、焦点区和两段文字组成常态布局上方是 Wi-Fi、音量、亮度和电池状态中间 112×112 区域包含 12 段环形状态点、90×90 核心、咕咕嘎嘎图像或 Emoji以及监听时使用的五根波形条下方两段文字分别承担状态提示和操作/对话摘要。#define UI_STATUS_POS_Y (4) #define UI_STATUS_HEIGHT (22) #define UI_FOCUS_POS_X (64) #define UI_FOCUS_POS_Y (36) #define UI_FOCUS_SIZE (112) #define UI_FOCUS_CORE_SIZE (90) #define UI_PROMPT_POS_Y (149) #define UI_CHAT_TXT_POS_Y (179) #define UI_CHAT_TXT_HEIGHT (49)这种布局不会在回答长文本时继续强行保留大焦点环。进入回答态后中心视觉让位给阅读区域只保留顶部状态和“正在回答”提示。小屏界面的优先级必须随任务改变而不是让所有组件永久同时可见。对象只创建一次状态变化通过隐藏与样式完成disp_create_ui在 LVGL 任务初始化时创建状态栏、焦点环、12 个环段、核心区、图像、5 个波形条、互动文字、回答页装饰、全屏文字和音量覆盖层。运行时不为每个 Agent 状态重复 new 一套对象而是使用LV_OBJ_FLAG_HIDDEN切换可见性并更新文本、颜色、尺寸。status_bar_obj lv_label_create(lv_scr_act()); s_status_divider_obj lv_obj_create(lv_scr_act()); s_focus_ring_obj lv_obj_create(lv_scr_act()); for (uint8_t i 0U; i ASSISTANT_RING_SEGMENT_COUNT; i) { s_focus_ring_segments[i] lv_obj_create(s_focus_ring_obj); } s_focus_core_obj lv_obj_create(lv_scr_act()); s_penguin_image_obj lv_image_create(s_focus_core_obj); for (uint8_t i 0U; i 5U; i) { s_wave_bars[i] lv_obj_create(s_focus_core_obj); }固定对象树减少频繁分配和销毁也让 LVGL 样式与定时器更容易管理。创建完成后代码默认隐藏互动区和焦点区先显示全屏启动文字“咕咕嘎嘎 / 正在醒来”待系统事件到达后再切入常态页面。五种视觉模式与 Agent 状态对齐当前视觉枚举包含空闲、连接、倾听、思考和回答五态。它们不是网络协议状态的完整复制而是 UI 需要表达的最小集合。监听态和思考态都可能来自AGENT_STATE_LISTENING再通过agent_is_waiting_response()区分语音采集尚在进行还是语音已上传、正在等待服务端。typedef enum { ASSISTANT_VISUAL_IDLE 0, ASSISTANT_VISUAL_CONNECTING, ASSISTANT_VISUAL_LISTENING, ASSISTANT_VISUAL_THINKING, ASSISTANT_VISUAL_SPEAKING, ASSISTANT_VISUAL_INVALID } assistant_visual_mode_t;每种模式设置不同的焦点背景、边框和前景色。空闲使用暖黄色连接偏紫倾听使用青色思考使用琥珀色回答使用绿色。12 段焦点环可以固定点亮或旋转五根波形条则按预设高度序列动画帮助用户不读文字也能分辨设备状态。状态文案来自真实分支而不是设计稿显示任务处理eDisp_Prompt_Update时读取 Agent 状态。空闲文案为“咕咕嘎嘎 / 按中键和我说话长按5秒进入配网”连接态显示“正在连接 / 网络连接中请稍候”监听态显示“我在听 / 请开始说话”等待返回时显示“让我想一想 / 语音已上传请稍候”回答态提示“正在回答”。assistant_visual_set_mode(waiting_response ? ASSISTANT_VISUAL_THINKING : ASSISTANT_VISUAL_LISTENING); lv_label_set_text( interact_txt_prompt, waiting_response ? 让我想一想 : 我在听); lv_label_set_text( interact_txt_chat, waiting_response ? 语音已上传请稍候 : 请开始说话);这段逻辑解决了“已经说完但界面还显示我在听”的语义错误。状态文案与颜色必须由同一个状态判断驱动否则用户会看到环在思考、文字却提示继续说话。OpenHarmony 构建只使用 16 行局部缓冲240×240 RGB565 全帧缓冲需要 115,200 字节。当前 OpenHarmony 路径把局部刷新缓冲设为 16 行即240 × 16 × 2 7,680字节非 OpenHarmony 分支使用 80 行、38,400 字节。代码在注释中明确说明缓冲越大刷新可能更快但 SRAM 压力也更高。#if SUPPORT_OHOS #define LVGL_PARTIAL_BUF_LINES (16) #else #define LVGL_PARTIAL_BUF_LINES (80) #endif static uint8_t disp_buffer[ DISP_WIDTH * LVGL_PARTIAL_BUF_LINES * 2];LVGL display 使用LV_DISPLAY_RENDER_MODE_PARTIAL注册该缓冲。局部缓冲不是“屏幕只有 16 行”而是 LVGL 分块绘制后通过 flush 回调送到 ST7789。文章给出精确字节数避免把帧缓冲、对象内存和外部 Flash 混在一起。LVGL tick 与对象更新必须留在显示任务当前代码专门注明不能在硬件定时器 ISR 中调用lv_tick_inc()否则会与LvglTask并发并破坏 LVGL 内部状态。实际循环在同一任务中根据内核 tick 推进 LVGL再调用lv_timer_handler()然后以它返回的下一定时器间隔等待显示消息。等待时间被限制在 120 ms。过大时动画和滚动发涩过小时任务空转、发热增加。显示事件通过g_disp_event_qid进入同一线程调用disp_update_ui因此其他任务只发枚举不直接操作 LVGL 对象。lv_tick_inc(xh_lvgl_elapsed_ms_since(lv_prev_kernel_tick)); uint32_t next_ms lv_timer_handler(); uint32_t qwait next_ms; if (qwait 1U) { qwait 1U; } if (qwait 20U) { qwait 20U; } ret osMessageQueueGet( g_disp_event_qid, (void *)msg_recv, NULL, qwait);回答页用 UTF-8 安全分页而不是按字节硬切动态回答先写入 512 字节 pending buffer。单页最多复制 144 字节页面推进步长为 108 字节相邻页因此保留一部分上下文每页停留 3.2 秒。这里的 144 和 108 是字节数不等于固定中文字数。UTF-8 中文通常占 3 字节Emoji 可能占 4 字节所以切页必须回退到字符起点。#define DISP_LVGL_TEXT_PENDING_MAX (512) #define DISP_SHOW_TEXT_LINE_LEN (108) #define DISP_SHOW_TEXT_MAX_LEN (144) #define DISP_SHOW_TEXT_DELAY_MS (3200)utf8_safe_rollback从目标位置向前跳过所有10xxxxxxcontinuation byte找到一个合法字符起点。第一页显示后创建 LVGL timer后续每次向前移动 108 字节再安全截取最多 144 字节并更新右下角当前页/总页数。最后一页还会完整停留一个周期再清理播放状态避免回答刚显示就返回首页。回答页把 144 像素高度让给正文回答布局的正文从 x16、y64 开始宽度为DISP_WIDTH - 28高度 144左侧有 3×144 的绿色强调条页码放在 y213。相较常态页 49 像素的摘要区回答页可以显示约五到六行 18 px 中文。顶部仍保留状态栏用户不会因为进入长文本页面失去网络、电量等信息。内置lv_font_Chinese_18_UI是兜底字体若 LittleFS 中的外部font.bin可用互动文字可切到流式字体。完整中文字库下载与替换属于第 08 篇范围本文只确认当前 UI 对字体缺失有回退路径不声称所有 Unicode 字形都在内置字体中。音量反馈使用覆盖层不挤压主状态收到eDisp_Volume_Update后显示任务先刷新状态栏再显示位于屏幕下部的音量覆盖层。覆盖层包含音量图标、116×11 的轨道、按百分比计算宽度的填充条和数值。它由定时器自动恢复原界面不需要重建整个对象树。填充最大宽度为 112当前值按112 × volume / 100计算音量为零时隐藏填充条。这个细节比只改状态栏图标更容易给用户及时反馈也不会永久占用回答区。从源码到实机还需要三类验收源码层已经确认 240×240 尺寸、对象层次、五种视觉模式、16 行局部缓冲、单任务 tick、120 ms 等待、512 字节 pending buffer、144/108 字节分页和 3.2 秒停留。最新 UI 包000_BURN_THIS_42_GUGUGAGA_DETAILED_UI_WS63_20260723_2040.fwpkg的大小和 SHA-256 也已在第 04 篇记录。但界面验收还应分三类静态布局检查边缘裁切、字体和页码动态检查焦点环、波形、音量覆盖层和分页节奏长时间运行检查任务占用、发热、队列堵塞和 LVGL fault。本轮没有重新烧录和拍摄设备因此本文结论是“当前 240×240 UI 实现已按源码复核”不是“今天完成了新包的全部实机视觉验收”。