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

26 KiB
Raw Permalink Blame History

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核心完全重写

// 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

#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 滚动容器自然处理:

-#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 修改

删除上滑回 HomeLV_DIR_TOP)和下滑进 ScreenSetLV_DIR_BOTTOM)的手势代码,由 overlay 统一处理:

-#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 中添加下拉面板收回逻辑:

// 如果下拉面板可见,先收回
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.cui_ScreenSet.h
  2. ui_ScreenHome.c 中删除 LV_DIR_BOTTOM 手势分支和 #include "ui_ScreenSet.h"
  3. ui_ScreenImg.c 中删除 LV_DIR_TOPLV_DIR_BOTTOM 手势分支和 #include "ui_ScreenSet.h"
  4. ui.c 中删除 dropdown_panel 相关引用(如有)
  5. main.cboot_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减少渲染面积