251 lines
9.0 KiB
C
251 lines
9.0 KiB
C
#include "ai_chat_ui.h"
|
||
#include "lvgl.h"
|
||
#include "esp_lvgl_port.h"
|
||
#include "esp_log.h"
|
||
#include <string.h>
|
||
|
||
// 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;
|
||
}
|