# ScreenSet 下拉 Overlay 实现方案 > 本文档记录了将 ScreenSet 界面从独立屏幕切换改造为 `lv_layer_top()` 下拉 overlay 模式的完整实现。 > 可按照「快速切换指南」章节快速恢复下拉模式。 ## 一、方案概述 - ScreenSet 不再作为独立 `lv_obj_create(NULL)` 屏幕,而是挂在 `lv_layer_top()` 上的滚动容器 - 利用 LVGL 原生滚动机制实现下拉效果,初始 `scroll_to_y(360)` 隐藏在屏幕上方 - 手指下拉时面板自然滑入,松手时自动吸附(超过一半展开,不到一半收回) - 所有手势(上滑/下滑/左滑/右滑)统一在 overlay 的触摸追踪回调中处理 - 不需要 `dropdown_panel.c`/`.h` 文件,所有逻辑合并到 `ui_ScreenSet.c` 中 ## 二、涉及文件及修改清单 ### 2.1 ui_ScreenSet.c(核心,完全重写) ```c // ScreenSet 界面 — 以 lv_layer_top() overlay 模式实现下拉通知栏 // 利用 LVGL 原生滚动机制,从屏幕顶部下拉覆盖 Home/Img 界面 // 合并原 dropdown_panel.c 的滚动容器 + 手势转发逻辑 #include "../ui.h" #include "ui_ScreenImg.h" #include "ui_ScreenHome.h" #include "../../pages/include/pages.h" #include "../../sleep_mgr/include/sleep_mgr.h" #include "esp_lvgl_port.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #define TAG "ScreenSet" // ==================== 布局参数 ==================== #define LCD_SIZE 360 #define PANEL_HEIGHT LCD_SIZE #define SWIPE_THRESHOLD 50 // ==================== 对象指针 ==================== // 滚动容器(挂在 lv_layer_top 上) lv_obj_t *ui_ScreenSet = NULL; // 即 set_container,保持外部兼容 static lv_obj_t *bg_panel = NULL; static lv_obj_t *spacer_panel = NULL; // 功能控件(直接挂在 ui_ScreenSet 滚动容器下,LV_ALIGN_CENTER + 偏移) lv_obj_t *ui_ImgLowPower = NULL; lv_obj_t *ui_ImgFlashlight = NULL; lv_obj_t *ui_ImgDelete = NULL; lv_obj_t *ui_SliderBrightness = NULL; lv_obj_t *ui_ImgSun = NULL; lv_obj_t *ui_LabelBrightness = NULL; lv_obj_t *ui_ArcPowerLevel = NULL; lv_obj_t *ui_ImgLightning = NULL; lv_obj_t *ui_LabelPowerLevel = NULL; // 不再需要的中间容器(保持 extern 兼容,始终为 NULL) lv_obj_t *ui_GlobalContainer = NULL; lv_obj_t *ui_ContainerTop = NULL; lv_obj_t *ui_ContainerCentral = NULL; // ==================== 状态 ==================== static bool panel_initialized = false; static lv_point_t press_point; static bool press_tracked = false; // ==================== 下拉面板接口 ==================== bool ui_ScreenSet_is_visible(void) { if (!ui_ScreenSet) return false; lv_coord_t scroll_y = lv_obj_get_scroll_y(ui_ScreenSet); return (scroll_y < PANEL_HEIGHT - 10); } // 前向声明 static void snap_anim_exec_cb(void *obj, int32_t val); void ui_ScreenSet_hide(void) { if (!ui_ScreenSet) return; lv_coord_t scroll_y = lv_obj_get_scroll_y(ui_ScreenSet); lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, ui_ScreenSet); lv_anim_set_exec_cb(&a, snap_anim_exec_cb); lv_anim_set_values(&a, scroll_y, PANEL_HEIGHT); lv_anim_set_time(&a, 350); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); lv_anim_start(&a); } // ==================== 手电筒功能 ==================== static lv_obj_t *flashlight_overlay = NULL; static lv_timer_t *flashlight_timer = NULL; static uint8_t flashlight_color_index = 0; static bool flashlight_bright = true; static uint8_t saved_brightness = 50; static const uint32_t flashlight_color_values[] = { 0xFF0000, 0x00FF00, 0x0000FF, }; #define FLASHLIGHT_COLOR_COUNT (sizeof(flashlight_color_values) / sizeof(flashlight_color_values[0])) static void flashlight_blink_timer_cb(lv_timer_t *timer) { if (!flashlight_overlay) return; flashlight_bright = !flashlight_bright; pwm_set_brightness(flashlight_bright ? 100 : 20); } static lv_timer_t *fade_timer = NULL; static uint8_t target_brightness = 100; static int8_t fade_step = 0; static void fade_in_delayed_cb(lv_timer_t *timer); static void flashlight_overlay_event_cb(lv_event_t *e); static void fade_brightness_cb(lv_timer_t *timer) { uint8_t current = pwm_get_brightness(); if (fade_step > 0) { if (current < target_brightness) { current += fade_step; if (current > target_brightness) current = target_brightness; pwm_set_brightness(current); } else { lv_timer_del(fade_timer); fade_timer = NULL; } } else if (fade_step < 0) { if (current > 0) { current += fade_step; if (current < 0 || current > 100) current = 0; pwm_set_brightness(current); } else { lv_timer_del(fade_timer); fade_timer = NULL; } } } static void start_fade(bool fade_out, uint8_t target_bright) { if (fade_timer) { lv_timer_del(fade_timer); fade_timer = NULL; } target_brightness = target_bright; fade_step = fade_out ? -25 : 25; fade_timer = lv_timer_create(fade_brightness_cb, 2, NULL); } static void color_switch_delayed_cb(lv_timer_t *timer) { flashlight_color_index = (flashlight_color_index + 1) % FLASHLIGHT_COLOR_COUNT; lvgl_port_lock(-1); if (flashlight_overlay) { lv_obj_set_style_bg_color(flashlight_overlay, lv_color_hex(flashlight_color_values[flashlight_color_index]), 0); lv_obj_invalidate(flashlight_overlay); } lvgl_port_unlock(); lv_timer_t *fade_in_timer = lv_timer_create(fade_in_delayed_cb, 90, NULL); lv_timer_set_repeat_count(fade_in_timer, 1); } static void fade_in_delayed_cb(lv_timer_t *timer) { start_fade(false, 100); } static void flashlight_overlay_event_cb(lv_event_t *e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; if (fade_timer) { lv_timer_del(fade_timer); fade_timer = NULL; } pwm_set_brightness(0); lv_timer_t *switch_timer = lv_timer_create(color_switch_delayed_cb, 2, NULL); lv_timer_set_repeat_count(switch_timer, 1); } void flashlight_exit(void) { pwm_set_brightness(0); if (lvgl_port_lock(100)) { if (flashlight_timer) { lv_timer_del(flashlight_timer); flashlight_timer = NULL; } if (fade_timer) { lv_timer_del(fade_timer); fade_timer = NULL; } if (flashlight_overlay) { lv_obj_del(flashlight_overlay); flashlight_overlay = NULL; } lvgl_port_unlock(); } } bool flashlight_is_active(void) { return (flashlight_overlay != NULL); } uint8_t flashlight_get_saved_brightness(void) { return saved_brightness; } void ui_ScreenSet_show_flashlight(void) { if (flashlight_overlay) return; saved_brightness = pwm_get_brightness(); flashlight_overlay = lv_obj_create(lv_layer_top()); lv_obj_remove_style_all(flashlight_overlay); lv_obj_set_size(flashlight_overlay, LV_HOR_RES, LV_VER_RES); lv_obj_set_pos(flashlight_overlay, 0, 0); lv_obj_clear_flag(flashlight_overlay, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(flashlight_overlay, LV_OBJ_FLAG_CLICKABLE); flashlight_color_index = 0; lv_obj_set_style_bg_color(flashlight_overlay, lv_color_hex(flashlight_color_values[0]), 0); lv_obj_set_style_bg_opa(flashlight_overlay, LV_OPA_COVER, 0); lv_obj_add_event_cb(flashlight_overlay, flashlight_overlay_event_cb, LV_EVENT_ALL, NULL); flashlight_bright = true; flashlight_timer = lv_timer_create(flashlight_blink_timer_cb, 500, NULL); pwm_set_brightness(100); } // ==================== 按钮事件回调 ==================== static void ui_event_SliderBrightness(lv_event_t *e) { if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; lv_obj_t *target = lv_event_get_target(e); int32_t val = lv_slider_get_value(target); if (val < 10) { val = 10; lv_slider_set_value(target, 10, LV_ANIM_OFF); } pwm_set_brightness((uint8_t)val); char buf[8]; lv_snprintf(buf, sizeof(buf), "%d%%", (int)val); lv_label_set_text(ui_LabelBrightness, buf); } static void ui_event_ImgFlashlight(lv_event_t *e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; ui_ScreenSet_hide(); ui_ScreenSet_show_flashlight(); } static void ui_event_ImgDelete(lv_event_t *e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; ui_ScreenSet_hide(); lv_obj_t *current = lv_scr_act(); if (current == ui_ScreenImg) { ui_ScreenImg_show_delete_container(); } else { _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); ui_ScreenImg_show_delete_container(); } } static void ui_event_ImgLowPower(lv_event_t *e) { if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; lv_obj_t *target = lv_event_get_target(e); bool checked = lv_obj_has_state(target, LV_STATE_CHECKED); sleep_mgr_set_enabled(checked); } // ==================== 触摸追踪手势检测 ==================== static void set_handle_swipe(int dx, int dy) { int abs_dx = dx < 0 ? -dx : dx; int abs_dy = dy < 0 ? -dy : dy; bool visible = ui_ScreenSet_is_visible(); lv_obj_t *current = lv_scr_act(); // 水平滑动 if (abs_dx > SWIPE_THRESHOLD && abs_dx > abs_dy + abs_dy / 2) { if (visible) { ui_ScreenSet_hide(); return; } if (dx < 0) { if (current == ui_ScreenHome) _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); else if (current == ui_ScreenImg) { const char *next_img = get_next_image(); if (next_img) update_ui_ImgBle(next_img); } } else { if (current == ui_ScreenHome) _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); else if (current == ui_ScreenImg) { const char *prev_img = get_prev_image(); if (prev_img) update_ui_ImgBle(prev_img); } } return; } // 上滑:面板隐藏 + Img 界面回 Home if (dy < -SWIPE_THRESHOLD && abs_dy > abs_dx + abs_dx / 2 && !visible) { if (current == ui_ScreenImg) { ui_ScreenImg_hide_delete_container(); _ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init); } } } // 自定义滚动动画回调 static void snap_anim_exec_cb(void *obj, int32_t val) { lv_obj_scroll_to_y((lv_obj_t *)obj, val, LV_ANIM_OFF); } // 吸附动画时长(ms),可根据体验调整 #define SNAP_ANIM_DURATION 350 // 滚动结束时自动吸附:超过一半则展开,不到一半则收回 static void set_event_scroll_end(lv_event_t *e) { if (!ui_ScreenSet) return; lv_coord_t scroll_y = lv_obj_get_scroll_y(ui_ScreenSet); // scroll_y=0 完全展开, scroll_y=PANEL_HEIGHT 完全隐藏 if (scroll_y > 0 && scroll_y < PANEL_HEIGHT) { int32_t target = (scroll_y < PANEL_HEIGHT / 2) ? 0 : PANEL_HEIGHT; lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, ui_ScreenSet); lv_anim_set_exec_cb(&a, snap_anim_exec_cb); lv_anim_set_values(&a, scroll_y, target); lv_anim_set_time(&a, SNAP_ANIM_DURATION); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); // 缓出曲线,结尾自然减速 lv_anim_start(&a); } } static void set_event_container(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_PRESSED) { lv_indev_get_point(lv_indev_get_act(), &press_point); press_tracked = true; } else if (code == LV_EVENT_RELEASED && press_tracked) { press_tracked = false; lv_point_t rel_point; lv_indev_get_point(lv_indev_get_act(), &rel_point); set_handle_swipe(rel_point.x - press_point.x, rel_point.y - press_point.y); } else if (code == LV_EVENT_PRESS_LOST) { press_tracked = false; } } // ==================== 旧接口(保持编译兼容) ==================== void ui_event_ScreenSet(lv_event_t *e) { /* 不再使用 */ } void ui_event_SliderBrightness_compat(lv_event_t *e) { /* 不再使用 */ } void ui_ScreenSet_set_previous(lv_obj_t **screen, void (*init_func)(void)) { /* 不再使用 */ } // ==================== 初始化 ==================== void ui_ScreenSet_screen_init(void) { if (panel_initialized) return; // 1. 外层透明滚动容器(挂在 lv_layer_top,跨界面共享) ui_ScreenSet = lv_obj_create(lv_layer_top()); lv_obj_set_size(ui_ScreenSet, LCD_SIZE, LCD_SIZE); lv_obj_set_pos(ui_ScreenSet, 0, 0); lv_obj_set_align(ui_ScreenSet, LV_ALIGN_TOP_MID); lv_obj_clear_flag(ui_ScreenSet, LV_OBJ_FLAG_SCROLL_ELASTIC | LV_OBJ_FLAG_SCROLL_CHAIN); lv_obj_set_scroll_dir(ui_ScreenSet, LV_DIR_VER); lv_obj_set_style_bg_opa(ui_ScreenSet, 0, LV_PART_MAIN); lv_obj_set_style_border_opa(ui_ScreenSet, 0, LV_PART_MAIN); lv_obj_set_style_pad_all(ui_ScreenSet, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(ui_ScreenSet, 0, LV_PART_SCROLLBAR); lv_obj_set_style_bg_opa(ui_ScreenSet, 0, LV_PART_SCROLLBAR | LV_STATE_SCROLLED); // 2. 不透明背景面板 bg_panel = lv_obj_create(ui_ScreenSet); lv_obj_set_size(bg_panel, LCD_SIZE, PANEL_HEIGHT); lv_obj_set_pos(bg_panel, 0, 0); lv_obj_set_align(bg_panel, LV_ALIGN_TOP_MID); lv_obj_clear_flag(bg_panel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(bg_panel, lv_color_hex(0x000000), LV_PART_MAIN); lv_obj_set_style_bg_opa(bg_panel, 255, LV_PART_MAIN); lv_obj_set_style_border_opa(bg_panel, 0, LV_PART_MAIN); lv_obj_set_style_radius(bg_panel, 0, LV_PART_MAIN); lv_obj_set_style_pad_all(bg_panel, 0, LV_PART_MAIN); // 3. ArcPowerLevel(居中) ui_ArcPowerLevel = lv_arc_create(ui_ScreenSet); lv_obj_set_size(ui_ArcPowerLevel, 320, 320); lv_obj_set_align(ui_ArcPowerLevel, LV_ALIGN_CENTER); lv_obj_clear_flag(ui_ArcPowerLevel, LV_OBJ_FLAG_CLICKABLE); lv_arc_set_value(ui_ArcPowerLevel, 50); lv_obj_set_style_arc_color(ui_ArcPowerLevel, lv_color_hex(0x39393E), LV_PART_MAIN); lv_obj_set_style_arc_opa(ui_ArcPowerLevel, 255, LV_PART_MAIN); lv_obj_set_style_arc_width(ui_ArcPowerLevel, 10, LV_PART_MAIN); lv_obj_set_style_arc_color(ui_ArcPowerLevel, lv_color_hex(0x19FA29), LV_PART_INDICATOR); lv_obj_set_style_arc_opa(ui_ArcPowerLevel, 255, LV_PART_INDICATOR); lv_obj_set_style_arc_width(ui_ArcPowerLevel, 10, LV_PART_INDICATOR); lv_obj_set_style_bg_opa(ui_ArcPowerLevel, 0, LV_PART_KNOB); // 4. 按钮行(居中,y=-41) ui_ImgLowPower = lv_imgbtn_create(ui_ScreenSet); lv_imgbtn_set_src(ui_ImgLowPower, LV_IMGBTN_STATE_RELEASED, NULL, &ui_img_s11_png, NULL); lv_imgbtn_set_src(ui_ImgLowPower, LV_IMGBTN_STATE_PRESSED, NULL, &ui_img_s11_png, NULL); lv_imgbtn_set_src(ui_ImgLowPower, LV_IMGBTN_STATE_DISABLED, NULL, &ui_img_s12_png, NULL); lv_imgbtn_set_src(ui_ImgLowPower, LV_IMGBTN_STATE_CHECKED_PRESSED, NULL, &ui_img_s12_png, NULL); lv_imgbtn_set_src(ui_ImgLowPower, LV_IMGBTN_STATE_CHECKED_RELEASED, NULL, &ui_img_s12_png, NULL); lv_obj_set_size(ui_ImgLowPower, 64, 64); lv_obj_set_align(ui_ImgLowPower, LV_ALIGN_CENTER); lv_obj_set_x(ui_ImgLowPower, -86); lv_obj_set_y(ui_ImgLowPower, -41); lv_obj_add_flag(ui_ImgLowPower, LV_OBJ_FLAG_CHECKABLE); lv_obj_clear_flag(ui_ImgLowPower, LV_OBJ_FLAG_SCROLLABLE); ui_ImgFlashlight = lv_img_create(ui_ScreenSet); lv_img_set_src(ui_ImgFlashlight, &ui_img_s9_png); lv_obj_set_width(ui_ImgFlashlight, LV_SIZE_CONTENT); lv_obj_set_height(ui_ImgFlashlight, LV_SIZE_CONTENT); lv_obj_set_align(ui_ImgFlashlight, LV_ALIGN_CENTER); lv_obj_set_x(ui_ImgFlashlight, -2); lv_obj_set_y(ui_ImgFlashlight, -41); lv_obj_add_flag(ui_ImgFlashlight, LV_OBJ_FLAG_ADV_HITTEST | LV_OBJ_FLAG_CLICKABLE); lv_obj_clear_flag(ui_ImgFlashlight, LV_OBJ_FLAG_SCROLLABLE); ui_ImgDelete = lv_img_create(ui_ScreenSet); lv_img_set_src(ui_ImgDelete, &ui_img_s6_png); lv_obj_set_width(ui_ImgDelete, LV_SIZE_CONTENT); lv_obj_set_height(ui_ImgDelete, LV_SIZE_CONTENT); lv_obj_set_align(ui_ImgDelete, LV_ALIGN_CENTER); lv_obj_set_x(ui_ImgDelete, 82); lv_obj_set_y(ui_ImgDelete, -41); lv_obj_add_flag(ui_ImgDelete, LV_OBJ_FLAG_ADV_HITTEST | LV_OBJ_FLAG_CLICKABLE); lv_obj_clear_flag(ui_ImgDelete, LV_OBJ_FLAG_SCROLLABLE); // 5. 亮度滑块行(居中,y=+44) ui_SliderBrightness = lv_slider_create(ui_ScreenSet); uint8_t cur_bright = pwm_get_brightness(); lv_slider_set_value(ui_SliderBrightness, cur_bright, LV_ANIM_OFF); lv_obj_set_size(ui_SliderBrightness, 220, 60); lv_obj_set_align(ui_SliderBrightness, LV_ALIGN_CENTER); lv_obj_set_x(ui_SliderBrightness, -3); lv_obj_set_y(ui_SliderBrightness, 44); lv_obj_set_style_radius(ui_SliderBrightness, 50, LV_PART_MAIN); lv_obj_set_style_radius(ui_SliderBrightness, 0, LV_PART_INDICATOR); lv_obj_set_style_bg_color(ui_SliderBrightness, lv_color_hex(0x64A8EB), LV_PART_INDICATOR); lv_obj_set_style_bg_opa(ui_SliderBrightness, 255, LV_PART_INDICATOR); lv_obj_set_style_bg_color(ui_SliderBrightness, lv_color_hex(0xFFFFFF), LV_PART_KNOB); lv_obj_set_style_bg_opa(ui_SliderBrightness, 0, LV_PART_KNOB); ui_ImgSun = lv_img_create(ui_ScreenSet); lv_img_set_src(ui_ImgSun, &ui_img_s10_png); lv_obj_set_width(ui_ImgSun, LV_SIZE_CONTENT); lv_obj_set_height(ui_ImgSun, LV_SIZE_CONTENT); lv_obj_set_align(ui_ImgSun, LV_ALIGN_CENTER); lv_obj_set_x(ui_ImgSun, -78); lv_obj_set_y(ui_ImgSun, 44); lv_obj_add_flag(ui_ImgSun, LV_OBJ_FLAG_ADV_HITTEST); lv_obj_clear_flag(ui_ImgSun, LV_OBJ_FLAG_SCROLLABLE); ui_LabelBrightness = lv_label_create(ui_ScreenSet); lv_obj_set_width(ui_LabelBrightness, LV_SIZE_CONTENT); lv_obj_set_height(ui_LabelBrightness, LV_SIZE_CONTENT); lv_obj_set_align(ui_LabelBrightness, LV_ALIGN_CENTER); lv_obj_set_x(ui_LabelBrightness, 6); lv_obj_set_y(ui_LabelBrightness, 44); char buf[8]; lv_snprintf(buf, sizeof(buf), "%d%%", (int)cur_bright); lv_label_set_text(ui_LabelBrightness, buf); lv_obj_set_style_text_color(ui_LabelBrightness, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_set_style_text_font(ui_LabelBrightness, &lv_font_montserrat_18, LV_PART_MAIN); // 6. 电量显示(居中,y=+121) ui_ImgLightning = lv_img_create(ui_ScreenSet); lv_img_set_src(ui_ImgLightning, &ui_img_s8_png); lv_obj_set_width(ui_ImgLightning, LV_SIZE_CONTENT); lv_obj_set_height(ui_ImgLightning, LV_SIZE_CONTENT); lv_obj_set_align(ui_ImgLightning, LV_ALIGN_CENTER); lv_obj_set_x(ui_ImgLightning, -23); lv_obj_set_y(ui_ImgLightning, 121); lv_obj_add_flag(ui_ImgLightning, LV_OBJ_FLAG_ADV_HITTEST); lv_obj_clear_flag(ui_ImgLightning, LV_OBJ_FLAG_SCROLLABLE); ui_LabelPowerLevel = lv_label_create(ui_ScreenSet); lv_obj_set_width(ui_LabelPowerLevel, LV_SIZE_CONTENT); lv_obj_set_height(ui_LabelPowerLevel, LV_SIZE_CONTENT); lv_obj_set_align(ui_LabelPowerLevel, LV_ALIGN_CENTER); lv_obj_set_x(ui_LabelPowerLevel, 26); lv_obj_set_y(ui_LabelPowerLevel, 121); lv_label_set_text(ui_LabelPowerLevel, "70%"); lv_obj_set_style_text_color(ui_LabelPowerLevel, lv_color_hex(0xFFFFFF), LV_PART_MAIN); lv_obj_set_style_text_font(ui_LabelPowerLevel, &lv_font_montserrat_20, LV_PART_MAIN); // 7. 底部透明占位(撑大滚动范围 = 360) spacer_panel = lv_obj_create(ui_ScreenSet); lv_obj_set_size(spacer_panel, LCD_SIZE, PANEL_HEIGHT); lv_obj_set_pos(spacer_panel, 0, PANEL_HEIGHT); lv_obj_set_align(spacer_panel, LV_ALIGN_TOP_MID); lv_obj_clear_flag(spacer_panel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_opa(spacer_panel, 0, LV_PART_MAIN); lv_obj_set_style_border_opa(spacer_panel, 0, LV_PART_MAIN); // 8. 事件注册 lv_obj_add_event_cb(ui_ImgLowPower, ui_event_ImgLowPower, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(ui_ImgFlashlight, ui_event_ImgFlashlight, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_ImgDelete, ui_event_ImgDelete, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_SliderBrightness, ui_event_SliderBrightness, LV_EVENT_VALUE_CHANGED, NULL); // 触摸追踪(只监听需要的事件) lv_obj_add_event_cb(ui_ScreenSet, set_event_container, LV_EVENT_PRESSED, NULL); lv_obj_add_event_cb(ui_ScreenSet, set_event_container, LV_EVENT_RELEASED, NULL); lv_obj_add_event_cb(ui_ScreenSet, set_event_container, LV_EVENT_PRESS_LOST, NULL); // 滚动结束自动吸附 lv_obj_add_event_cb(ui_ScreenSet, set_event_scroll_end, LV_EVENT_SCROLL_END, NULL); // 9. 初始滚动到隐藏位置 lv_obj_update_layout(ui_ScreenSet); lv_obj_scroll_to_y(ui_ScreenSet, PANEL_HEIGHT, LV_ANIM_OFF); panel_initialized = true; ESP_LOGI(TAG, "ScreenSet overlay 初始化完成"); } void ui_ScreenSet_screen_destroy(void) { if (!panel_initialized) return; if (ui_ScreenSet) { lv_obj_del(ui_ScreenSet); ui_ScreenSet = NULL; } bg_panel = NULL; spacer_panel = NULL; ui_GlobalContainer = NULL; ui_ContainerTop = NULL; ui_ContainerCentral = NULL; ui_ImgLowPower = NULL; ui_ImgFlashlight = NULL; ui_ImgDelete = NULL; ui_SliderBrightness = NULL; ui_ImgSun = NULL; ui_LabelBrightness = NULL; ui_ArcPowerLevel = NULL; ui_ImgLightning = NULL; ui_LabelPowerLevel = NULL; panel_initialized = false; } ``` ### 2.2 ui_ScreenSet.h ```c #ifndef UI_SCREENSET_H #define UI_SCREENSET_H #ifdef __cplusplus extern "C" { #endif // SCREEN: ui_ScreenSet(以 lv_layer_top overlay 模式实现) extern void ui_ScreenSet_screen_init(void); extern void ui_ScreenSet_screen_destroy(void); extern lv_obj_t *ui_ScreenSet; extern lv_obj_t *ui_GlobalContainer; extern lv_obj_t *ui_ContainerTop; extern lv_obj_t *ui_ImgLowPower; extern lv_obj_t *ui_ImgFlashlight; extern lv_obj_t *ui_ImgDelete; extern lv_obj_t *ui_ContainerCentral; extern lv_obj_t *ui_SliderBrightness; extern lv_obj_t *ui_ImgSun; extern lv_obj_t *ui_LabelBrightness; extern lv_obj_t *ui_ArcPowerLevel; extern lv_obj_t *ui_ImgLightning; extern lv_obj_t *ui_LabelPowerLevel; // 下拉面板接口 extern bool ui_ScreenSet_is_visible(void); extern void ui_ScreenSet_hide(void); // 手电筒功能 extern void ui_ScreenSet_show_flashlight(void); extern void flashlight_exit(void); extern bool flashlight_is_active(void); extern uint8_t flashlight_get_saved_brightness(void); // 旧接口(保持编译兼容) extern void ui_event_ScreenSet(lv_event_t *e); extern void ui_ScreenSet_set_previous(lv_obj_t **screen, void (*init_func)(void)); #ifdef __cplusplus } /*extern "C"*/ #endif #endif ``` ### 2.3 ui_ScreenHome.c 修改 删除下滑手势进入 ScreenSet 的代码块(`LV_DIR_BOTTOM` 分支),因为下滑由 overlay 滚动容器自然处理: ```diff -#include "ui_ScreenSet.h" // 引入ScreenSet的函数声明 -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()); - ui_ScreenSet_set_previous(&ui_ScreenHome, &ui_ScreenHome_screen_init); - _ui_screen_change( &ui_ScreenSet, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSet_screen_init); -} ``` ### 2.4 ui_ScreenImg.c 修改 删除上滑回 Home(`LV_DIR_TOP`)和下滑进 ScreenSet(`LV_DIR_BOTTOM`)的手势代码,由 overlay 统一处理: ```diff -#include "ui_ScreenSet.h" -if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) { -lv_indev_wait_release(lv_indev_get_act()); - ui_ScreenImg_hide_delete_container(); - _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 ) { -lv_indev_wait_release(lv_indev_get_act()); - ui_ScreenImg_hide_delete_container(); - ui_ScreenSet_set_previous(&ui_ScreenImg, &ui_ScreenImg_screen_init); - _ui_screen_change( &ui_ScreenSet, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenSet_screen_init); -} ``` ### 2.5 ui.c 修改 移除 `dropdown_panel.h` 引用和 `dropdown_panel_init()`/`dropdown_panel_destroy()` 调用。`ui_ScreenSet_screen_init()` 保留(现在创建 overlay 而非独立屏幕)。 ### 2.6 main.c 修改 在 `boot_btn_handler` 中添加下拉面板收回逻辑: ```c // 如果下拉面板可见,先收回 if (ui_ScreenSet_is_visible()) { ui_ScreenSet_hide(); ESP_LOGI("BTN_HANDLER", "下拉面板已收回"); } ``` 移除 `#include "ui/dropdown_panel.h"`。 ### 2.7 CMakeLists.txt 移除 `"./ui/dropdown_panel.c"` 行。 ### 2.8 删除文件 - `main/ui/dropdown_panel.c` - `main/ui/dropdown_panel.h` ## 三、快速切换指南 ### 从「独立屏幕」切回「下拉 overlay」 1. 用本文档第二章的代码替换 `ui_ScreenSet.c` 和 `ui_ScreenSet.h` 2. 在 `ui_ScreenHome.c` 中删除 `LV_DIR_BOTTOM` 手势分支和 `#include "ui_ScreenSet.h"` 3. 在 `ui_ScreenImg.c` 中删除 `LV_DIR_TOP` 和 `LV_DIR_BOTTOM` 手势分支和 `#include "ui_ScreenSet.h"` 4. 在 `ui.c` 中删除 `dropdown_panel` 相关引用(如有) 5. 在 `main.c` 的 `boot_btn_handler` 中添加 `ui_ScreenSet_is_visible()`/`ui_ScreenSet_hide()` 调用 6. 确保 `CMakeLists.txt` 中没有 `dropdown_panel.c` 7. 编译:`idf.py build` ### 从「下拉 overlay」切回「独立屏幕」 1. `git checkout HEAD -- main/ui/screens/ui_ScreenSet.c main/ui/screens/ui_ScreenSet.h main/ui/screens/ui_ScreenHome.c main/ui/screens/ui_ScreenImg.c main/ui/ui.c main/main.c` 2. 编译:`idf.py build` ## 四、技术要点 ### 4.1 为什么挂在 lv_layer_top() `lv_layer_top()` 是 LVGL 的全局顶层,不随 `lv_disp_load_scr()` 切换而销毁,实现跨界面共享 overlay。 ### 4.2 滚动吸附机制 - `LV_EVENT_SCROLL_END` 事件检测 `scroll_y` 位置 - `scroll_y < 180`(面板露出超过一半)→ `lv_anim_t` 动画展开到 0 - `scroll_y >= 180`(面板露出不到一半)→ 动画收回到 360 - 使用 `lv_anim_path_ease_out` 缓出曲线,时长 350ms ### 4.3 手势转发 overlay 在 `lv_layer_top()` 上拦截所有触摸事件。通过 `PRESSED`/`RELEASED` 坐标差计算手势方向,在回调中主动调用 `_ui_screen_change()` 和图片切换函数,实现对底层 Home/Img 界面的手势转发。 ### 4.4 性能优化 - 事件回调只注册 `PRESSED`/`RELEASED`/`PRESS_LOST`/`SCROLL_END`,不用 `LV_EVENT_ALL` - 背景面板 `bg_opa=255` 完全不透明,避免 alpha 混合开销 - Arc 宽度 10px + 隐藏 knob,减少渲染面积