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