Dzbj_ESP32-C3/docs/LVGL下拉通知栏实现指南.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

343 lines
14 KiB
Markdown
Raw 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.

# 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. **容器高度计算**:必须 >= 内容区高度 + 屏幕高度,否则滚动范围不够