3. 事件类型详解:LV_EVENT_CLICKED、LV_EVENT_PRESSING、LV_EVENT_VALUE_CHANGED 等核心事件类型

好,咱们接着聊。上一节我们把事件驱动模型的骨架搭起来了,知道了事件是怎么注册、怎么派发的。但光知道骨架还不够,你得知道每个「事件」到底长什么样、什么时候触发、能拿来干什么。

说白了,LVGL 里的事件类型有几十种,但真正天天打交道的,也就那么七八个。我个人习惯是把它们分成三类:触摸类状态类值变化类。今天咱们就把这三类里最核心的几个,一个一个掰开揉碎了讲清楚。

3.1 LV_EVENT_CLICKED:最常用的「点击」事件

这个事件,你肯定不陌生。按钮按下去、弹起来,就算一次点击。但这里有个细节——它不是在按下时触发的,而是在释放时触发的

为什么会这样?你想想看,用户按下一个按钮,可能突然反悔了,手指滑出去再松开。如果按下就触发,那误触率就太高了。LVGL 的设计很聪明:只有按下和释放都在同一个对象上,才算一次有效的 CLICKED

核心要点:LV_EVENT_CLICKED 是「按下 + 释放」的组合事件,不是单纯的按下。

我在项目中遇到过一个问题:有个同事用 LV_EVENT_PRESSED 来做按钮响应,结果用户手指轻轻一碰就触发了,体验非常糟糕。后来改成 LV_EVENT_CLICKED,问题就解决了。

// 典型用法:按钮点击回调
static void btn_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_CLICKED) {
        LV_LOG_USER("按钮被点击了!");
        // 执行你的业务逻辑
    }
}

小技巧:如果你需要区分「短按」和「长按」,可以用 LV_EVENT_SHORT_CLICKEDLV_EVENT_LONG_PRESSED。这两个事件是 CLICKED 的细化版本。

3.2 LV_EVENT_PRESSING:持续按下的「进行时」

这个事件很有意思。它不是一次性的,而是在手指按住不松的过程中,持续不断地触发。你想想看,什么场景需要它?

嗯,最常见的例子就是「长按加速」——比如音量键,按着不放,音量持续增加。还有游戏里的虚拟摇杆,手指按在上面,角色持续移动。

我记得有一次做智能家居面板,需要实现一个「长按调光」的功能。用户按住灯光图标,亮度从 0% 慢慢增加到 100%。如果用 CLICKED,只能做一次跳变;用 PRESSING,就能在回调里不断更新亮度值。

// 长按调光示例
static void dimmer_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_PRESSING) {
        static uint8_t brightness = 0;
        brightness += 5;
        if(brightness > 100) brightness = 100;
        // 更新亮度显示
        lv_bar_set_value(brightness_bar, brightness, LV_ANIM_ON);
        LV_LOG_USER("当前亮度:%d%%", brightness);
    }
}

注意:PRESSING 事件的触发频率很高(默认每 20ms 一次)。如果你的回调里有耗时操作,比如读写 Flash 或网络请求,一定要做节流处理。我曾经就因为这个踩过坑——长按 3 秒,Flash 被写了 150 次,直接写坏了。

3.3 LV_EVENT_VALUE_CHANGED:值变化时的「通知」

这个事件,可以说是控件专属事件里最常用的。它不关心你怎么按、怎么滑,只关心一件事:控件的值变了没有

哪些控件会触发它?很多:

  • 滑块(Slider):拖动时值变化
  • 开关(Switch):切换时值变化
  • 下拉列表(Dropdown):选中项变化
  • 复选框(Checkbox):勾选状态变化
  • 滚动条(Roller):滚动到新选项

说白了,只要控件的「数值」或「状态」发生了改变,就会触发 VALUE_CHANGED。你不需要自己去轮询检查,LVGL 会主动通知你。

