[LVGL] 从0开始,学LVGL: 界面布局与用户交互

第3部分:界面布局与用户交互

第7章:事件的奥秘

7.1 事件是什么?理解事件驱动架构

在前面的章节中,我们创建了漂亮的界面,但它们还缺乏"灵魂"——交互性。事件系统就是LVGL的"神经系统",它让界面能够感知和响应用户操作。

事件驱动的数学模型:

在事件驱动系统中,程序的执行流程由事件序列决定:

P = f ( E 1 , E 2 , E 3 , . . . , E n ) P = f(E_1, E_2, E_3, ..., E_n) P=f(E1,E2,E3,...,En)

其中:

  • P P P = 程序行为
  • E i E_i Ei = 第i个事件
  • f f f = 事件处理函数

每个事件包含关键信息:
E = ⟨ type , target , data , timestamp ⟩ E = \langle \text{type}, \text{target}, \text{data}, \text{timestamp} \rangle E=type,target,data,timestamp

7.2 事件处理流程

LVGL的事件处理遵循一个清晰的管道模型:

继续
停止
硬件输入
输入设备驱动
LVGL核心事件分发
事件预处理
对象事件回调?
执行用户回调
默认事件处理
事件冒泡?
传递给父对象
事件处理结束

7.3 核心事件类型详解

LVGL定义了丰富的事件类型,以下是其中最常用的:

事件类型触发条件典型应用场景
LV_EVENT_CLICKED对象被点击后释放按钮点击、菜单项选择
LV_EVENT_PRESSED对象被按下时触摸反馈、按下状态
LV_EVENT_RELEASED对象被释放时结束动画、状态恢复
LV_EVENT_VALUE_CHANGED对象值发生变化滑块调节、开关切换
LV_EVENT_FOCUSED对象获得焦点导航高亮、键盘输入
LV_EVENT_DEFOCUSED对象失去焦点验证输入、保存状态
LV_EVENT_SHORT_CLICKED短点击事件快速操作
LV_EVENT_LONG_PRESSED长按事件上下文菜单、高级操作

7.4 实战:构建完整的事件处理系统

让我们通过一个综合示例来掌握事件处理:

#include <lvgl.h>

// 全局状态变量
static struct {
    int click_count;
    int slider_value;
    bool switch_state;
} app_state = {0, 50, false};

/**
 * 通用事件处理函数 - 演示多对象事件处理
 */
static void event_handler(lv_event_t * e) {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    void * user_data = lv_event_get_user_data(e);
    
    const char * obj_name = (const char *)user_data;
    
    switch(event_code) {
        case LV_EVENT_CLICKED:
            printf("[%s] 被点击\n", obj_name);
            
            if(strcmp(obj_name, "counter_btn") == 0) {
                // 计数器按钮点击处理
                app_state.click_count++;
                lv_label_set_text_fmt((lv_obj_t *)lv_obj_get_user_data(target), 
                                    "点击次数: %d", app_state.click_count);
            }
            else if(strcmp(obj_name, "reset_btn") == 0) {
                // 重置按钮点击处理
                app_state.click_count = 0;
                lv_label_set_text_fmt(lv_obj_get_child(lv_obj_get_parent(target), 0), 
                                    "点击次数: %d", app_state.click_count);
            }
            break;
            
        case LV_EVENT_VALUE_CHANGED:
            printf("[%s] 值改变\n", obj_name);
            
            if(strcmp(obj_name, "brightness_slider") == 0) {
                // 亮度滑块处理
                app_state.slider_value = lv_slider_get_value(target);
                lv_label_set_text_fmt((lv_obj_t *)lv_obj_get_user_data(target), 
                                    "亮度: %d%%", app_state.slider_value);
            }
            else if(strcmp(obj_name, "power_switch") == 0) {
                // 电源开关处理
                app_state.switch_state = lv_obj_has_state(target, LV_STATE_CHECKED);
                const char * state_text = app_state.switch_state ? "已开启" : "已关闭";
                lv_label_set_text((lv_obj_t *)lv_obj_get_user_data(target), state_text);
                
                // 联动控制:开关关闭时禁用滑块
                lv_obj_t * slider = (lv_obj_t *)lv_obj_get_user_data(target);
                if(app_state.switch_state) {
                    lv_obj_clear_state(slider, LV_STATE_DISABLED);
                } else {
                    lv_obj_add_state(slider, LV_STATE_DISABLED);
                }
            }
            break;
            
        case LV_EVENT_PRESSED:
            // 添加按下动画效果
            lv_anim_t a;
            lv_anim_init(&a);
            lv_anim_set_var(&a, target);
            lv_anim_set_values(&a, 255, 200);
            lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);
            lv_anim_set_time(&a, 100);
            lv_anim_start(&a);
            break;
            
        case LV_EVENT_RELEASED:
            // 恢复透明度
            lv_obj_set_style_opa(target, LV_OPA_COVER, 0);
            break;
    }
}

/**
 * 创建交互式控制面板
 */
