Dzbj_ESP32-C3/docs/ScreenSet下拉overlay实现方案.md
Rdzleo 05dbf1f1ab 添加ScreenSet下拉overlay实现方案文档,同步sdkconfig默认配置
1、docs/: 添加LVGL下拉通知栏实现指南和ScreenSet下拉overlay完整实现方案文档,
   包含完整源码、修改清单和快速切换指南,便于后续在独立屏幕/下拉模式间切换;
2、sdkconfig: ESP-IDF构建系统自动补充的默认配置项(SOC/BLE/TLS/PHY/mbedTLS),
   不影响项目功能;

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:01:09 +08:00

663 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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减少渲染面积