#include "ai_chat_ui.h" #include "lvgl.h" #include "esp_lvgl_port.h" #include "esp_log.h" #include // lv_gif API 通过 lvgl.h 自动包含(LV_USE_GIF=y 时) static const char *TAG = "AI_CHAT_UI"; // 声明阿里巴巴普惠体 20px 4bpp(GB2312 简体中文+ASCII) LV_FONT_DECLARE(font_puhui_20_4); #if LV_USE_GIF // 需要访问 lv_gif_t 内部结构(暂停/恢复定时器) #include "extra/libs/gif/lv_gif.h" // 声明 GIF 表情资源(压缩优化,200x89) LV_IMG_DECLARE(emotion_angry_200_89); LV_IMG_DECLARE(emotion_blink1_200_89); LV_IMG_DECLARE(emotion_blink_fast_200_89); LV_IMG_DECLARE(emotion_blink_slow_200_89); LV_IMG_DECLARE(emotion_dizzy_200_89); LV_IMG_DECLARE(emotion_happy_200_89); LV_IMG_DECLARE(emotion_sad_200_89); LV_IMG_DECLARE(emotion_sleep_200_89); // 声明 GIF 图标资源(压缩优化,45x45) LV_IMG_DECLARE(icon_emotion_confused_45); LV_IMG_DECLARE(icon_emotion_sleep_45); LV_IMG_DECLARE(icon_emotion_thinking_45); #endif // 声明旧 PNG 表情资源(LV_USE_GIF 未启用时的回退) LV_IMG_DECLARE(ui_img_neutral_png); LV_IMG_DECLARE(ui_img_happy_png); LV_IMG_DECLARE(ui_img_sad_png); LV_IMG_DECLARE(ui_img_angry_png); LV_IMG_DECLARE(ui_img_crying_png); LV_IMG_DECLARE(ui_img_funny_png); LV_IMG_DECLARE(ui_img_laughing_png); // 屏幕和控件 static lv_obj_t *ai_screen = NULL; static lv_obj_t *status_label = NULL; static lv_obj_t *chat_label = NULL; #if LV_USE_GIF static lv_obj_t *gif_emotion = NULL; // GIF 表情对象 static lv_obj_t *gif_icon = NULL; // GIF 图标对象(叠加在表情右上角) static bool gif_animation_paused = false; #else static lv_obj_t *emoji_img = NULL; // 旧 PNG 表情 #endif // 背景色(纯黑,与 GIF 背景一致避免色差) #define BG_COLOR 0x000000 // 表情→GIF 映射表 typedef struct { const char *name; const lv_img_dsc_t *emotion_gif; const lv_img_dsc_t *icon_gif; // NULL 表示无叠加图标 } emotion_gif_entry_t; #if LV_USE_GIF static const emotion_gif_entry_t emotion_gif_map[] = { {"neutral", &emotion_blink_slow_200_89, NULL}, {"happy", &emotion_happy_200_89, NULL}, {"laughing", &emotion_happy_200_89, NULL}, {"funny", &emotion_happy_200_89, NULL}, {"loving", &emotion_happy_200_89, NULL}, {"relaxed", &emotion_happy_200_89, NULL}, {"delicious", &emotion_happy_200_89, NULL}, {"kissy", &emotion_happy_200_89, NULL}, {"confident", &emotion_happy_200_89, NULL}, {"sad", &emotion_sad_200_89, NULL}, {"crying", &emotion_sad_200_89, NULL}, {"angry", &emotion_angry_200_89, NULL}, {"surprised", &emotion_blink_fast_200_89, NULL}, {"shocked", &emotion_blink_fast_200_89, NULL}, {"silly", &emotion_blink_fast_200_89, NULL}, {"embarrassed", &emotion_blink_fast_200_89, &icon_emotion_thinking_45}, {"thinking", &emotion_blink_fast_200_89, &icon_emotion_thinking_45}, {"confused", &emotion_blink_fast_200_89, &icon_emotion_confused_45}, {"curious", &emotion_blink_fast_200_89, &icon_emotion_confused_45}, {"dizzy", &emotion_dizzy_200_89, NULL}, {"blink", &emotion_blink1_200_89, NULL}, {"sleepy", &emotion_sleep_200_89, &icon_emotion_sleep_45}, }; #define EMOTION_GIF_MAP_SIZE (sizeof(emotion_gif_map) / sizeof(emotion_gif_map[0])) #endif void ai_chat_screen_init(void) { // 创建 AI 对话屏幕 ai_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(ai_screen, lv_color_hex(BG_COLOR), 0); lv_obj_set_style_bg_opa(ai_screen, 255, 0); lv_obj_clear_flag(ai_screen, LV_OBJ_FLAG_SCROLLABLE); #if LV_USE_GIF // GIF 表情(屏幕正中央,200x89) gif_emotion = lv_gif_create(ai_screen); lv_gif_set_src(gif_emotion, &emotion_blink_slow_200_89); lv_obj_align(gif_emotion, LV_ALIGN_CENTER, 0, 0); // 降低 GIF 定时器频率(10ms→20ms),平衡动画流畅度与 CPU 占用 lv_gif_t *gifobj = (lv_gif_t *)gif_emotion; lv_timer_set_period(gifobj->timer, 20); // GIF 图标(表情上方居中,45x45) // 表情高89,顶边y=-44.5,icon高45,中心再上移几像素避免重叠 gif_icon = lv_gif_create(ai_screen); lv_obj_align(gif_icon, LV_ALIGN_CENTER, 0, -70); lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN); #else // 旧 PNG 表情(回退方案) emoji_img = lv_img_create(ai_screen); lv_img_set_src(emoji_img, &ui_img_neutral_png); lv_obj_align(emoji_img, LV_ALIGN_CENTER, 0, 0); #endif // 状态文本(暂时隐藏,GIF 模式下不需要文字) status_label = lv_label_create(ai_screen); lv_obj_set_style_text_font(status_label, &font_puhui_20_4, 0); lv_obj_set_style_text_color(status_label, lv_color_white(), 0); lv_obj_set_width(status_label, 300); lv_obj_set_style_text_align(status_label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_text(status_label, ""); lv_obj_align(status_label, LV_ALIGN_CENTER, 0, 90); #if LV_USE_GIF lv_obj_add_flag(status_label, LV_OBJ_FLAG_HIDDEN); #endif // 聊天消息文本(暂时隐藏,不显示字幕) chat_label = lv_label_create(ai_screen); lv_obj_set_style_text_font(chat_label, &font_puhui_20_4, 0); lv_obj_set_style_text_color(chat_label, lv_color_hex(0xAAAAAA), 0); lv_obj_set_width(chat_label, 300); lv_obj_set_style_text_align(chat_label, LV_TEXT_ALIGN_LEFT, 0); lv_label_set_text(chat_label, ""); lv_obj_align(chat_label, LV_ALIGN_CENTER, 0, 50); lv_label_set_long_mode(chat_label, LV_LABEL_LONG_WRAP); lv_obj_add_flag(chat_label, LV_OBJ_FLAG_HIDDEN); // 加载屏幕 lv_disp_load_scr(ai_screen); } void ai_chat_set_status(const char* status) { if (!status_label) return; #if LV_USE_GIF // GIF 模式下隐藏状态文字,仅记录日志 ESP_LOGD(TAG, "状态: %s(GIF模式不显示)", status); (void)status; #else lvgl_port_lock(50); lv_label_set_text(status_label, status); lvgl_port_unlock(); #endif } void ai_chat_resume_animation(void) { #if LV_USE_GIF if (!gif_emotion || !gif_animation_paused) return; if (!lvgl_port_lock(200)) return; lv_gif_t *gifobj = (lv_gif_t *)gif_emotion; lv_timer_resume(gifobj->timer); gif_animation_paused = false; ESP_LOGI(TAG, "GIF动画已恢复播放"); lvgl_port_unlock(); #endif } void ai_chat_set_emotion(const char* emotion) { if (!emotion) return; #if LV_USE_GIF if (!gif_emotion) return; if (!lvgl_port_lock(200)) { ESP_LOGW(TAG, "LVGL锁超时,跳过表情切换: %s", emotion); return; } // 查找映射表 const emotion_gif_entry_t *entry = NULL; for (int i = 0; i < EMOTION_GIF_MAP_SIZE; i++) { if (strcmp(emotion, emotion_gif_map[i].name) == 0) { entry = &emotion_gif_map[i]; break; } } // 未找到映射,默认使用 neutral if (!entry) { ESP_LOGW(TAG, "未映射的GIF表情: %s, 使用 neutral", emotion); entry = &emotion_gif_map[0]; // neutral } // 切换表情 GIF(lv_gif_set_src 内部已自动启动播放) lv_gif_set_src(gif_emotion, entry->emotion_gif); // set_src 内部会重建 10ms 定时器,重新设置为 50ms 降低 CPU 占用 lv_gif_t *gifobj = (lv_gif_t *)gif_emotion; lv_timer_set_period(gifobj->timer, 20); gif_animation_paused = false; // 处理叠加图标 if (entry->icon_gif) { lv_gif_set_src(gif_icon, entry->icon_gif); lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon; lv_timer_set_period(icon_gifobj->timer, 20); lv_obj_clear_flag(gif_icon, LV_OBJ_FLAG_HIDDEN); } else { // 隐藏图标时暂停其定时器,避免空跑浪费 CPU lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon; if (icon_gifobj->gif) { lv_timer_pause(icon_gifobj->timer); } lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN); } ESP_LOGI(TAG, "GIF表情切换: %s%s", emotion, entry->icon_gif ? " +图标" : ""); lvgl_port_unlock(); #else // 旧 PNG 回退逻辑 if (!emoji_img) return; lvgl_port_lock(50); const lv_img_dsc_t *img = &ui_img_neutral_png; if (strcmp(emotion, "neutral") == 0) img = &ui_img_neutral_png; else if (strcmp(emotion, "happy") == 0) img = &ui_img_happy_png; else if (strcmp(emotion, "sad") == 0) img = &ui_img_sad_png; else if (strcmp(emotion, "angry") == 0) img = &ui_img_angry_png; else if (strcmp(emotion, "surprised") == 0) img = &ui_img_funny_png; else if (strcmp(emotion, "crying") == 0) img = &ui_img_crying_png; else if (strcmp(emotion, "laughing") == 0) img = &ui_img_laughing_png; lv_img_set_src(emoji_img, img); lvgl_port_unlock(); #endif } void ai_chat_set_chat_message(const char* role, const char* content) { if (!chat_label) return; // 字幕暂时隐藏,不更新内容 // 后续恢复时去掉 return 和 ai_chat_screen_init 中的 LV_OBJ_FLAG_HIDDEN (void)role; (void)content; }