Dzbj_C3_Key/docs/LVGL下拉通知栏实现指南.md

14 KiB
Raw Permalink Blame History

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 主屏幕:禁用滚动

主屏幕自身必须禁止滚动,否则会和下拉容器的滚动冲突:

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 + 滚动回调

九、注意事项

  1. 主屏幕必须禁用滚动:否则下拉手势会被主屏幕捕获
  2. 禁用 SCROLL_ELASTIC:弹性回弹会让下拉栏位置不稳定
  3. 禁用 SCROLL_CHAIN:防止滚动事件冒泡到父对象
  4. 按钮挂在 dropdown 容器上,不是 bg_panel 上bg_panel 已禁用 CLICKABLE按钮挂在它上面会收不到点击
  5. 占位面板的 y 坐标要足够大,确保收起时内容区完全不可见
  6. 容器高度计算:必须 >= 内容区高度 + 屏幕高度,否则滚动范围不够