14 KiB
14 KiB
LVGL 下拉通知栏实现指南
基于 OV-Watch 项目(STM32F411 + LVGL 8.2)的下拉栏实现分析,适用于在其他 LVGL 项目中复现类似效果。
一、效果描述
在主界面(表盘)上方隐藏一个通知/快捷操作面板,用户手指下拉时面板滑入,上推时收回。类似 Android/iOS 的通知栏下拉效果。
二、核心原理
不使用任何自定义绘制或动画 API,纯粹利用 LVGL 原生的滚动机制:
- 在主界面上创建一个超出屏幕高度的透明滚动容器
- 将下拉栏内容放在容器顶部
- 通过
lv_obj_scroll_by()设置初始偏移,将内容滚到屏幕上方不可见区域 - 用户触摸下拉时,LVGL 滚动机制自然将内容带回视口
三、对象层级结构
ui_HomePage (主屏幕,禁用滚动)
├── 时间、电量、步数等表盘控件 ...
│
└── ui_DropDownPanel (透明滚动容器,覆盖全屏)
├── ui_UpBGPanel (半透明背景面板,承载视觉效果)
├── ui_NFCButton (功能按钮)
├── ui_BLEButton
├── ui_PowerButton
├── ui_SetButton
├── ui_LightSlider (亮度滑块)
└── ui_DownBGPanel (透明占位面板,撑大滚动区域)
四、各层详细实现
4.1 主屏幕:禁用滚动
主屏幕自身必须禁止滚动,否则会和下拉容器的滚动冲突:
lv_obj_clear_flag(ui_HomePage, LV_OBJ_FLAG_SCROLLABLE);
4.2 外层透明滚动容器 ui_DropDownPanel
这是整个下拉效果的核心。容器本身完全透明,只提供滚动能力。
// 创建容器,挂在主屏幕上
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 控制透明度:
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 上),通过坐标定位:
// 示例: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
完全透明,唯一作用是撑大容器的可滚动区域,保证下拉栏可以被完全收起:
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 事件注册
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 实现代码
// 可调参数
#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() 的事件注册区域添加:
// 在原有事件注册之后添加
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 中创建透明滚动容器
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:创建半透明背景面板
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:在背景面板区域内添加功能控件
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:创建底部透明占位
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(可选):添加渐变透明度回调
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+ 滚动回调
九、注意事项
- 主屏幕必须禁用滚动:否则下拉手势会被主屏幕捕获
- 禁用 SCROLL_ELASTIC:弹性回弹会让下拉栏位置不稳定
- 禁用 SCROLL_CHAIN:防止滚动事件冒泡到父对象
- 按钮挂在 dropdown 容器上,不是 bg_panel 上:bg_panel 已禁用 CLICKABLE,按钮挂在它上面会收不到点击
- 占位面板的 y 坐标要足够大,确保收起时内容区完全不可见
- 容器高度计算:必须 >= 内容区高度 + 屏幕高度,否则滚动范围不够