void create_interactive_panel() {
    lv_obj_t * parent = lv_scr_act();
    
    // 创建主容器
    lv_obj_t * container = lv_obj_create(parent);
    lv_obj_set_size(container, 300, 400);
    lv_obj_center(container);
    lv_obj_set_style_bg_color(container, lv_color_hex(0x2C3E50), 0);
    lv_obj_set_style_radius(container, 15, 0);
    lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_style_pad_all(container, 20, 0);
    lv_obj_set_style_pad_gap(container, 15, 0);
    
    // 1. 计数器区域
    lv_obj_t * counter_label = lv_label_create(container);
    lv_label_set_text_fmt(counter_label, "点击次数: %d", app_state.click_count);
    lv_obj_set_style_text_color(counter_label, lv_color_white(), 0);
    lv_obj_set_style_text_font(counter_label, &lv_font_montserrat_18, 0);
    
    lv_obj_t * counter_btn = lv_btn_create(container);
    lv_obj_set_size(counter_btn, 120, 40);
    lv_obj_t * btn_label = lv_label_create(counter_btn);
    lv_label_set_text(btn_label, "点击我!");
    lv_obj_center(btn_label);
    
    // 存储计数器标签的引用,用于在事件中更新
    lv_obj_set_user_data(counter_btn, counter_label);
    lv_obj_add_event_cb(counter_btn, event_handler, LV_EVENT_ALL, (void*)"counter_btn");
    
    // 2. 亮度控制区域
    lv_obj_t * brightness_label = lv_label_create(container);
    lv_label_set_text_fmt(brightness_label, "亮度: %d%%", app_state.slider_value);
    lv_obj_set_style_text_color(brightness_label, lv_color_white(), 0);
    
    lv_obj_t * brightness_slider = lv_slider_create(container);
    lv_obj_set_size(brightness_slider, 200, 20);
    lv_slider_set_range(brightness_slider, 0, 100);
    lv_slider_set_value(brightness_slider, app_state.slider_value, LV_ANIM_OFF);
    
    // 存储亮度标签的引用
    lv_obj_set_user_data(brightness_slider, brightness_label);
    lv_obj_add_event_cb(brightness_slider, event_handler, LV_EVENT_ALL, (void*)"brightness_slider");
    
    // 3. 电源控制区域
    lv_obj_t * power_label = lv_label_create(container);
    lv_label_set_text(power_label, app_state.switch_state ? "已开启" : "已关闭");
    lv_obj_set_style_text_color(power_label, lv_color_white(), 0);
    
    lv_obj_t * power_switch = lv_switch_create(container);
    lv_obj_set_size(power_switch, 60, 30);
    if(app_state.switch_state) {
        lv_obj_add_state(power_switch, LV_STATE_CHECKED);
    }
    
    // 存储电源标签和滑块的引用(用于联动控制)
    lv_obj_set_user_data(power_switch, power_label);
    lv_obj_set_user_data(power_label, brightness_slider); // 在power_label中存储slider引用
    lv_obj_add_event_cb(power_switch, event_handler, LV_EVENT_ALL, (void*)"power_switch");
    
    // 4. 重置按钮
    lv_obj_t * reset_btn = lv_btn_create(container);
    lv_obj_set_size(reset_btn, 100, 35);
    lv_obj_set_style_bg_color(reset_btn, lv_color_hex(0xE74C3C), 0);
    
    lv_obj_t * reset_label = lv_label_create(reset_btn);
    lv_label_set_text(reset_label, "重置");
    lv_obj_center(reset_label);
    lv_obj_add_event_cb(reset_btn, event_handler, LV_EVENT_ALL, (void*)"reset_btn");
}

7.5 高级事件技巧

事件冒泡机制:
事件会从目标对象向上冒泡到父对象,直到被处理或到达屏幕。

// 阻止事件冒泡
lv_obj_add_event_cb(obj, [](lv_event_t * e) {
    lv_event_stop_bubbling(e);  // 阻止事件继续向上传递
}, LV_EVENT_CLICKED, NULL);

事件用户数据:
通过用户数据在不同事件回调间共享信息:

typedef struct {
    lv_obj_t * label;
    lv_obj_t * slider;
    int min_value;
    int max_value;
} event_data_t;

// 创建并传递数据结构
event_data_t * data = malloc(sizeof(event_data_t));
data->label = my_label;
data->slider = my_slider;
data->min_value = 0;
data->max_value = 100;

lv_obj_add_event_cb(slider, event_handler, LV_EVENT_ALL, data);

第8章:强大的布局助手 - Flex布局

8.1 为什么需要自动布局?

在传统UI开发中,我们手动设置每个对象的x、y坐标。这种方法存在明显问题:

  • 难以维护:修改一个元素需要重新计算所有相关元素位置
  • 响应式差:无法适应不同屏幕尺寸
  • 代码冗余:重复的位置计算逻辑

Flex布局通过数学模型自动计算元素位置:

设容器尺寸为 ( W c , H c ) (W_c, H_c) (Wc,Hc),包含 n n n 个子元素,每个元素尺寸为 ( w i , h i ) (w_i, h_i) (wi,hi)

Flex布局的目标是找到最优的位置分配函数:
P i = F ( W c , H c , { w j , h j } j = 1 n , align , flow ) P_i = F(W_c, H_c, \{w_j, h_j\}_{j=1}^n, \text{align}, \text{flow}) Pi=F(Wc,Hc,{wj,hj}j=1n,align,flow)

8.2 Flex布局核心概念

