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