From 05dbf1f1abf3fa8a1dc41b83dba6813d4dfe96d3 Mon Sep 17 00:00:00 2001 From: Rdzleo Date: Wed, 11 Mar 2026 14:01:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ScreenSet=E4=B8=8B=E6=8B=89ov?= =?UTF-8?q?erlay=E5=AE=9E=E7=8E=B0=E6=96=B9=E6=A1=88=E6=96=87=E6=A1=A3?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=AD=A5sdkconfig=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、docs/: 添加LVGL下拉通知栏实现指南和ScreenSet下拉overlay完整实现方案文档, 包含完整源码、修改清单和快速切换指南,便于后续在独立屏幕/下拉模式间切换; 2、sdkconfig: ESP-IDF构建系统自动补充的默认配置项(SOC/BLE/TLS/PHY/mbedTLS), 不影响项目功能; Co-Authored-By: Claude Opus 4.6 --- docs/LVGL下拉通知栏实现指南.md | 342 ++++++++++++++ docs/ScreenSet下拉overlay实现方案.md | 662 +++++++++++++++++++++++++++ sdkconfig | 7 + 3 files changed, 1011 insertions(+) create mode 100644 docs/LVGL下拉通知栏实现指南.md create mode 100644 docs/ScreenSet下拉overlay实现方案.md diff --git a/docs/LVGL下拉通知栏实现指南.md b/docs/LVGL下拉通知栏实现指南.md new file mode 100644 index 0000000..b2f119b --- /dev/null +++ b/docs/LVGL下拉通知栏实现指南.md @@ -0,0 +1,342 @@ +# LVGL 下拉通知栏实现指南 + +> 基于 OV-Watch 项目(STM32F411 + LVGL 8.2)的下拉栏实现分析,适用于在其他 LVGL 项目中复现类似效果。 + +## 一、效果描述 + +在主界面(表盘)上方隐藏一个通知/快捷操作面板,用户手指下拉时面板滑入,上推时收回。类似 Android/iOS 的通知栏下拉效果。 + +## 二、核心原理 + +**不使用任何自定义绘制或动画 API**,纯粹利用 LVGL 原生的滚动机制: + +1. 在主界面上创建一个**超出屏幕高度**的透明滚动容器 +2. 将下拉栏内容放在容器顶部 +3. 通过 `lv_obj_scroll_by()` 设置初始偏移,将内容**滚到屏幕上方不可见区域** +4. 用户触摸下拉时,LVGL 滚动机制自然将内容带回视口 + +## 三、对象层级结构 + +``` +ui_HomePage (主屏幕,禁用滚动) +├── 时间、电量、步数等表盘控件 ... +│ +└── ui_DropDownPanel (透明滚动容器,覆盖全屏) + ├── ui_UpBGPanel (半透明背景面板,承载视觉效果) + ├── ui_NFCButton (功能按钮) + ├── ui_BLEButton + ├── ui_PowerButton + ├── ui_SetButton + ├── ui_LightSlider (亮度滑块) + └── ui_DownBGPanel (透明占位面板,撑大滚动区域) +``` + +## 四、各层详细实现 + +### 4.1 主屏幕:禁用滚动 + +主屏幕自身必须禁止滚动,否则会和下拉容器的滚动冲突: + +```c +lv_obj_clear_flag(ui_HomePage, LV_OBJ_FLAG_SCROLLABLE); +``` + +### 4.2 外层透明滚动容器 `ui_DropDownPanel` + +这是整个下拉效果的核心。容器本身**完全透明**,只提供滚动能力。 + +```c +// 创建容器,挂在主屏幕上 +ui_DropDownPanel = lv_obj_create(ui_HomePage); +lv_obj_set_width(ui_DropDownPanel, LCD_WIDTH); // 240 +lv_obj_set_height(ui_DropDownPanel, 420); // 远大于屏幕高度(240),容纳内容+占位 +lv_obj_set_x(ui_DropDownPanel, 0); +lv_obj_set_y(ui_DropDownPanel, -10); // 略微上移,确保顶部触摸可达 +lv_obj_set_align(ui_DropDownPanel, LV_ALIGN_TOP_MID); + +// ★ 关键:初始向上滚动,将内容区藏到屏幕上方 +// CONTENT_HEIGHT 为下拉栏内容区高度(本项目为130px) +lv_obj_scroll_by(ui_DropDownPanel, 0, -130, LV_ANIM_OFF); + +// 禁用弹性滚动和滚动链(防止过度回弹和事件冒泡) +lv_obj_clear_flag(ui_DropDownPanel, LV_OBJ_FLAG_SCROLL_ELASTIC | LV_OBJ_FLAG_SCROLL_CHAIN); + +// 只允许垂直方向滚动 +lv_obj_set_scroll_dir(ui_DropDownPanel, LV_DIR_VER); + +// 容器背景完全透明(不遮挡底层表盘) +lv_obj_set_style_bg_opa(ui_DropDownPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); +lv_obj_set_style_border_opa(ui_DropDownPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + +// 隐藏滚动条(默认和滚动中都透明) +lv_obj_set_style_bg_opa(ui_DropDownPanel, 0, LV_PART_SCROLLBAR | LV_STATE_DEFAULT); +lv_obj_set_style_bg_opa(ui_DropDownPanel, 0, LV_PART_SCROLLBAR | LV_STATE_SCROLLED); +``` + +**参数设计要点**: +- `height(420)` = 内容区高度(130) + 屏幕高度(240) + 占位面板(130) - 重叠区域,确保滚动范围足够 +- `scroll_by(0, -130)` 中的 130 = 内容区(`ui_UpBGPanel`)的高度,刚好将其完全藏起来 +- 禁用 `SCROLL_ELASTIC` 防止下拉到底时弹跳,影响操作体验 +- 禁用 `SCROLL_CHAIN` 防止滚动事件冒泡到父对象(主屏幕) + +### 4.3 半透明背景面板 `ui_UpBGPanel` + +这个面板提供下拉栏的**视觉背景**,通过 `bg_opa` 控制透明度: + +```c +ui_UpBGPanel = lv_obj_create(ui_DropDownPanel); +lv_obj_set_width(ui_UpBGPanel, LCD_WIDTH); // 240,铺满宽度 +lv_obj_set_height(ui_UpBGPanel, 130); // 下拉栏内容区高度 +lv_obj_set_x(ui_UpBGPanel, 0); +lv_obj_set_y(ui_UpBGPanel, -10); +lv_obj_set_align(ui_UpBGPanel, LV_ALIGN_TOP_MID); + +// 不需要接收点击和滚动(让事件穿透到按钮) +lv_obj_clear_flag(ui_UpBGPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); + +// 半透明深灰色背景 —— 这就是"透明效果"的来源 +lv_obj_set_style_bg_color(ui_UpBGPanel, lv_color_hex(0x323232), LV_PART_MAIN | LV_STATE_DEFAULT); +lv_obj_set_style_bg_opa(ui_UpBGPanel, 200, LV_PART_MAIN | LV_STATE_DEFAULT); +// 200/255 ≈ 78% 不透明度,底层表盘内容隐约可见 +``` + +**透明度值参考**: +| bg_opa 值 | 效果 | +|-----------|------| +| 0 | 完全透明,等于没有背景 | +| 128 | 50% 半透明 | +| 200 | ~78% 不透明(本项目使用) | +| 255 | 完全不透明,完全遮挡底层 | + +### 4.4 功能按钮(在容器内布局) + +按钮直接创建在 `ui_DropDownPanel` 上(不是 `ui_UpBGPanel` 上),通过坐标定位: + +```c +// 示例:NFC 按钮 +ui_NFCButton = lv_btn_create(ui_DropDownPanel); +lv_obj_set_width(ui_NFCButton, 50); +lv_obj_set_height(ui_NFCButton, 50); +lv_obj_set_x(ui_NFCButton, 0); +lv_obj_set_y(ui_NFCButton, 5); + +// 支持开关切换(CHECKABLE) +lv_obj_add_flag(ui_NFCButton, LV_OBJ_FLAG_CHECKABLE | LV_OBJ_FLAG_SCROLL_ON_FOCUS); +lv_obj_clear_flag(ui_NFCButton, LV_OBJ_FLAG_SCROLLABLE); + +// 默认灰色,选中时蓝色 +lv_obj_set_style_bg_color(ui_NFCButton, lv_color_hex(0x808080), LV_PART_MAIN | LV_STATE_DEFAULT); +lv_obj_set_style_bg_opa(ui_NFCButton, 255, LV_PART_MAIN | LV_STATE_DEFAULT); +lv_obj_set_style_bg_color(ui_NFCButton, lv_color_hex(0x3264C8), LV_PART_MAIN | LV_STATE_CHECKED); +lv_obj_set_style_bg_opa(ui_NFCButton, 255, LV_PART_MAIN | LV_STATE_CHECKED); +``` + +### 4.5 底部透明占位面板 `ui_DownBGPanel` + +完全透明,唯一作用是**撑大容器的可滚动区域**,保证下拉栏可以被完全收起: + +```c +ui_DownBGPanel = lv_obj_create(ui_DropDownPanel); +lv_obj_set_width(ui_DownBGPanel, LCD_WIDTH); +lv_obj_set_height(ui_DownBGPanel, 130); +lv_obj_set_y(ui_DownBGPanel, 420); // 放在容器最底部 +lv_obj_set_align(ui_DownBGPanel, LV_ALIGN_TOP_MID); +lv_obj_clear_flag(ui_DownBGPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); +lv_obj_set_style_bg_opa(ui_DownBGPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); // 完全透明 +lv_obj_set_style_border_opa(ui_DownBGPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); // 边框也透明 +``` + +### 4.6 事件注册 + +```c +lv_obj_add_event_cb(ui_HomePage, ui_event_HomePage, LV_EVENT_ALL, NULL); // 主界面手势 +lv_obj_add_event_cb(ui_NFCButton, ui_event_NFCButton, LV_EVENT_ALL, NULL); // NFC 开关 +lv_obj_add_event_cb(ui_BLEButton, ui_event_BLEButton, LV_EVENT_ALL, NULL); // 蓝牙开关 +lv_obj_add_event_cb(ui_PowerButton, ui_event_PowerButton, LV_EVENT_ALL, NULL); // 电源 +lv_obj_add_event_cb(ui_SetButton, ui_event_SetButton, LV_EVENT_ALL, NULL); // 设置 +lv_obj_add_event_cb(ui_LightSlider, ui_event_LightSlider, LV_EVENT_ALL, NULL); // 亮度 +``` + +## 五、滚动状态示意图 + +``` + ┌──────────────┐ + │ ui_UpBGPanel │ ← 初始被 scroll_by(-130) 藏在屏幕上方 + │ (opa=200) │ + │ NFC BLE 按钮 │ + ═════════════════├══════════════╡══════ 屏幕顶部 ═══ + │ │ + 收起状态 │ (透明区域) │ ← 用户只看到底层表盘 + scroll_y = 130 │ │ + │ │ + ═════════════════├══════════════╡══════ 屏幕底部 ═══ + │ui_DownBGPanel│ ← 透明占位 + └──────────────┘ + + ─── 用户下拉后 ─── + + ═════════════════╤══════════════╕══════ 屏幕顶部 ═══ + │ ui_UpBGPanel │ ← 滑入可见区域 + 展开状态 │ (opa=200) │ 半透明,底层表盘隐约可见 + scroll_y = 0 │ NFC BLE 按钮 │ + ├──────────────┤ + │ │ + ═════════════════├══════════════╡══════ 屏幕底部 ═══ + │ui_DownBGPanel│ + └──────────────┘ +``` + +## 六、进阶:下拉渐变透明度(从半透明到不透明) + +OV-Watch 原版的 `bg_opa` 是固定值 200。如果想实现**随下拉幅度从半透明渐变到完全不透明**,需要添加滚动事件回调,根据 `scroll_y` 动态计算透明度。 + +### 6.1 原理 + +``` +scroll_y = 130 (完全收起) → opa 不关心(不可见) +scroll_y = 100 (刚露头) → opa = OPA_MIN(如 150,半透明) +scroll_y = 0 (完全展开) → opa = 255(完全不透明,遮挡底层) + +映射公式: opa = 255 - scroll_y * (255 - OPA_MIN) / CONTENT_HEIGHT +``` + +### 6.2 实现代码 + +```c +// 可调参数 +#define DROPDOWN_CONTENT_HEIGHT 130 // 下拉栏内容区高度 +#define DROPDOWN_OPA_MIN 150 // 刚出现时的最低透明度 +#define DROPDOWN_OPA_MAX 255 // 完全展开时的最高透明度(不透明) + +// 滚动事件回调 +void ui_event_DropDownPanel_scroll(lv_event_t * e) +{ + lv_obj_t * panel = lv_event_get_target(e); + if (lv_event_get_code(e) != LV_EVENT_SCROLL) return; + + // 获取当前垂直滚动偏移量 + // scroll_y = 130: 完全收起(内容在屏幕上方) + // scroll_y = 0: 完全展开(内容完全可见) + lv_coord_t scroll_y = lv_obj_get_scroll_y(panel); + + // 线性映射: scroll_y → opa + int32_t opa = DROPDOWN_OPA_MAX + - (scroll_y * (DROPDOWN_OPA_MAX - DROPDOWN_OPA_MIN)) + / DROPDOWN_CONTENT_HEIGHT; + + // 钳位到有效范围 + if (opa < DROPDOWN_OPA_MIN) opa = DROPDOWN_OPA_MIN; + if (opa > DROPDOWN_OPA_MAX) opa = DROPDOWN_OPA_MAX; + + // 动态设置半透明背景面板的不透明度 + lv_obj_set_style_bg_opa(ui_UpBGPanel, (lv_opa_t)opa, + LV_PART_MAIN | LV_STATE_DEFAULT); +} +``` + +### 6.3 注册回调 + +在 `ui_HomePage_screen_init()` 的事件注册区域添加: + +```c +// 在原有事件注册之后添加 +lv_obj_add_event_cb(ui_DropDownPanel, ui_event_DropDownPanel_scroll, + LV_EVENT_SCROLL, NULL); +// 注意:这里只监听 LV_EVENT_SCROLL,不是 LV_EVENT_ALL,减少不必要的回调开销 +``` + +### 6.4 性能说明 + +- `LV_EVENT_SCROLL` 仅在滚动时触发,静止时无开销 +- `lv_obj_set_style_bg_opa()` 只修改一个属性值,LVGL 在下一帧渲染时应用 +- 额外 CPU 开销极小,适合 STM32F411 等资源有限的 MCU + +### 6.5 效果对比 + +| 状态 | 原版 (固定 opa=200) | 渐变版 | +|------|---------------------|--------| +| 刚露头 | 78% 不透明 | ~59% 不透明(opa=150),更通透 | +| 半程 | 78% 不透明 | ~80% 不透明(opa=203) | +| 完全展开 | 78% 不透明 | 100% 不透明(opa=255),完全遮挡 | + +## 七、在新项目中复现的最小步骤 + +### 步骤 1:在主屏幕 init 中创建透明滚动容器 + +```c +lv_obj_t * dropdown = lv_obj_create(main_screen); +lv_obj_set_size(dropdown, LCD_WIDTH, LCD_HEIGHT + CONTENT_HEIGHT * 2); +lv_obj_set_align(dropdown, LV_ALIGN_TOP_MID); +lv_obj_scroll_by(dropdown, 0, -CONTENT_HEIGHT, LV_ANIM_OFF); +lv_obj_clear_flag(dropdown, LV_OBJ_FLAG_SCROLL_ELASTIC | LV_OBJ_FLAG_SCROLL_CHAIN); +lv_obj_set_scroll_dir(dropdown, LV_DIR_VER); +// 容器自身完全透明 +lv_obj_set_style_bg_opa(dropdown, 0, LV_PART_MAIN); +lv_obj_set_style_border_opa(dropdown, 0, LV_PART_MAIN); +lv_obj_set_style_bg_opa(dropdown, 0, LV_PART_SCROLLBAR); +lv_obj_set_style_bg_opa(dropdown, 0, LV_PART_SCROLLBAR | LV_STATE_SCROLLED); +``` + +### 步骤 2:创建半透明背景面板 + +```c +lv_obj_t * bg_panel = lv_obj_create(dropdown); +lv_obj_set_size(bg_panel, LCD_WIDTH, CONTENT_HEIGHT); +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(0x323232), LV_PART_MAIN); +lv_obj_set_style_bg_opa(bg_panel, 200, LV_PART_MAIN); // 半透明 +``` + +### 步骤 3:在背景面板区域内添加功能控件 + +```c +lv_obj_t * btn = lv_btn_create(dropdown); // 注意:挂在 dropdown 上,不是 bg_panel +lv_obj_set_size(btn, 50, 50); +lv_obj_set_pos(btn, 10, 10); +// ... 添加更多控件 +``` + +### 步骤 4:创建底部透明占位 + +```c +lv_obj_t * spacer = lv_obj_create(dropdown); +lv_obj_set_size(spacer, LCD_WIDTH, CONTENT_HEIGHT); +lv_obj_set_y(spacer, LCD_HEIGHT + CONTENT_HEIGHT); // 放在最底部 +lv_obj_clear_flag(spacer, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); +lv_obj_set_style_bg_opa(spacer, 0, LV_PART_MAIN); +lv_obj_set_style_border_opa(spacer, 0, LV_PART_MAIN); +``` + +### 步骤 5(可选):添加渐变透明度回调 + +```c +lv_obj_add_event_cb(dropdown, ui_event_DropDownPanel_scroll, LV_EVENT_SCROLL, NULL); +``` + +回调函数见第六章。 + +## 八、SquareLine Studio 的局限 + +| 功能 | SquareLine 能做 | 需要手写代码 | +|------|:-:|:-:| +| 创建容器、面板、按钮等控件 | Yes | - | +| 设置固定的 bg_opa 半透明值 | Yes | - | +| 设置滚动方向 LV_DIR_VER | Yes | - | +| `lv_obj_scroll_by()` 初始偏移 | **No** | **Yes** | +| 禁用 SCROLL_ELASTIC / SCROLL_CHAIN | 部分 | 可能需调整 | +| LV_EVENT_SCROLL 回调动态改 opa | **No** | **Yes** | +| 隐藏滚动条样式 | Yes | - | + +**结论**:SquareLine 适合搭建静态布局和样式,但 `scroll_by` 初始偏移和滚动驱动的动态透明度必须在导出代码后手动添加。推荐工作流: + +> SquareLine 设计布局 → 导出 C 代码 → 手动添加 `scroll_by` + 滚动回调 + +## 九、注意事项 + +1. **主屏幕必须禁用滚动**:否则下拉手势会被主屏幕捕获 +2. **禁用 SCROLL_ELASTIC**:弹性回弹会让下拉栏位置不稳定 +3. **禁用 SCROLL_CHAIN**:防止滚动事件冒泡到父对象 +4. **按钮挂在 dropdown 容器上**,不是 bg_panel 上:bg_panel 已禁用 CLICKABLE,按钮挂在它上面会收不到点击 +5. **占位面板的 y 坐标**要足够大,确保收起时内容区完全不可见 +6. **容器高度计算**:必须 >= 内容区高度 + 屏幕高度,否则滚动范围不够 diff --git a/docs/ScreenSet下拉overlay实现方案.md b/docs/ScreenSet下拉overlay实现方案.md new file mode 100644 index 0000000..1ed4c09 --- /dev/null +++ b/docs/ScreenSet下拉overlay实现方案.md @@ -0,0 +1,662 @@ +# 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,减少渲染面积 diff --git a/sdkconfig b/sdkconfig index 06d0a6a..65258af 100644 --- a/sdkconfig +++ b/sdkconfig @@ -79,6 +79,7 @@ CONFIG_SOC_ADC_SHARED_POWER=y CONFIG_SOC_APB_BACKUP_DMA=y CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y CONFIG_SOC_SHARED_IDCACHE_SUPPORTED=y +CONFIG_SOC_CACHE_FREEZE_SUPPORTED=y CONFIG_SOC_CACHE_MEMORY_IBANK_SIZE=0x4000 CONFIG_SOC_CPU_CORES_NUM=1 CONFIG_SOC_CPU_INTR_NUM=32 @@ -747,6 +748,7 @@ CONFIG_BT_BLE_50_EXTEND_SCAN_EN=y CONFIG_BT_BLE_50_EXTEND_SYNC_EN=y CONFIG_BT_BLE_50_DTM_TEST_EN=y # CONFIG_BT_BLE_42_FEATURES_SUPPORTED is not set +CONFIG_BT_BLE_VENDOR_HCI_EN=y # CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set # CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set # end of Bluedroid Options @@ -952,6 +954,7 @@ CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y # CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set # CONFIG_ESP_TLS_PSK_VERIFICATION is not set # CONFIG_ESP_TLS_INSECURE is not set +CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y # end of ESP-TLS # @@ -1272,8 +1275,11 @@ CONFIG_ESP_PHY_RF_CAL_PARTIAL=y # CONFIG_ESP_PHY_RF_CAL_NONE is not set # CONFIG_ESP_PHY_RF_CAL_FULL is not set CONFIG_ESP_PHY_CALIBRATION_MODE=0 +CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000 # CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set # CONFIG_ESP_PHY_RECORD_USED_TIME is not set +CONFIG_ESP_PHY_IRAM_OPT=y +# CONFIG_ESP_PHY_DEBUG is not set # end of PHY # @@ -1862,6 +1868,7 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 # CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set # CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y +# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set CONFIG_MBEDTLS_PKCS7_C=y # end of mbedTLS v3.x related