主轴与交叉轴:

  • 主轴:主要排列方向
  • 交叉轴:与主轴垂直的方向
Flex容器
主轴方向 row
主轴方向 column
水平排列
从左到右 row
从右到左 row-reverse
垂直排列
从上到下 column
从下到上 column-reverse

8.3 Flex布局属性详解

容器属性:

// 设置排列方向
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);        // 水平排列
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);     // 垂直排列
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW_WRAP);   // 水平排列自动换行

// 设置主轴对齐
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START,     // 主轴起点
                             LV_FLEX_ALIGN_CENTER,        // 交叉轴居中
                             LV_FLEX_ALIGN_SPACE_EVENLY); // 跟踪对齐

// 设置内边距
lv_obj_set_style_pad_all(container, 10, 0);              // 统一内边距
lv_obj_set_style_pad_row(container, 5, 0);               // 行间距
lv_obj_set_style_pad_column(container, 5, 0);            // 列间距

子元素属性:

// 设置元素在Flex容器中的增长因子
lv_obj_set_flex_grow(item, 1);        // 占据剩余空间

// 设置对齐方式
lv_obj_set_style_align(item, LV_ALIGN_CENTER, 0);

// 设置基础大小
lv_obj_set_width(item, LV_PCT(30));   // 宽度30%

8.4 实战:使用Flex布局创建现代化界面

让我们用Flex布局重构前面的事件示例,创建一个真正响应式的界面:

/**
 * 使用Flex布局创建响应式控制面板
 */
