feat: 新增ScreenChar角色动画界面 + 启用GIF解码器
- 新增ui_ScreenChar:LVGL基元绘制动漫角色(金黄头发/脸/眼/嘴/身体/胳膊) - 序列帧动画:BOOT按键触发眨眼+说话动画,再按停止 - ScreenImg上滑导航至ScreenChar(保留原Home路径可切换) - BOOT按键逻辑:ScreenChar界面切换动画,其他界面返回Home - 启用CONFIG_LV_USE_GIF=y支持GIF动图显示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9223fd5a7d
commit
6711a24a68
@ -15,6 +15,7 @@ idf_component_register(
|
|||||||
"./ui/screens/ui_ScreenHome.c"
|
"./ui/screens/ui_ScreenHome.c"
|
||||||
"./ui/screens/ui_ScreenSet.c"
|
"./ui/screens/ui_ScreenSet.c"
|
||||||
"./ui/screens/ui_ScreenImg.c"
|
"./ui/screens/ui_ScreenImg.c"
|
||||||
|
"./ui/screens/ui_ScreenChar.c"
|
||||||
"./ui/ui_helpers.c"
|
"./ui/ui_helpers.c"
|
||||||
"./ui/images/ui_img_s1_png.c"
|
"./ui/images/ui_img_s1_png.c"
|
||||||
"./ui/images/ui_img_s6_png.c"
|
"./ui/images/ui_img_s6_png.c"
|
||||||
|
|||||||
21
main/main.c
21
main/main.c
@ -22,6 +22,7 @@
|
|||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "ui/screens/ui_ScreenSet.h"
|
#include "ui/screens/ui_ScreenSet.h"
|
||||||
#include "ui/screens/ui_ScreenImg.h"
|
#include "ui/screens/ui_ScreenImg.h"
|
||||||
|
#include "ui/screens/ui_ScreenChar.h"
|
||||||
|
|
||||||
|
|
||||||
// #include "axis.h"
|
// #include "axis.h"
|
||||||
@ -71,11 +72,27 @@ void boot_btn_handler(int gpio_num, void *usr_data) {
|
|||||||
ESP_LOGI("BTN_HANDLER", "BOOT按键:低功耗模式,仅唤醒屏幕");
|
ESP_LOGI("BTN_HANDLER", "BOOT按键:低功耗模式,仅唤醒屏幕");
|
||||||
sleep_mgr_notify_activity(); // 唤醒屏幕,恢复亮度
|
sleep_mgr_notify_activity(); // 唤醒屏幕,恢复亮度
|
||||||
} else {
|
} else {
|
||||||
// 正常模式下:返回ScreenHome界面
|
// 正常模式下的按键处理
|
||||||
|
lv_obj_t *current_screen = lv_scr_act();
|
||||||
|
|
||||||
|
// 在ScreenChar界面:切换角色动画(不返回Home)
|
||||||
|
if (current_screen == ui_ScreenChar) {
|
||||||
|
sleep_mgr_notify_activity();
|
||||||
|
if (char_anim_is_playing()) {
|
||||||
|
char_anim_stop();
|
||||||
|
ESP_LOGI("BTN_HANDLER", "BOOT按键:停止角色动画");
|
||||||
|
} else {
|
||||||
|
char_anim_start();
|
||||||
|
ESP_LOGI("BTN_HANDLER", "BOOT按键:启动角色动画");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 其他界面:返回ScreenHome
|
||||||
ESP_LOGI("BTN_HANDLER", "BOOT按键:正常模式,返回ScreenHome");
|
ESP_LOGI("BTN_HANDLER", "BOOT按键:正常模式,返回ScreenHome");
|
||||||
|
|
||||||
// 检查当前是否在ScreenImg界面,如果是则先隐藏ContainerDle
|
// 检查当前是否在ScreenImg界面,如果是则先隐藏ContainerDle
|
||||||
lv_obj_t *current_screen = lv_scr_act();
|
|
||||||
if (current_screen == ui_ScreenImg) {
|
if (current_screen == ui_ScreenImg) {
|
||||||
ui_ScreenImg_hide_delete_container();
|
ui_ScreenImg_hide_delete_container();
|
||||||
ESP_LOGI("BTN_HANDLER", "从ScreenImg离开,已隐藏ContainerDle");
|
ESP_LOGI("BTN_HANDLER", "从ScreenImg离开,已隐藏ContainerDle");
|
||||||
|
|||||||
381
main/ui/screens/ui_ScreenChar.c
Normal file
381
main/ui/screens/ui_ScreenChar.c
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
// 动漫角色界面 - LVGL图形基元绘制 + 序列帧动画
|
||||||
|
// 默认静态显示,BOOT按键触发眨眼+说话动画
|
||||||
|
// 零图片资源,纯代码绘制
|
||||||
|
|
||||||
|
#include "../ui.h"
|
||||||
|
#include "ui_ScreenChar.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char *TAG = "CHAR";
|
||||||
|
|
||||||
|
lv_obj_t *ui_ScreenChar = NULL;
|
||||||
|
|
||||||
|
// === 角色配色 ===
|
||||||
|
#define COL_SKIN 0xFFD5B8 // 肤色
|
||||||
|
#define COL_HAIR 0xFFD54F // 金黄色头发
|
||||||
|
#define COL_EYE_WHITE 0xFFFFFF // 眼白
|
||||||
|
#define COL_PUPIL 0x1565C0 // 蓝色瞳孔
|
||||||
|
#define COL_MOUTH 0xE57373 // 粉红嘴巴
|
||||||
|
#define COL_BLUSH 0xFFAB91 // 腮红
|
||||||
|
#define COL_SHIRT 0x42A5F5 // 蓝色衣服
|
||||||
|
#define COL_COLLAR 0xFFFFFF // 白色衣领
|
||||||
|
#define COL_BG 0x1A1A2E // 深蓝背景
|
||||||
|
|
||||||
|
// === 角色布局坐标(基于360x360屏幕)===
|
||||||
|
// 角色居中偏上,留出底部空间给提示文字
|
||||||
|
#define CHAR_CX 180 // 角色中心X
|
||||||
|
#define FACE_CY 120 // 脸部中心Y
|
||||||
|
#define FACE_R 72 // 脸部半径
|
||||||
|
|
||||||
|
// 眼睛
|
||||||
|
#define EYE_Y 110 // 眼睛Y
|
||||||
|
#define EYE_L_X 152 // 左眼X
|
||||||
|
#define EYE_R_X 208 // 右眼X
|
||||||
|
#define EYE_W 26 // 眼白宽度
|
||||||
|
#define EYE_H 26 // 眼白高度(眨眼时变小)
|
||||||
|
#define PUPIL_R 7 // 瞳孔半径
|
||||||
|
|
||||||
|
// 嘴巴
|
||||||
|
#define MOUTH_Y 155 // 嘴巴Y
|
||||||
|
#define MOUTH_W 24 // 嘴巴宽度
|
||||||
|
#define MOUTH_H_IDLE 6 // 静态嘴巴高度(微笑线)
|
||||||
|
#define MOUTH_H_TALK 18 // 说话时嘴巴高度
|
||||||
|
|
||||||
|
// 身体
|
||||||
|
#define NECK_Y 185 // 脖子顶部Y
|
||||||
|
#define NECK_W 28 // 脖子宽度
|
||||||
|
#define NECK_H 18 // 脖子高度
|
||||||
|
#define BODY_Y 203 // 身体顶部Y
|
||||||
|
#define BODY_W 160 // 肩宽
|
||||||
|
#define BODY_H 160 // 身体高度(延伸到屏幕外)
|
||||||
|
#define SHOULDER_R 25 // 肩部圆角
|
||||||
|
|
||||||
|
// 胳膊
|
||||||
|
#define ARM_W 28 // 胳膊宽度
|
||||||
|
#define ARM_H 100 // 胳膊长度
|
||||||
|
#define ARM_Y 215 // 胳膊顶部Y(肩膀下方)
|
||||||
|
#define ARM_L_X (CHAR_CX - BODY_W / 2 - ARM_W / 2 + 8) // 左胳膊X
|
||||||
|
#define ARM_R_X (CHAR_CX + BODY_W / 2 + ARM_W / 2 - 8) // 右胳膊X
|
||||||
|
#define HAND_R 16 // 手掌半径
|
||||||
|
|
||||||
|
// === 角色控件指针 ===
|
||||||
|
static lv_obj_t *hair_obj = NULL; // 头发
|
||||||
|
static lv_obj_t *face_obj = NULL; // 脸
|
||||||
|
static lv_obj_t *eye_l_white = NULL; // 左眼白
|
||||||
|
static lv_obj_t *eye_r_white = NULL; // 右眼白
|
||||||
|
static lv_obj_t *pupil_l = NULL; // 左瞳孔
|
||||||
|
static lv_obj_t *pupil_r = NULL; // 右瞳孔
|
||||||
|
static lv_obj_t *blush_l = NULL; // 左腮红
|
||||||
|
static lv_obj_t *blush_r = NULL; // 右腮红
|
||||||
|
static lv_obj_t *mouth_obj = NULL; // 嘴巴
|
||||||
|
static lv_obj_t *neck_obj = NULL; // 脖子
|
||||||
|
static lv_obj_t *body_obj = NULL; // 身体/衣服
|
||||||
|
static lv_obj_t *collar_l = NULL; // 左衣领
|
||||||
|
static lv_obj_t *collar_r = NULL; // 右衣领
|
||||||
|
static lv_obj_t *arm_l = NULL; // 左胳膊
|
||||||
|
static lv_obj_t *arm_r = NULL; // 右胳膊
|
||||||
|
static lv_obj_t *hand_l = NULL; // 左手
|
||||||
|
static lv_obj_t *hand_r = NULL; // 右手
|
||||||
|
static lv_obj_t *hint_label = NULL; // 提示文字
|
||||||
|
|
||||||
|
// === 动画状态 ===
|
||||||
|
static lv_timer_t *blink_timer = NULL; // 眨眼定时器
|
||||||
|
static lv_timer_t *mouth_timer = NULL; // 说话定时器
|
||||||
|
static bool anim_playing = false; // 动画是否在播放
|
||||||
|
|
||||||
|
// 眨眼动画状态
|
||||||
|
static uint8_t blink_phase = 0; // 0=睁眼 1=半闭 2=闭眼 3=半闭
|
||||||
|
static uint32_t next_blink_ms = 0; // 下次眨眼时间
|
||||||
|
|
||||||
|
// 说话动画状态
|
||||||
|
static uint8_t mouth_phase = 0; // 嘴巴帧索引
|
||||||
|
static const lv_coord_t mouth_heights[] = {6, 12, 18, 14, 8, 14, 18, 12}; // 嘴巴高度序列
|
||||||
|
#define MOUTH_FRAMES (sizeof(mouth_heights) / sizeof(mouth_heights[0]))
|
||||||
|
|
||||||
|
// === 创建圆形对象的辅助函数 ===
|
||||||
|
static lv_obj_t *create_circle(lv_obj_t *parent, lv_coord_t cx, lv_coord_t cy,
|
||||||
|
lv_coord_t w, lv_coord_t h, uint32_t color) {
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent);
|
||||||
|
lv_obj_remove_style_all(obj);
|
||||||
|
lv_obj_set_size(obj, w, h);
|
||||||
|
lv_obj_set_pos(obj, cx - w / 2, cy - h / 2);
|
||||||
|
lv_obj_set_style_bg_color(obj, lv_color_hex(color), 0);
|
||||||
|
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, 0);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建矩形/圆角矩形
|
||||||
|
static lv_obj_t *create_rect(lv_obj_t *parent, lv_coord_t x, lv_coord_t y,
|
||||||
|
lv_coord_t w, lv_coord_t h, uint32_t color, lv_coord_t radius) {
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent);
|
||||||
|
lv_obj_remove_style_all(obj);
|
||||||
|
lv_obj_set_size(obj, w, h);
|
||||||
|
lv_obj_set_pos(obj, x, y);
|
||||||
|
lv_obj_set_style_bg_color(obj, lv_color_hex(color), 0);
|
||||||
|
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(obj, radius, 0);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, 0);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 绘制角色 ===
|
||||||
|
static void draw_character(lv_obj_t *screen) {
|
||||||
|
// 1. 胳膊(最底层,被身体部分遮挡)
|
||||||
|
// 左胳膊(衣袖)
|
||||||
|
arm_l = create_rect(screen, ARM_L_X - ARM_W / 2, ARM_Y,
|
||||||
|
ARM_W, ARM_H, COL_SHIRT, ARM_W / 2);
|
||||||
|
// 右胳膊(衣袖)
|
||||||
|
arm_r = create_rect(screen, ARM_R_X - ARM_W / 2, ARM_Y,
|
||||||
|
ARM_W, ARM_H, COL_SHIRT, ARM_W / 2);
|
||||||
|
// 左手(肤色圆形)
|
||||||
|
hand_l = create_circle(screen, ARM_L_X, ARM_Y + ARM_H - 5,
|
||||||
|
HAND_R * 2, HAND_R * 2, COL_SKIN);
|
||||||
|
// 右手(肤色圆形)
|
||||||
|
hand_r = create_circle(screen, ARM_R_X, ARM_Y + ARM_H - 5,
|
||||||
|
HAND_R * 2, HAND_R * 2, COL_SKIN);
|
||||||
|
|
||||||
|
// 2. 身体/衣服
|
||||||
|
body_obj = create_rect(screen,
|
||||||
|
CHAR_CX - BODY_W / 2, BODY_Y,
|
||||||
|
BODY_W, BODY_H, COL_SHIRT, SHOULDER_R);
|
||||||
|
|
||||||
|
// 3. 衣领(V形,用两个斜矩形模拟)
|
||||||
|
collar_l = create_rect(screen,
|
||||||
|
CHAR_CX - 18, BODY_Y,
|
||||||
|
20, 28, COL_COLLAR, 3);
|
||||||
|
lv_obj_set_style_transform_angle(collar_l, 200, 0); // 旋转20度
|
||||||
|
|
||||||
|
collar_r = create_rect(screen,
|
||||||
|
CHAR_CX - 2, BODY_Y,
|
||||||
|
20, 28, COL_COLLAR, 3);
|
||||||
|
lv_obj_set_style_transform_angle(collar_r, -200, 0); // 旋转-20度
|
||||||
|
|
||||||
|
// 3. 脖子
|
||||||
|
neck_obj = create_rect(screen,
|
||||||
|
CHAR_CX - NECK_W / 2, NECK_Y,
|
||||||
|
NECK_W, NECK_H, COL_SKIN, 5);
|
||||||
|
|
||||||
|
// 4. 头发(脸后面的半圆,稍大于脸)
|
||||||
|
hair_obj = create_circle(screen, CHAR_CX, FACE_CY - 5,
|
||||||
|
FACE_R * 2 + 16, FACE_R * 2 + 16, COL_HAIR);
|
||||||
|
|
||||||
|
// 头发刘海(额头上方的矩形)
|
||||||
|
create_rect(screen,
|
||||||
|
CHAR_CX - FACE_R - 2, FACE_CY - FACE_R - 10,
|
||||||
|
FACE_R * 2 + 4, 35, COL_HAIR, 8);
|
||||||
|
|
||||||
|
// 5. 脸(圆形)
|
||||||
|
face_obj = create_circle(screen, CHAR_CX, FACE_CY,
|
||||||
|
FACE_R * 2, FACE_R * 2, COL_SKIN);
|
||||||
|
|
||||||
|
// 6. 腮红
|
||||||
|
blush_l = create_circle(screen, EYE_L_X - 5, EYE_Y + 18, 22, 12, COL_BLUSH);
|
||||||
|
lv_obj_set_style_bg_opa(blush_l, LV_OPA_60, 0);
|
||||||
|
blush_r = create_circle(screen, EYE_R_X + 5, EYE_Y + 18, 22, 12, COL_BLUSH);
|
||||||
|
lv_obj_set_style_bg_opa(blush_r, LV_OPA_60, 0);
|
||||||
|
|
||||||
|
// 7. 眼睛(眼白 + 瞳孔)
|
||||||
|
eye_l_white = create_circle(screen, EYE_L_X, EYE_Y, EYE_W, EYE_H, COL_EYE_WHITE);
|
||||||
|
eye_r_white = create_circle(screen, EYE_R_X, EYE_Y, EYE_W, EYE_H, COL_EYE_WHITE);
|
||||||
|
|
||||||
|
pupil_l = create_circle(screen, EYE_L_X, EYE_Y + 2, PUPIL_R * 2, PUPIL_R * 2, COL_PUPIL);
|
||||||
|
pupil_r = create_circle(screen, EYE_R_X, EYE_Y + 2, PUPIL_R * 2, PUPIL_R * 2, COL_PUPIL);
|
||||||
|
|
||||||
|
// 眼睛高光(小白点)
|
||||||
|
create_circle(screen, EYE_L_X + 3, EYE_Y - 1, 5, 5, COL_EYE_WHITE);
|
||||||
|
create_circle(screen, EYE_R_X + 3, EYE_Y - 1, 5, 5, COL_EYE_WHITE);
|
||||||
|
|
||||||
|
// 8. 眉毛(深棕色,黄毛配深眉更协调)
|
||||||
|
lv_obj_t *brow_l = create_rect(screen, EYE_L_X - 14, EYE_Y - 20, 22, 4, 0x5D4037, 2);
|
||||||
|
lv_obj_set_style_transform_angle(brow_l, -50, 0); // 微微倾斜
|
||||||
|
lv_obj_t *brow_r = create_rect(screen, EYE_R_X - 8, EYE_Y - 20, 22, 4, 0x5D4037, 2);
|
||||||
|
lv_obj_set_style_transform_angle(brow_r, 50, 0);
|
||||||
|
|
||||||
|
// 9. 鼻子(小三角,用一个很小的圆角矩形)
|
||||||
|
create_circle(screen, CHAR_CX, FACE_CY + 12, 6, 6, 0xFFBCA0);
|
||||||
|
|
||||||
|
// 10. 嘴巴(椭圆形,高度可变实现说话效果)
|
||||||
|
mouth_obj = create_circle(screen, CHAR_CX, MOUTH_Y,
|
||||||
|
MOUTH_W, MOUTH_H_IDLE, COL_MOUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 眨眼动画回调 ===
|
||||||
|
static void blink_timer_cb(lv_timer_t *t) {
|
||||||
|
if (!anim_playing || !eye_l_white || !eye_r_white) return;
|
||||||
|
|
||||||
|
uint32_t now = lv_tick_get();
|
||||||
|
|
||||||
|
if (blink_phase == 0) {
|
||||||
|
// 睁眼状态,等待下次眨眼
|
||||||
|
if (now < next_blink_ms) return;
|
||||||
|
blink_phase = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 眨眼帧序列:睁眼26 → 半闭12 → 闭眼3 → 半闭12 → 睁眼26
|
||||||
|
static const lv_coord_t eye_h[] = {12, 3, 12, EYE_H};
|
||||||
|
lv_coord_t h = eye_h[blink_phase - 1];
|
||||||
|
|
||||||
|
// 更新眼睛高度(模拟眨眼)
|
||||||
|
lv_obj_set_height(eye_l_white, h);
|
||||||
|
lv_obj_set_y(eye_l_white, EYE_Y - h / 2);
|
||||||
|
lv_obj_set_height(eye_r_white, h);
|
||||||
|
lv_obj_set_y(eye_r_white, EYE_Y - h / 2);
|
||||||
|
|
||||||
|
// 瞳孔跟随(闭眼时隐藏)
|
||||||
|
if (h <= 5) {
|
||||||
|
lv_obj_add_flag(pupil_l, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_add_flag(pupil_r, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_flag(pupil_l, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_clear_flag(pupil_r, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
blink_phase++;
|
||||||
|
if (blink_phase > 4) {
|
||||||
|
blink_phase = 0;
|
||||||
|
// 随机间隔2-4秒后下次眨眼
|
||||||
|
next_blink_ms = now + 2000 + (lv_tick_get() % 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 说话动画回调 ===
|
||||||
|
static void mouth_timer_cb(lv_timer_t *t) {
|
||||||
|
if (!anim_playing || !mouth_obj) return;
|
||||||
|
|
||||||
|
lv_coord_t h = mouth_heights[mouth_phase];
|
||||||
|
lv_obj_set_height(mouth_obj, h);
|
||||||
|
lv_obj_set_y(mouth_obj, MOUTH_Y - h / 2);
|
||||||
|
|
||||||
|
mouth_phase = (mouth_phase + 1) % MOUTH_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 手势回调 ===
|
||||||
|
static void ui_event_ScreenChar(lv_event_t *e) {
|
||||||
|
lv_event_code_t code = lv_event_get_code(e);
|
||||||
|
if (code == LV_EVENT_GESTURE) {
|
||||||
|
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
|
||||||
|
// 下滑返回ScreenImg
|
||||||
|
if (dir == LV_DIR_BOTTOM) {
|
||||||
|
lv_indev_wait_release(lv_indev_get_act());
|
||||||
|
char_anim_stop();
|
||||||
|
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
|
||||||
|
}
|
||||||
|
// 左滑/右滑返回Home
|
||||||
|
if (dir == LV_DIR_LEFT || dir == LV_DIR_RIGHT) {
|
||||||
|
lv_indev_wait_release(lv_indev_get_act());
|
||||||
|
char_anim_stop();
|
||||||
|
_ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 界面初始化 ===
|
||||||
|
void ui_ScreenChar_screen_init(void) {
|
||||||
|
ui_ScreenChar = lv_obj_create(NULL);
|
||||||
|
lv_obj_clear_flag(ui_ScreenChar, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_bg_color(ui_ScreenChar, lv_color_hex(COL_BG), LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_bg_opa(ui_ScreenChar, 255, LV_PART_MAIN);
|
||||||
|
|
||||||
|
// 绘制角色
|
||||||
|
draw_character(ui_ScreenChar);
|
||||||
|
|
||||||
|
// 提示标签
|
||||||
|
hint_label = lv_label_create(ui_ScreenChar);
|
||||||
|
lv_label_set_text(hint_label, "BOOT: Talk");
|
||||||
|
lv_obj_set_align(hint_label, LV_ALIGN_BOTTOM_MID);
|
||||||
|
lv_obj_set_y(hint_label, -15);
|
||||||
|
lv_obj_set_style_text_color(hint_label, lv_color_hex(0x666688), LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_text_font(hint_label, &lv_font_montserrat_14, LV_PART_MAIN);
|
||||||
|
|
||||||
|
// 手势:下滑返回Img,左右滑返回Home
|
||||||
|
lv_obj_add_flag(ui_ScreenChar, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_flag(ui_ScreenChar, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_event_cb(ui_ScreenChar, ui_event_ScreenChar, LV_EVENT_ALL, NULL);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "ScreenChar界面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_ScreenChar_screen_destroy(void) {
|
||||||
|
char_anim_stop();
|
||||||
|
if (ui_ScreenChar) lv_obj_del(ui_ScreenChar);
|
||||||
|
ui_ScreenChar = NULL;
|
||||||
|
// 所有子控件随屏幕一起销毁,只需清空指针
|
||||||
|
face_obj = hair_obj = NULL;
|
||||||
|
eye_l_white = eye_r_white = NULL;
|
||||||
|
pupil_l = pupil_r = NULL;
|
||||||
|
blush_l = blush_r = NULL;
|
||||||
|
mouth_obj = neck_obj = body_obj = NULL;
|
||||||
|
collar_l = collar_r = NULL;
|
||||||
|
arm_l = arm_r = hand_l = hand_r = NULL;
|
||||||
|
hint_label = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 动画控制接口 ===
|
||||||
|
|
||||||
|
void char_anim_start(void) {
|
||||||
|
if (anim_playing) return;
|
||||||
|
anim_playing = true;
|
||||||
|
blink_phase = 0;
|
||||||
|
mouth_phase = 0;
|
||||||
|
next_blink_ms = lv_tick_get() + 500; // 0.5秒后第一次眨眼
|
||||||
|
|
||||||
|
// 眨眼定时器:50ms间隔(眨眼帧切换速度)
|
||||||
|
blink_timer = lv_timer_create(blink_timer_cb, 50, NULL);
|
||||||
|
// 说话定时器:80ms间隔(嘴巴变化速度)
|
||||||
|
mouth_timer = lv_timer_create(mouth_timer_cb, 80, NULL);
|
||||||
|
|
||||||
|
// 更新提示
|
||||||
|
if (hint_label) {
|
||||||
|
lv_label_set_text(hint_label, "Speaking...");
|
||||||
|
lv_obj_set_style_text_color(hint_label, lv_color_hex(0x42A5F5), LV_PART_MAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "角色动画启动(序列帧)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void char_anim_stop(void) {
|
||||||
|
if (!anim_playing) return;
|
||||||
|
anim_playing = false;
|
||||||
|
|
||||||
|
// 删除定时器
|
||||||
|
if (blink_timer) {
|
||||||
|
lv_timer_del(blink_timer);
|
||||||
|
blink_timer = NULL;
|
||||||
|
}
|
||||||
|
if (mouth_timer) {
|
||||||
|
lv_timer_del(mouth_timer);
|
||||||
|
mouth_timer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复静态状态
|
||||||
|
if (eye_l_white) {
|
||||||
|
lv_obj_set_height(eye_l_white, EYE_H);
|
||||||
|
lv_obj_set_y(eye_l_white, EYE_Y - EYE_H / 2);
|
||||||
|
}
|
||||||
|
if (eye_r_white) {
|
||||||
|
lv_obj_set_height(eye_r_white, EYE_H);
|
||||||
|
lv_obj_set_y(eye_r_white, EYE_Y - EYE_H / 2);
|
||||||
|
}
|
||||||
|
if (pupil_l) lv_obj_clear_flag(pupil_l, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (pupil_r) lv_obj_clear_flag(pupil_r, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (mouth_obj) {
|
||||||
|
lv_obj_set_height(mouth_obj, MOUTH_H_IDLE);
|
||||||
|
lv_obj_set_y(mouth_obj, MOUTH_Y - MOUTH_H_IDLE / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复提示
|
||||||
|
if (hint_label) {
|
||||||
|
lv_label_set_text(hint_label, "BOOT: Talk");
|
||||||
|
lv_obj_set_style_text_color(hint_label, lv_color_hex(0x666688), LV_PART_MAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "角色动画停止");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool char_anim_is_playing(void) {
|
||||||
|
return anim_playing;
|
||||||
|
}
|
||||||
25
main/ui/screens/ui_ScreenChar.h
Normal file
25
main/ui/screens/ui_ScreenChar.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 动漫角色界面 - LVGL图形基元绘制 + 序列帧动画
|
||||||
|
#ifndef UI_SCREENCHAR_H
|
||||||
|
#define UI_SCREENCHAR_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
// SCREEN: ui_ScreenChar
|
||||||
|
extern void ui_ScreenChar_screen_init(void);
|
||||||
|
extern void ui_ScreenChar_screen_destroy(void);
|
||||||
|
extern lv_obj_t *ui_ScreenChar;
|
||||||
|
|
||||||
|
// 角色动画控制(BOOT按键触发)
|
||||||
|
void char_anim_start(void); // 开始眨眼+说话动画
|
||||||
|
void char_anim_stop(void); // 停止动画,恢复静态
|
||||||
|
bool char_anim_is_playing(void); // 查询动画是否在播放
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /*extern "C"*/
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -109,7 +109,10 @@ lv_indev_wait_release(lv_indev_get_act());
|
|||||||
pages_cleanup_gif();
|
pages_cleanup_gif();
|
||||||
#endif
|
#endif
|
||||||
ui_ScreenImg_hide_delete_container();
|
ui_ScreenImg_hide_delete_container();
|
||||||
_ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
|
// 上滑进入角色动画界面
|
||||||
|
_ui_screen_change( &ui_ScreenChar, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenChar_screen_init);
|
||||||
|
// 原有上滑返回Home(如需切换回来,注释上面一行,取消下面一行注释)
|
||||||
|
// _ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init);
|
||||||
}
|
}
|
||||||
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
|
if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) {
|
||||||
lv_indev_wait_release(lv_indev_get_act());
|
lv_indev_wait_release(lv_indev_get_act());
|
||||||
|
|||||||
@ -19,6 +19,7 @@ extern "C" {
|
|||||||
#include "screens/ui_ScreenHome.h"
|
#include "screens/ui_ScreenHome.h"
|
||||||
#include "screens/ui_ScreenSet.h"
|
#include "screens/ui_ScreenSet.h"
|
||||||
#include "screens/ui_ScreenImg.h"
|
#include "screens/ui_ScreenImg.h"
|
||||||
|
#include "screens/ui_ScreenChar.h"
|
||||||
|
|
||||||
///////////////////// VARIABLES ////////////////////
|
///////////////////// VARIABLES ////////////////////
|
||||||
|
|
||||||
|
|||||||
@ -2826,7 +2826,7 @@ CONFIG_LV_FS_POSIX_CACHE_SIZE=0
|
|||||||
# CONFIG_LV_USE_PNG is not set
|
# CONFIG_LV_USE_PNG is not set
|
||||||
# CONFIG_LV_USE_BMP is not set
|
# CONFIG_LV_USE_BMP is not set
|
||||||
# CONFIG_LV_USE_SJPG is not set
|
# CONFIG_LV_USE_SJPG is not set
|
||||||
# CONFIG_LV_USE_GIF is not set
|
CONFIG_LV_USE_GIF=y
|
||||||
# CONFIG_LV_USE_QRCODE is not set
|
# CONFIG_LV_USE_QRCODE is not set
|
||||||
# CONFIG_LV_USE_FREETYPE is not set
|
# CONFIG_LV_USE_FREETYPE is not set
|
||||||
# CONFIG_LV_USE_TINY_TTF is not set
|
# CONFIG_LV_USE_TINY_TTF is not set
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user