// 滑块值变化回调
static void slider_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_VALUE_CHANGED) {
        lv_obj_t * slider = lv_event_get_target(e);
        int32_t val = lv_slider_get_value(slider);
        // 更新显示文本
        lv_label_set_text_fmt(value_label, "%d", val);
    }
}

一个容易混淆的点:VALUE_CHANGEDCLICKED 有什么区别?

简单说:CLICKED 是「操作完成」事件,VALUE_CHANGED 是「数值变化」事件。比如一个开关,你点一下它,两个事件都会触发。但如果你用代码 lv_switch_set_state(sw, true) 来改变状态,只有 VALUE_CHANGED 会触发,CLICKED 不会。

3.4 其他几个常用事件

除了上面三个,还有几个事件我平时也经常用,简单列一下:

事件名称 触发时机 典型用途
LV_EVENT_PRESSED 手指刚按下时 按钮按下效果(变色、缩小)
LV_EVENT_RELEASED 手指松开时(无论是否在对象上) 恢复按钮样式
LV_EVENT_LONG_PRESSED 按住超过一定时间(默认 400ms) 长按菜单、删除确认
LV_EVENT_FOCUSED 对象获得焦点(键盘/遥控器模式) 高亮当前选中项
LV_EVENT_DEFOCUSED 对象失去焦点 取消高亮

我的个人习惯:在做复杂交互时,我通常会在一个回调里同时处理多个事件,用 switchif-else 分支。这样代码集中,逻辑清晰。比如一个按钮,按下时变暗,释放时恢复,点击时执行动作——三个事件,一个回调搞定。

3.5 实战:组合使用多个事件

光说不练假把式。咱们来看一个实际例子:做一个「长按删除」的按钮。

需求是这样的:用户短按按钮,执行普通操作;长按 1.5 秒,弹出确认对话框。这里就需要组合使用 CLICKEDLONG_PRESSED

static void delete_btn_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    
    switch(code) {
        case LV_EVENT_CLICKED:
            LV_LOG_USER("短按:执行普通操作");
            // 比如打开设置页面
            break;
            
        case LV_EVENT_LONG_PRESSED:
            LV_LOG_USER("长按:弹出删除确认");
            // 创建确认对话框
            lv_obj_t * mbox = lv_msgbox_create(NULL, "确认删除", 
                                               "确定要删除这条记录吗?", 
                                               NULL, true);
            lv_obj_center(mbox);
            break;
            
        case LV_EVENT_PRESSED:
            // 按下时视觉反馈
            lv_obj_set_style_bg_color(btn, lv_palette_darken(LV_PALETTE_RED, 2), 0);
            break;
            
        case LV_EVENT_RELEASED:
            // 松开时恢复样式
            lv_obj_set_style_bg_color(btn, lv_palette_main(LV_PALETTE_RED), 0);
            break;
    }
}

你看,一个回调里处理了四个事件,各司其职。这就是事件驱动模型的魅力——你不用去管「什么时候该做什么」,你只需要告诉 LVGL:「当这个发生时,做这个;当那个发生时,做那个」。剩下的,框架帮你搞定。

避坑指南:我曾经在 LONG_PRESSED 的回调里创建了一个模态对话框,结果对话框挡住了按钮,导致 RELEASED 事件无法触发,按钮样式一直卡在「按下」状态。后来我在创建对话框之前,手动调了一下 lv_event_send(btn, LV_EVENT_RELEASED, NULL),才解决了这个问题。

3.6 小结

今天咱们把几个核心事件类型过了一遍。记住一句话:选对事件,事半功倍

  • 需要「点击完成」的反馈?用 CLICKED
  • 需要「持续按住」的效果?用 PRESSING
  • 需要「数值变化」的通知?用 VALUE_CHANGED
  • 需要「长按」的特殊操作?用 LONG_PRESSED

下一节,咱们会深入讲讲「事件冒泡」和「事件拦截」——这两个概念,在复杂 UI 里几乎是必用的。到时候我会拿一个实际项目里的例子,带你看看怎么用事件冒泡来简化代码。

嗯,今天就到这儿。有问题随时交流。