void create_flex_layout_panel() {
    lv_obj_t * parent = lv_scr_act();
    
    // 创建主容器 - 使用Flex布局
    lv_obj_t * container = lv_obj_create(parent);
    lv_obj_set_size(container, 320, 480);
    lv_obj_center(container);
    lv_obj_set_style_bg_color(container, lv_color_hex(0x34495E), 0);
    lv_obj_set_style_radius(container, 20, 0);
    lv_obj_set_style_pad_all(container, 20, 0);
    
    // 关键:设置为垂直Flex布局
    lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(container, 
                         LV_FLEX_ALIGN_START,      // 主轴从开始排列
                         LV_FLEX_ALIGN_CENTER,     // 交叉轴居中
                         LV_FLEX_ALIGN_SPACE_EVENLY); // 均匀分布
    
    // 1. 标题区域
    lv_obj_t * title_section = lv_obj_create(container);
    lv_obj_set_size(title_section, LV_PCT(100), 60);
    lv_obj_set_style_bg_opa(title_section, LV_OPA_0, 0);    // 透明背景
    lv_obj_set_style_border_width(title_section, 0, 0);
    lv_obj_set_flex_flow(title_section, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(title_section, LV_FLEX_ALIGN_SPACE_BETWEEN, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    lv_obj_t * title = lv_label_create(title_section);
    lv_label_set_text(title, "智能家居控制");
    lv_obj_set_style_text_color(title, lv_color_white(), 0);
    lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
    
    lv_obj_t * status = lv_label_create(title_section);
    lv_label_set_text(status, "在线");
    lv_obj_set_style_text_color(status, lv_color_hex(0x2ECC71), 0);
    
    // 2. 卡片式布局 - 灯光控制
    lv_obj_t * light_card = create_flex_card(container, "💡 灯光控制", 120);
    
    // 在卡片内创建水平排列的控制组
    lv_obj_t * light_controls = lv_obj_create(light_card);
    lv_obj_set_size(light_controls, LV_PCT(100), LV_PCT(70));
    lv_obj_set_style_bg_opa(light_controls, LV_OPA_0, 0);
    lv_obj_set_flex_flow(light_controls, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(light_controls, LV_FLEX_ALIGN_SPACE_AROUND, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    // 开关控制
    lv_obj_t * switch_container = lv_obj_create(light_controls);
    lv_obj_set_size(switch_container, 80, 80);
    lv_obj_set_style_bg_opa(switch_container, LV_OPA_0, 0);
    lv_obj_set_flex_flow(switch_container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(switch_container, LV_FLEX_ALIGN_SPACE_AROUND, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    lv_obj_t * power_switch = lv_switch_create(switch_container);
    lv_obj_set_style_bg_color(power_switch, lv_color_hex(0x7F8C8D), LV_PART_MAIN);
    lv_obj_set_style_bg_color(power_switch, lv_color_hex(0x2ECC71), LV_PART_INDICATOR);
    
    lv_obj_t * switch_label = lv_label_create(switch_container);
    lv_label_set_text(switch_label, "电源");
    lv_obj_set_style_text_color(switch_label, lv_color_white(), 0);
    
    // 亮度滑块
    lv_obj_t * slider_container = lv_obj_create(light_controls);
    lv_obj_set_size(slider_container, 150, 80);
    lv_obj_set_style_bg_opa(slider_container, LV_OPA_0, 0);
    lv_obj_set_flex_flow(slider_container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(slider_container, LV_FLEX_ALIGN_SPACE_AROUND, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    lv_obj_t * brightness_slider = lv_slider_create(slider_container);
    lv_obj_set_width(brightness_slider, LV_PCT(100));
    lv_slider_set_range(brightness_slider, 0, 100);
    lv_slider_set_value(brightness_slider, 75, LV_ANIM_OFF);
    
    lv_obj_t * brightness_label = lv_label_create(slider_container);
    lv_label_set_text(brightness_label, "75%");
    lv_obj_set_style_text_color(brightness_label, lv_color_white(), 0);
    
    // 3. 卡片式布局 - 场景模式
    lv_obj_t * scene_card = create_flex_card(container, "🎭 场景模式", 100);
    
    // 场景按钮网格 - 使用换行Flex
    lv_obj_t * scene_grid = lv_obj_create(scene_card);
    lv_obj_set_size(scene_grid, LV_PCT(100), LV_PCT(70));
    lv_obj_set_style_bg_opa(scene_grid, LV_OPA_0, 0);
    lv_obj_set_flex_flow(scene_grid, LV_FLEX_FLOW_ROW_WRAP);
    lv_obj_set_flex_align(scene_grid, LV_FLEX_ALIGN_SPACE_EVENLY, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    const char * scenes[] = {"阅读", "影院", "睡眠", "聚会", "工作", "离家"};
    for(int i = 0; i < 6; i++) {
        lv_obj_t * scene_btn = lv_btn_create(scene_grid);
        lv_obj_set_size(scene_btn, 80, 35);
        lv_obj_set_style_bg_color(scene_btn, lv_color_hex(0x3498DB), 0);
        lv_obj_set_style_radius(scene_btn, 15, 0);
        
        lv_obj_t * scene_label = lv_label_create(scene_btn);
        lv_label_set_text(scene_label, scenes[i]);
        lv_obj_center(scene_label);
    }
    
    // 4. 底部操作栏
    lv_obj_t * bottom_bar = lv_obj_create(container);
    lv_obj_set_size(bottom_bar, LV_PCT(100), 50);
    lv_obj_set_style_bg_opa(bottom_bar, LV_OPA_30, 0);
    lv_obj_set_style_bg_color(bottom_bar, lv_color_white(), 0);
    lv_obj_set_style_radius(bottom_bar, 10, 0);
    lv_obj_set_flex_flow(bottom_bar, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(bottom_bar, LV_FLEX_ALIGN_SPACE_AROUND, 
                         LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    const char * actions[] = {"设置", "历史", "帮助", "关于"};
    for(int i = 0; i < 4; i++) {
        lv_obj_t * action_btn = lv_btn_create(bottom_bar);
        lv_obj_set_size(action_btn, 60, 35);
        lv_obj_set_style_bg_opa(action_btn, LV_OPA_0, 0);
        lv_obj_set_style_border_width(action_btn, 0, 0);
        
        lv_obj_t * action_label = lv_label_create(action_btn);
        lv_label_set_text(action_label, actions[i]);
        lv_obj_set_style_text_color(action_label, lv_color_white(), 0);
        lv_obj_center(action_label);
    }
}

/**
 * 创建Flex卡片通用函数
 */
lv_obj_t * create_flex_card(lv_obj_t * parent, const char * title, int height) {
    lv_obj_t * card = lv_obj_create(parent);
    lv_obj_set_size(card, LV_PCT(100), height);
    lv_obj_set_style_bg_color(card, lv_color_hex(0x4A6572), 0);
    lv_obj_set_style_radius(card, 12, 0);
    lv_obj_set_style_pad_all(card, 15, 0);
    lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, 
                         LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
    
    lv_obj_t * card_title = lv_label_create(card);
    lv_label_set_text(card_title, title);
    lv_obj_set_style_text_color(card_title, lv_color_white(), 0);
    lv_obj_set_style_text_font(card_title, &lv_font_montserrat_16, 0);
    
    return card;
}

8.5 Flex布局的数学原理

Flex布局的核心算法基于空间分配策略:

可用空间计算:
a v a i l a b l e = c o n t a i n e r S i z e − ∑ f i x e d S i z e s − p a d d i n g s available = containerSize - \sum fixedSizes - paddings available=containerSizefixedSizespaddings

弹性项尺寸计算:
对于每个弹性项,其最终尺寸为:
f i n a l S i z e = b a s e S i z e + g r o w F a c t o r ∑ g r o w F a c t o r s × a v a i l a b l e finalSize = baseSize + \frac{growFactor}{\sum growFactors} \times available finalSize=baseSize+growFactorsgrowFactor×available

对齐算法:

  • LV_FLEX_ALIGN_SPACE_BETWEEN:
    g a p = a v a i l a b l e n − 1 gap = \frac{available}{n-1} gap=n1available
  • LV_FLEX_ALIGN_SPACE_AROUND:
    g a p = a v a i l a b l e n gap = \frac{available}{n} gap=navailable
  • LV_FLEX_ALIGN_SPACE_EVENLY:
    g a p = a v a i l a b l e n + 1 gap = \frac{available}{n+1} gap=n+1available

第9章:强大的布局助手 - Grid布局

9.1 Grid布局 vs Flex布局

虽然Flex布局很强大,但它主要解决一维布局问题。对于复杂的二维布局,Grid布局是更好的选择。

对比分析:

特性Flex布局Grid布局
维度一维(行或列)二维(行和列)
复杂度简单到中等中等到复杂
控制精度中等高精度
适用场景线性列表、导航栏仪表盘、复杂表单、卡片网格

9.2 Grid布局核心概念

Grid布局将容器划分为网格系统:

Grid容器
网格轨道
网格线
网格单元格
网格区域
行轨道
列轨道
行线
列线
基本单元
合并单元格

9.3 Grid布局属性详解

定义网格轨道:

// 定义列轨道:3列,分别为100px, 1fr, 2fr
static lv_coord_t col_dsc[] = {100, LV_GRID_FR(1), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST};

// 定义行轨道:4行,固定高度和自适应
static lv_coord_t row_dsc[] = {40, 60, LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};

lv_obj_set_grid_dsc_array(container, col_dsc, row_dsc);

fr单位:
fr单位表示分数单位,分配剩余空间:
e l e m e n t W i d t h = f r V a l u e ∑ f r V a l u e s × a v a i l a b l e S p a c e elementWidth = \frac{frValue}{\sum frValues} \times availableSpace elementWidth=frValuesfrValue×availableSpace

放置网格项:

// 将对象放置到网格的特定位置
lv_obj_set_grid_cell(obj, 
    LV_GRID_ALIGN_STRETCH,  // 列对齐
    0,                      // 起始列
    2,                      // 列跨度
    LV_GRID_ALIGN_START,    // 行对齐  
    1,                      // 起始行
    1                       // 行跨度
);

9.4 实战:使用Grid布局创建计算器界面

让我们用Grid布局创建一个专业的计算器界面:

/**
 * 使用Grid布局创建计算器
 */
void create_calculator_grid() {
    lv_obj_t * parent = lv_scr_act();
    
    // 创建计算器容器
    lv_obj_t * calc = lv_obj_create(parent);
    lv_obj_set_size(calc, 320, 400);
    lv_obj_center(calc);
    lv_obj_set_style_bg_color(calc, lv_color_hex(0x2C3E50), 0);
    lv_obj_set_style_radius(calc, 20, 0);
    lv_obj_set_style_pad_all(calc, 15, 0);
    
    /********************
     * 定义Grid网格
     ********************/
    
    // 列定义:等宽5列
    static lv_coord_t col_dsc[] = {
        LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1),
        LV_GRID_TEMPLATE_LAST
    };
    
    // 行定义:显示区 + 按钮区
    static lv_coord_t row_dsc[] = {
        80,    // 显示区域
        60, 60, 60, 60, 60,  // 按钮行
        LV_GRID_TEMPLATE_LAST
    };
    
    lv_obj_set_grid_dsc_array(calc, col_dsc, row_dsc);
    
    /********************
     * 1. 显示区域 - 占据全部5列
     ********************/
    lv_obj_t * display = lv_obj_create(calc);
    lv_obj_set_style_bg_color(display, lv_color_hex(0x1A252F), 0);
    lv_obj_set_style_radius(display, 10, 0);
    lv_obj_set_style_border_width(display, 0, 0);
    lv_obj_set_style_pad_all(display, 10, 0);
    
    // 放置在网格位置:第0行,跨越5列
    lv_obj_set_grid_cell(display, 
        LV_GRID_ALIGN_STRETCH, 0, 5, 
        LV_GRID_ALIGN_STRETCH, 0, 1
    );
    
    // 显示文本
    lv_obj_t * display_text = lv_label_create(display);
    lv_label_set_text(display_text, "0");
    lv_obj_set_style_text_color(display_text, lv_color_white(), 0);
    lv_obj_set_style_text_font(display_text, &lv_font_montserrat_24, 0);
    lv_obj_align(display_text, LV_ALIGN_RIGHT_MID, -10, 0);
    
    /********************
     * 2. 按钮定义和布局
     ********************/
    
    // 按钮文本和样式定义
    struct {
        const char * text;
        int row, col, colspan;
        lv_color_t color;
        const char * event_name;
    } buttons[] = {
        {"C",  1, 0, 1, LV_COLOR_MAKE(0xE7, 0x4C, 0x3C), "clear"},
        {"±",  1, 1, 1, LV_COLOR_MAKE(0x95, 0xA5, 0xA6), "sign"},
        {"%",  1, 2, 1, LV_COLOR_MAKE(0x95, 0xA5, 0xA6), "percent"},
        {"÷",  1, 3, 1, LV_COLOR_MAKE(0xF3, 0x9C, 0x12), "divide"},
        
        {"7",  2, 0, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "7"},
        {"8",  2, 1, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "8"},
        {"9",  2, 2, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "9"},
        {"×",  2, 3, 1, LV_COLOR_MAKE(0xF3, 0x9C, 0x12), "multiply"},
        
        {"4",  3, 0, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "4"},
        {"5",  3, 1, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "5"},
        {"6",  3, 2, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "6"},
        {"-",  3, 3, 1, LV_COLOR_MAKE(0xF3, 0x9C, 0x12), "subtract"},
        
        {"1",  4, 0, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "1"},
        {"2",  4, 1, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "2"},
        {"3",  4, 2, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "3"},
        {"+",  4, 3, 1, LV_COLOR_MAKE(0xF3, 0x9C, 0x12), "add"},
        
        {"0",  5, 0, 2, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "0"},  // 跨越2列
        {".",  5, 2, 1, LV_COLOR_MAKE(0x34, 0x49, 0x5E), "decimal"},
        {"=",  5, 3, 1, LV_COLOR_MAKE(0xF3, 0x9C, 0x12), "equals"},
    };
    
    // 创建所有按钮
    for(int i = 0; i < sizeof(buttons) / sizeof(buttons[0]); i++) {
        lv_obj_t * btn = lv_btn_create(calc);
        
        // 设置按钮样式
        lv_obj_set_style_bg_color(btn, buttons[i].color, 0);
        lv_obj_set_style_radius(btn, 25, 0);
        lv_obj_set_style_shadow_width(btn, 0, 0);
        
        // 设置网格位置
        lv_obj_set_grid_cell(btn,
            LV_GRID_ALIGN_STRETCH, buttons[i].col, buttons[i].colspan,
            LV_GRID_ALIGN_STRETCH, buttons[i].row, 1
        );
        
        // 添加按钮文本
        lv_obj_t * label = lv_label_create(btn);
        lv_label_set_text(label, buttons[i].text);
        lv_obj_set_style_text_color(label, lv_color_white(), 0);
        lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0);
        lv_obj_center(label);
        
        // 添加事件处理
        lv_obj_add_event_cb(btn, calculator_event_handler, LV_EVENT_CLICKED, 
                           (void*)buttons[i].event_name);
    }
}

/**
 * 计算器事件处理
 */
static void calculator_event_handler(lv_event_t * e) {
    const char * button_name = (const char *)lv_event_get_user_data(e);
    
    // 这里可以实现完整的计算器逻辑
    printf("计算器按钮: %s\n", button_name);
    
    // 示例:更新显示
    lv_obj_t * display = lv_obj_get_child(lv_scr_act(), 0); // 获取显示区域
    lv_obj_t * display_text = lv_obj_get_child(display, 0);
    
    if(strcmp(button_name, "clear") == 0) {
        lv_label_set_text(display_text, "0");
    } else {
        // 简单的数字输入示例
        const char * current = lv_label_get_text(display_text);
        if(strcmp(current, "0") == 0) {
            lv_label_set_text(display_text, button_name);
        } else {
            char new_text[32];
            snprintf(new_text, sizeof(new_text), "%s%s", current, button_name);
            lv_label_set_text(display_text, new_text);
        }
    }
}

第10章:动画 - 为界面注入灵魂

10.1 动画的数学基础

动画本质上是属性随时间的变化函数:

P ( t ) = P 0 + ( P 1 − P 0 ) ⋅ f ( t − t 0 t 1 − t 0 ) P(t) = P_0 + (P_1 - P_0) \cdot f\left(\frac{t - t_0}{t_1 - t_0}\right) P(t)=P0+(P1P0)f(t1t0tt0)

其中:

  • P ( t ) P(t) P(t) = 时间 t t t 时的属性值
  • P 0 P_0 P0 = 起始值
  • P 1 P_1 P1 = 结束值
  • f ( x ) f(x) f(x) = 缓动函数, x ∈ [ 0 , 1 ] x \in [0, 1] x[0,1]
  • t 0 t_0 t0, t 1 t_1 t1 = 动画开始和结束时间

10.2 LVGL动画系统架构

LVGL的动画系统基于一个精心设计的架构:

缓动函数类型
线性 linear
缓入 ease-in
缓出 ease-out
缓入缓出 ease-in-out
弹性 elastic
弹跳 bounce
动画创建
动画时间线
缓动函数计算
属性插值
回调执行
动画结束?
清理资源
缓动函数库
用户回调

10.3 创建动画的三种方式

方法1:直接使用动画对象

// 创建动画对象
lv_anim_t anim;
lv_anim_init(&anim);

// 配置动画参数
lv_anim_set_var(&anim, target_object);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_x);
lv_anim_set_values(&anim, start_x, end_x);
lv_anim_set_time(&anim, 500);
lv_anim_set_path_cb(&anim, lv_anim_path_ease_out);

// 可选:设置回调
lv_anim_set_ready_cb(&anim, anim_ready_callback);

// 启动动画
lv_anim_start(&anim);

方法2:使用内置动画快捷方式

// 内置的属性动画
lv_anim_t * anim = lv_anim_create();
lv_anim_set_prop(anim, LV_ANIM_PROP_X);  // 预定义属性
lv_anim_set_values(anim, 0, 100);
lv_anim_set_time(anim, 300);
lv_anim_start(anim);

方法3:使用动画时间线

// 创建时间线
lv_anim_timeline_t * timeline = lv_anim_timeline_create();

// 添加多个动画到时间线
lv_anim_timeline_add(timeline, 0, &anim1);    // 0ms时开始
lv_anim_timeline_add(timeline, 200, &anim2);  // 200ms时开始
lv_anim_timeline_add(timeline, 400, &anim3);  // 400ms时开始

// 控制时间线
lv_anim_timeline_start(timeline);
lv_anim_timeline_set_reverse(timeline, true); // 反向播放
lv_anim_timeline_set_progress(timeline, 0.5); // 跳转到50%

10.4 实战:创建流畅的界面动画

让我们创建一个具有丰富动画效果的侧边栏菜单:

/**
 * 创建带动画的侧边栏菜单
 */
typedef struct {
    lv_obj_t * sidebar;
    lv_obj_t * overlay;
    bool is_open;
    lv_anim_timeline_t * timeline;
} sidebar_t;

static sidebar_t menu;

// 侧边栏动画执行回调
static void sidebar_set_width(void * obj, int32_t value) {
    lv_obj_set_width((lv_obj_t *)obj, value);
}

static void sidebar_set_x(void * obj, int32_t value) {
    lv_obj_set_x((lv_obj_t *)obj, value);
}

static void overlay_set_opa(void * obj, int32_t value) {
    lv_obj_set_style_opa((lv_obj_t *)obj, value, 0);
}

/**
 * 创建侧边栏菜单
 */
void create_animated_sidebar() {
    lv_obj_t * parent = lv_scr_act();
    
    // 创建遮罩层
    menu.overlay = lv_obj_create(parent);
    lv_obj_set_size(menu.overlay, LV_PCT(100), LV_PCT(100));
    lv_obj_set_style_bg_color(menu.overlay, lv_color_black(), 0);
    lv_obj_set_style_bg_opa(menu.overlay, LV_OPA_50, 0);
    lv_obj_set_style_opa(menu.overlay, 0, 0);  // 初始透明
    lv_obj_add_flag(menu.overlay, LV_OBJ_FLAG_CLICKABLE);
    lv_obj_add_event_cb(menu.overlay, overlay_click_handler, LV_EVENT_CLICKED, NULL);
    
    // 创建侧边栏
    menu.sidebar = lv_obj_create(parent);
    lv_obj_set_size(menu.sidebar, 280, LV_PCT(100));
    lv_obj_set_style_bg_color(menu.sidebar, lv_color_hex(0x34495E), 0);
    lv_obj_set_style_radius(menu.sidebar, 0, 0);
    lv_obj_set_x(menu.sidebar, -280);  // 初始在屏幕外
    
    // 侧边栏内容
    lv_obj_set_flex_flow(menu.sidebar, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_style_pad_all(menu.sidebar, 20, 0);
    lv_obj_set_style_pad_gap(menu.sidebar, 15, 0);
    
    // 标题
    lv_obj_t * title = lv_label_create(menu.sidebar);
    lv_label_set_text(title, "菜单");
    lv_obj_set_style_text_color(title, lv_color_white(), 0);
    lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0);
    
    // 菜单项
    const char * menu_items[] = {"📊 仪表盘", "⚙️ 设置", "🔔 通知", "📈 统计", "👤 个人资料", "❓ 帮助"};
    for(int i = 0; i < 6; i++) {
        lv_obj_t * item = lv_btn_create(menu.sidebar);
        lv_obj_set_size(item, LV_PCT(100), 50);
        lv_obj_set_style_bg_color(item, lv_color_hex(0x4A6572), 0);
        lv_obj_set_style_radius(item, 10, 0);
        lv_obj_set_style_border_width(item, 0, 0);
        
        lv_obj_t * label = lv_label_create(item);
        lv_label_set_text(label, menu_items[i]);
        lv_obj_set_style_text_color(label, lv_color_white(), 0);
        lv_obj_center(label);
        
        // 为每个菜单项添加点击动画
        lv_obj_add_event_cb(item, menu_item_click_handler, LV_EVENT_CLICKED, NULL);
    }
    
    // 底部退出按钮
    lv_obj_t * exit_btn = lv_btn_create(menu.sidebar);
    lv_obj_set_size(exit_btn, LV_PCT(100), 50);
    lv_obj_set_style_bg_color(exit_btn, lv_color_hex(0xE74C3C), 0);
    lv_obj_set_style_radius(exit_btn, 10, 0);
    
    lv_obj_t * exit_label = lv_label_create(exit_btn);
    lv_label_set_text(exit_label, "退出");
    lv_obj_center(exit_label);
    lv_obj_add_event_cb(exit_btn, exit_click_handler, LV_EVENT_CLICKED, NULL);
    
    menu.is_open = false;
}

/**
 * 切换侧边栏状态
 */
void toggle_sidebar() {
    if(menu.timeline) {
        lv_anim_timeline_del(menu.timeline);
    }
    
    menu.timeline = lv_anim_timeline_create();
    
    if(!menu.is_open) {
        // 打开动画
        lv_anim_t a_sidebar, a_overlay;
        
        // 侧边栏滑动动画
        lv_anim_init(&a_sidebar);
        lv_anim_set_var(&a_sidebar, menu.sidebar);
        lv_anim_set_exec_cb(&a_sidebar, sidebar_set_x);
        lv_anim_set_values(&a_sidebar, -280, 0);
        lv_anim_set_time(&a_sidebar, 400);
        lv_anim_set_path_cb(&a_sidebar, lv_anim_path_ease_out);
        lv_anim_timeline_add(menu.timeline, 0, &a_sidebar);
        
        // 遮罩层淡入动画
        lv_anim_init(&a_overlay);
        lv_anim_set_var(&a_overlay, menu.overlay);
        lv_anim_set_exec_cb(&a_overlay, overlay_set_opa);
        lv_anim_set_values(&a_overlay, 0, LV_OPA_50);
        lv_anim_set_time(&a_overlay, 300);
        lv_anim_timeline_add(menu.timeline, 100, &a_overlay);
        
        // 清除隐藏标志
        lv_obj_clear_flag(menu.overlay, LV_OBJ_FLAG_HIDDEN);
    } else {
        // 关闭动画
        lv_anim_t a_sidebar, a_overlay;
        
        // 遮罩层淡出动画
        lv_anim_init(&a_overlay);
        lv_anim_set_var(&a_overlay, menu.overlay);
        lv_anim_set_exec_cb(&a_overlay, overlay_set_opa);
        lv_anim_set_values(&a_overlay, LV_OPA_50, 0);
        lv_anim_set_time(&a_overlay, 200);
        lv_anim_timeline_add(menu.timeline, 0, &a_overlay);
        
        // 侧边栏滑动动画
        lv_anim_init(&a_sidebar);
        lv_anim_set_var(&a_sidebar, menu.sidebar);
        lv_anim_set_exec_cb(&a_sidebar, sidebar_set_x);
        lv_anim_set_values(&a_sidebar, 0, -280);
        lv_anim_set_time(&a_sidebar, 400);
        lv_anim_set_path_cb(&a_sidebar, lv_anim_path_ease_in);
        lv_anim_timeline_add(menu.timeline, 100, &a_sidebar);
    }
    
    lv_anim_timeline_start(menu.timeline);
    menu.is_open = !menu.is_open;
}

/**
 * 菜单项点击动画
 */
static void menu_item_click_handler(lv_event_t * e) {
    lv_obj_t * target = lv_event_get_target(e);
    
    // 创建点击波纹动画
    lv_anim_t ripple;
    lv_anim_init(&ripple);
    lv_anim_set_var(&ripple, target);
    lv_anim_set_exec_cb(&ripple, (lv_anim_exec_xcb_t)lv_obj_set_style_bg_color);
    lv_anim_set_values(&ripple, lv_color_hex(0x4A6572), lv_color_hex(0x5D6D7E));
    lv_anim_set_time(&ripple, 100);
    lv_anim_set_playback_time(&ripple, 100);
    lv_anim_start(&ripple);
    
    // 获取菜单项文本
    lv_obj_t * label = lv_obj_get_child(target, 0);
    const char * item_text = lv_label_get_text(label);
    printf("菜单项点击: %s\n", item_text);
}

/**
 * 遮罩层点击事件
 */
static void overlay_click_handler(lv_event_t * e) {
    toggle_sidebar();  // 点击遮罩层关闭菜单
}

/**
 * 退出按钮点击事件
 */
static void exit_click_handler(lv_event_t * e) {
    // 创建退出确认对话框(这里简化为直接退出)
    printf("退出应用\n");
}

/**
 * 创建触发侧边栏的按钮
 */
void create_sidebar_trigger() {
    lv_obj_t * menu_btn = lv_btn_create(lv_scr_act());
    lv_obj_set_size(menu_btn, 50, 50);
    lv_obj_align(menu_btn, LV_ALIGN_TOP_LEFT, 20, 20);
    lv_obj_set_style_radius(menu_btn, 25, 0);
    lv_obj_set_style_bg_color(menu_btn, lv_color_hex(0x3498DB), 0);
    
    lv_obj_t * menu_icon = lv_label_create(menu_btn);
    lv_label_set_text(menu_icon, "☰");
    lv_obj_set_style_text_color(menu_icon, lv_color_white(), 0);
    lv_obj_set_style_text_font(menu_icon, &lv_font_montserrat_20, 0);
    lv_obj_center(menu_icon);
    
    lv_obj_add_event_cb(menu_btn, [](lv_event_t * e) {
        toggle_sidebar();
    }, LV_EVENT_CLICKED, NULL);
}

10.5 动画性能优化

性能优化策略:

  1. 避免频繁创建/销毁动画:重用动画对象
  2. 使用合适的缓动函数:复杂缓动函数计算开销大
  3. 限制同时运行的动画数量:通常不超过5-10个
  4. 使用硬件加速属性:位置、透明度等

性能监控:

// 监控动画性能
void anim_performance_check() {
    uint32_t anim_count = lv_anim_count_running();
    printf("当前运行中的动画数量: %d\n", anim_count);
    
    if(anim_count > 8) {
        printf("警告: 动画数量过多可能影响性能\n");
    }
}

本章总结与挑战

恭喜!你已经掌握了LVGL交互和布局的核心技术。现在你能够:

  1. 理解并实现完整的事件处理系统
  2. 使用Flex布局创建响应式界面
  3. 使用Grid布局处理复杂二维布局
  4. 创建流畅的动画效果增强用户体验

综合挑战:

  1. 智能家居仪表盘:结合本章所有技术,创建一个完整的智能家居控制仪表盘,要求:

    • 使用Grid布局组织各个功能区域
    • 实现设备状态的实时更新和动画反馈
    • 添加侧边栏菜单进行场景切换
  2. 数据可视化:创建一个数据监控界面,包含:

    • 使用Flex布局的卡片式设计
    • 实时数据更新的动画效果
    • 交互式图表控件
  3. 设置向导:实现一个多步骤的设置向导:

    • 使用动画进行页面切换
    • 表单验证和错误提示
    • 进度指示器

在下一部分,我们将进入进阶应用与项目实战,学习更复杂的控件和完整的项目开发流程,将你的LVGL技能提升到新的高度!


研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

极客不孤独

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值