第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的事件处理遵循一个清晰的管道模型:
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布局核心概念
主轴与交叉轴:
- 主轴:主要排列方向
- 交叉轴:与主轴垂直的方向
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=containerSize−∑fixedSizes−paddings
弹性项尺寸计算:
对于每个弹性项,其最终尺寸为:
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=n−1availableLV_FLEX_ALIGN_SPACE_AROUND:
g a p = a v a i l a b l e n gap = \frac{available}{n} gap=navailableLV_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布局将容器划分为网格系统:
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+(P1−P0)⋅f(t1−t0t−t0)
其中:
- 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的动画系统基于一个精心设计的架构:
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 动画性能优化
性能优化策略:
- 避免频繁创建/销毁动画:重用动画对象
- 使用合适的缓动函数:复杂缓动函数计算开销大
- 限制同时运行的动画数量:通常不超过5-10个
- 使用硬件加速属性:位置、透明度等
性能监控:
// 监控动画性能
void anim_performance_check() {
uint32_t anim_count = lv_anim_count_running();
printf("当前运行中的动画数量: %d\n", anim_count);
if(anim_count > 8) {
printf("警告: 动画数量过多可能影响性能\n");
}
}
本章总结与挑战
恭喜!你已经掌握了LVGL交互和布局的核心技术。现在你能够:
- 理解并实现完整的事件处理系统
- 使用Flex布局创建响应式界面
- 使用Grid布局处理复杂二维布局
- 创建流畅的动画效果增强用户体验
综合挑战:
-
智能家居仪表盘:结合本章所有技术,创建一个完整的智能家居控制仪表盘,要求:
- 使用Grid布局组织各个功能区域
- 实现设备状态的实时更新和动画反馈
- 添加侧边栏菜单进行场景切换
-
数据可视化:创建一个数据监控界面,包含:
- 使用Flex布局的卡片式设计
- 实时数据更新的动画效果
- 交互式图表控件
-
设置向导:实现一个多步骤的设置向导:
- 使用动画进行页面切换
- 表单验证和错误提示
- 进度指示器
在下一部分,我们将进入进阶应用与项目实战,学习更复杂的控件和完整的项目开发流程,将你的LVGL技能提升到新的高度!
研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)

被折叠的 条评论
为什么被折叠?



