做嵌入式UI的人迟早会碰上一件事:界面里需要一排一排的按钮。我前阵子做某个触摸控制面板,需要一块数字键盘、一排模式切换按钮、还有一个带加减档的面板,按钮总数加起来超过三十个。最早用普通按钮对象一个个创建,代码写出来不仅长,而且改布局非常痛苦。后来把所有按钮切到了按钮矩阵控件——LVGL 9 里叫lv_buttonmatrix,老版本叫lv_btnmatrix——整个项目的 UI 代码量直接少了一截,维护起来也舒心很多。
按钮矩阵的定位很纯粹:用一个字符串数组声明整个布局,控件自动把按钮排成网格,点击事件统一回调。它特别适合键盘、遥控器、配页面板、菜单导航这类大量重复按钮的场景。这篇文章我把自己的用法、踩过的坑、以及调试思路整理一遍,给正在用 LVGL 做界面的朋友一个参考。
1. 按钮总数超过两位数之后,“一个按钮一个对象”的思路就不太行了
1.1 普通按钮数组的问题不只是代码长
假设你要做一个 3x3 的数字键盘,用lv_button逐个创建,通常写出来是这个画风:
lv_obj_t * btn; btn = lv_button_create(parent); lv_obj_set_pos(btn, 0, 0); lv_obj_set_size(btn, 80, 50); lv_obj_add_event_cb(btn, cb_1, LV_EVENT_CLICKED, NULL); lv_obj_t * label = lv_label_create(btn); lv_label_set_text(label, "1"); btn = lv_button_create(parent); lv_obj_set_pos(btn, 90, 0); ...十几个按钮还能忍,几十个按钮就是折磨。真正麻烦的不是创建代码多,而是布局逻辑分散:按钮间距、行高、列宽全要自己算。想加一个按钮、换一列宽度、调整行距,都得从头改一遍坐标,稍不留神就重叠或者留白。
而且每个按钮都是一个独立对象,内存开销也很现实。嵌入式设备上对象数量动辄上百,每个对象都要维护自己的样式、尺寸、事件列表,对 RAM 不宽裕的片子来说,这种浪费不值得。
1.2 按钮矩阵的核心思路:布局由 map 数组决定
按钮矩阵把“布局”和“按键内容”压缩成一个const char *数组。举个例子:
static const char * key_map[] = { "7", "8", "9", "C", "\n", "4", "5", "6", "BS", "\n", "1", "2", "3", "OK", "\n", "0", ".", "00", "+/-", "" };这一个数组就把 16 个按钮的排列方式说清楚了。普通字符串代表一个按钮,"\n"表示换到下一行,最后一个空字符串""表示数组结束。控件内部会自己计算按钮尺寸,按行均分宽度,按对象尺寸自动布局。
从对象模型上看,按钮矩阵是一个整体控件,按钮不是独立对象,而是内部绘制出来的矩形区域。好处是内存占用低、布局统一,代价是单个按钮的独立性没有了——你不能给某个按钮单独lv_obj_set_*定位,所有按钮共享同一套样式体系,通过状态区分。
1.3 什么时候用按钮矩阵,什么时候应该换别的控件
我也见过有人不分场合硬上按钮矩阵,结果做复杂列表时反而更痛苦。用之前先分清场景:
| 场景 | 推荐控件 | 原因 |
|---|---|---|
| 数字键盘、符号键盘、遥控器 | 按钮矩阵 | 行列规整、map 清晰、事件统一 |
| 菜单选项、底部导航、多选标签 | 按钮矩阵 | 支持 checked 状态,可做单选/多选 |
| 复杂混合布局、图文卡片 | 普通按钮 | 每个按钮独立定制,布局自由度高 |
| 表格数据展示 | 表格控件或列表 | 按钮矩阵不适合大量文本+列宽动态变化 |
| 纯文本输入键盘 | 官方键盘控件 | 键盘控件内部就是按钮矩阵,但封装了按键映射 |
官方键盘控件这类“现成方案”确实够用,但如果你要自定义布局、符号页、大小写切换逻辑,直接用按钮矩阵反而更透明。下面我拿一个完整的数字键盘项目作为主线来讲。
2. map 数组就是布局本身:换行、宽度与按钮 ID 分配
2.1 换行与结束标志是最容易忽略的细节
map 数组的规则看起来简单,但我在代码评审里见过不止一次两种写法出问题:
第一种是忘了最后那个空字符串""。LVGL 解析 map 时靠空字符串判断数组结束,如果漏了,它就会继续往后扫描内存,直到读到随机内存里的0x00,轻则布局错乱,重则直接复位。
第二种是把结尾写成NULL。LVGL 内部对这个结束标记的处理是按“字符指针是否为空字符串”判断的,你传NULL进去,它解引用的时候就会爆炸。正确写法是:
static const char * map[] = { "A", "B", "\n", "C", "D", "" };另外,"\n"本身不产生按钮,只负责换行。同一行的按钮数量不需要固定,第一行四个、第二行三个完全没问题,每行会按各自数量均分行宽。
2.2 按钮宽度:默认均分,也可以让某个按钮多占列
默认情况下,每一行有多少个按钮,就把行宽均分成多少份。但很多实际布局里需要“某个键更宽”,比如回车键、确定键、退格键。
这时候用lv_buttonmatrix_set_btn_width()调整:
/* 让第 11 号按钮(OK)占两个份额的宽度 */ lv_buttonmatrix_set_btn_width(keypad, 11, 2);这里 width 的单位是“相对份额”,不是像素,也不是百分比。假如这一行默认 4 个按钮,每个宽 1 份,你把其中某个按钮设成 2,这一行就变成总共 5 份,其他 3 个按钮仍然是 1 份,于是这个按钮占了行宽的 2/5,其余按钮各占 1/5。
这个细节特别容易踩坑,后面第五部分我会专门展开讲。在 map 数组层面,部分版本也支持在按钮文本前写数字前缀控制宽度,但不同版本解析规则有差异,我建议直接用set_btn_width,意图清清楚楚。
2.3 按钮 ID 的计算方法:从 0 开始,从左到右,从上到下
按钮矩阵里每个按钮有一个全局 ID,从左往右、从上往下依次递增。拿前面那个键盘 map 举例,ID 和按钮对应关系是这样的:
| 行 | 按钮文本 | ID |
|---|---|---|
| 第一行 | 7 / 8 / 9 / C | 0 / 1 / 2 / 3 |
| 第二行 | 4 / 5 / 6 / BS | 4 / 5 / 6 / 7 |
| 第三行 | 1 / 2 / 3 / OK | 8 / 9 / 10 / 11 |
| 第四行 | 0 / . / 00 / +/- | 12 / 13 / 14 / 15 |
注意:这个 ID 和“该按钮在第几行”没有直接换算关系。如果每行按钮数量不一样,ID 依然全局连续,计算方法就是“前面所有行的按钮总数 + 当前行内的位置”。我一般是直接把 map 复制到注释里,标上每个 ID,再写宽度和事件逻辑,避免数错。
2.4 空按钮与占位:两种思路
偶尔需要留一个“空位”不打字也不显示,比如计算器键盘里左下角想留白。常见做法是放一个空字符串按钮,然后给它加HIDDEN控制位:
lv_buttonmatrix_set_btn_ctrl(keypad, 14, LV_BUTTONMATRIX_CTRL_HIDDEN);隐藏按钮不显示,不参与布局,但它的 ID 仍然保留。这样 map 的结构不变,ID 不受影响,后续想恢复显示,只要清除掉HIDDEN位就行。
另一种是留一个可见但不可按的“占位”按钮,用空文本加DISABLED,样式上和底色融为一体。两种方式各有适用场景,我一般优先用HIDDEN,因为 DISABLED 按钮在部分主题下会有明显的置灰样式,想做得跟背景完全一致反而要多写样式。
3. 事件回调里识别按键:value_changed 与按钮 ID
3.1 按钮矩阵的事件入口只有一个
按钮矩阵不会给每个按钮单独注册回调,而是整体监听LV_EVENT_VALUE_CHANGED事件。只要按钮被点击、长按重复触发、或者通过键盘/遥控器聚焦确认,都会走到同一个回调里。
基本写法:
static void keypad_event_cb(lv_event_t * e) { lv_obj_t * obj = lv_event_get_target(e); uint32_t id = lv_buttonmatrix_get_selected_btn(obj); if(id == LV_BUTTONMATRIX_BTN_NONE) return; const char * txt = lv_buttonmatrix_get_btn_text(obj, id); // 根据 txt 或 id 分发处理 }lv_buttonmatrix_get_selected_btn()返回当前选中(即刚点击)的按钮 ID。如果当前没有任何按钮被选中,LVGL 9 返回LV_BUTTONMATRIX_BTN_NONE,旧版本是LV_BTNMATRIX_BTN_NONE,值都是 0xFFFF。回调里第一步必须判掉这个值,否则读文本时会读到无效地址。
3.2 用文本分发还是用 ID 分发
我建议简单场景用文本分发,复杂场景用 ID 分发。
文本分发的意思是直接比较按钮文本:
if(strcmp(txt, "BS") == 0) { // 退格逻辑 } else if(strcmp(txt, "OK") == 0) { // 确定逻辑 } else { // 默认当成普通字符输入 }这种方式直观,map 里改成什么文本,逻辑跟着文本走,适合键盘这种“按钮含义由文本决定”的场景。
ID 分发适合按钮含义固定的情况,比如一排模式选择按钮,第一个是“日间模式”,第二个是“夜间模式”,第三个是“自动模式”。这时候用 switch 处理 ID 更不容易被文本改动影响:
switch(id) { case 0: set_mode(MODE_DAY); break; case 1: set_mode(MODE_NIGHT); break; case 2: set_mode(MODE_AUTO); break; }我更倾向于混合使用:键盘类用文本分发,模式切换类用 ID 分发。因为按键文本本身就是用户看到的含义,而模式按钮的文本可能随语言切换变化,搞个字典表维护反而麻烦。
3.3 长按重复:退格键的“连发”问题
按钮矩阵有一个很实用的行为:默认情况下,长按某个按钮会周期性地重复触发LV_EVENT_VALUE_CHANGED。也就是说,退格键不用你额外做定时器,按住就会连续删除字符。
但这样也会带来一个问题:数字键长按也会连续输入。比如用户不小心按住“7”,输入框里会冒出一串 7。所以我通常的做法是给“所有按钮默认禁止重复”,然后单独给退格键恢复重复:
/* 全部按钮先禁止长按重复 */ lv_buttonmatrix_set_btn_ctrl_all(keypad, LV_BUTTONMATRIX_CTRL_NO_REPEAT); /* 退格键 ID 为 7,允许长按重复 */ lv_buttonmatrix_clear_btn_ctrl(keypad, 7, LV_BUTTONMATRIX_CTRL_NO_REPEAT);LV_BUTTONMATRIX_CTRL_NO_REPEAT的意思是“不要重复触发”,设置了之后长按就只触发一次。不设置就是允许重复。默认其实是允许重复的,所以如果不想让数字键连发,必须手动加上这个控制位。
重复触发的频率由 LVGL 输入设备的“长按重复延迟”控制,全局生效:
lv_indev_t * indev = lv_indev_active(); lv_indev_set_long_press_repeat_time(indev, 150);单位是毫秒,150 到 200 之间手感比较舒服。
3.4 和输入框联动:一个完整的数字键盘处理逻辑
下面贴一段我常用的逻辑,配合lv_textarea做输入框:
static lv_obj_t * input_area; static lv_obj_t * keypad; static void input_append(const char * s) { char buf[32]; const char * cur = lv_textarea_get_text(input_area); if(strlen(cur) + strlen(s) >= sizeof(buf)) return; strcpy(buf, cur); strcat(buf, s); lv_textarea_set_text(input_area, buf); } static void keypad_event_cb(lv_event_t * e) { lv_obj_t * obj = lv_event_get_target(e); uint32_t id = lv_buttonmatrix_get_selected_btn(obj); if(id == LV_BUTTONMATRIX_BTN_NONE) return; const char * txt = lv_buttonmatrix_get_btn_text(obj, id); if(strcmp(txt, "C") == 0) { lv_textarea_set_text(input_area, ""); } else if(strcmp(txt, "BS") == 0) { char buf[32]; const char * cur = lv_textarea_get_text(input_area); uint16_t len = strlen(cur); if(len == 0) return; strcpy(buf, cur); buf[len - 1] = '\0'; lv_textarea_set_text(input_area, buf); } else if(strcmp(txt, "OK") == 0) { /* 这里是确认提交逻辑,可以关闭键盘页,也可以把输入值存起来 */ } else { input_append(txt); } }这里有个细节:input_area如果是动态创建的,必须保证回调执行时它已经存在,否则lv_textarea_get_text会拿到空指针。我在初始化顺序上一般是先创建输入框,再创建键盘,最后注册事件回调。
4. 进阶用法:单选、禁用、动态换布局和样式定制
4.1 CHECKABLE + one_checked 做菜单切换
按钮矩阵支持“可勾选”状态,配合单选项模式可以做很有质感的菜单。先让部分按钮变成可勾选:
for(uint32_t i = 0; i < 4; i++) { lv_buttonmatrix_set_btn_ctrl(nav_btnm, i, LV_BUTTONMATRIX_CTRL_CHECKABLE); } lv_buttonmatrix_set_one_checked(nav_btnm, true);set_one_checked(true)的含义是:同一时间最多只有一个按钮保持 checked 状态。点击按钮 A,A 会变成 checked,其他按钮自动取消 checked。这让“底部四个 Tab 切换”变成了天然的单选组,不需要自己在回调里维护状态切换逻辑。
事件回调里怎么知道当前哪个被选中?用lv_buttonmatrix_get_selected_btn拿到的是最近点击的按钮,但如果你在别处通过代码修改了 checked 状态,更可靠的是遍历按钮读 checked 状态:
for(uint32_t i = 0; i < 4; i++) { if(lv_buttonmatrix_has_btn_ctrl(nav_btnm, i, LV_BUTTONMATRIX_CTRL_CHECKED)) { /* i 就是当前选中的菜单 */ } }lv_buttonmatrix_has_btn_ctrl这个名字在 LVGL 9 里是存在的,用来判断按钮是否设置了某个控制位。如果用的是 LVGL 8,对应函数是lv_btnmatrix_has_btn_ctrl。
4.2 HIDDEN 和 DISABLED 做动态约束
有些交互需要让按钮“暂时不能按”。比如输入框为空时,退格键应该置灰;输入内容达到上限时,确定键应该禁用。
禁用一个按钮:
lv_buttonmatrix_set_btn_ctrl(keypad, key_id, LV_BUTTONMATRIX_CTRL_DISABLED);恢复按钮:
lv_buttonmatrix_clear_btn_ctrl(keypad, key_id, LV_BUTTONMATRIX_CTRL_DISABLED);被 DISABLED 的按钮仍然显示,但点击不会触发事件回调,样式上会套用 LVGL 为禁用状态准备的样式。如果你希望按钮完全消失,那就用HIDDEN:
lv_buttonmatrix_set_btn_ctrl(keypad, key_id, LV_BUTTONMATRIX_CTRL_HIDDEN);注意HIDDEN和DISABLED不是一回事。隐藏按钮不占布局空间,但会保留 ID;禁用按钮占据空间、看得见,只是不能交互。我经常用隐藏按钮做“临时懒加载”的效果:某个功能没解锁时直接不留痕迹,解锁后再显示出来。
4.3 动态换 map:键盘大小写/符号页切换
按钮矩阵最爽的一点是运行中可以整体替换 map,瞬间切换整套按键布局。比如数字键盘下方放一个“符号”按钮,点击后把 map 换成符号键盘:
static const char * map_digits[] = { "7", "8", "9", "\n", "4", "5", "6", "\n", "1", "2", "3", "\n", "0", "sym", "OK", "" }; static const char * map_symbols[] = { "!", "@", "#", "\n", "$", "%", "^", "\n", "&", "*", "(", "\n", "abc", "OK", ")", "" }; static void switch_page(lv_obj_t * keypad, const char * const * map) { lv_buttonmatrix_set_map(keypad, map); /* 切换布局后,之前某些按钮的宽度、状态会被重置,需要重新设置 */ lv_buttonmatrix_set_btn_width(keypad, ok_id_current, 2); }换 map 之后有几个隐藏问题需要处理:
第一,按钮 ID 会重新从 0 编号。如果你在回调里用硬编码 ID,比如“退格是 7”,换页后这个 ID 可能变成别的键,必须同步更新或者改用文本分发。
第二,原来设置的按钮宽度不会跟着 map 一起换。每个按钮的宽度设置是挂在对象状态上的,切换 map 后原来的按钮 ID 对应的宽度设置可能还在,但按钮文本变了,宽度未必合适。所以每次切换完 map,重新调用一遍set_btn_width之类的配置函数最稳妥。
第三,如果之前某个按钮处于 checked 状态,切换 map 后状态可能残留在相同 ID 的新按钮上。必要时主动清除:
lv_buttonmatrix_clear_btn_ctrl_all(keypad, LV_BUTTONMATRIX_CTRL_CHECKED);4.4 按钮样式:LV_PART_ITEMS 才是重点
按钮矩阵的样式体系分两层:
LV_PART_MAIN:容器本身,控制整体背景、内边距、行列间距。LV_PART_ITEMS:容器里的所有按钮,控制按钮自身的背景、圆角、文本。
我见过不少新手直接给LV_PART_MAIN设置背景色,以为按钮会跟着变色,结果按钮区域是平的或者完全透明。按钮的底色要挂在LV_PART_ITEMS上。
一个基本样式配置:
static lv_style_t style_container; static lv_style_t style_button; static lv_style_t style_pressed; static lv_style_t style_checked; lv_style_init(&style_container); lv_style_set_pad_all(&style_container, 8); lv_style_set_pad_row(&style_container, 4); /* 行间距 */ lv_style_set_pad_column(&style_container, 4); /* 列间距 */ lv_style_set_bg_color(&style_container, lv_color_hex(0x202020)); lv_style_set_radius(&style_container, 10); lv_style_init(&style_button); lv_style_set_bg_color(&style_button, lv_color_hex(0x303030)); lv_style_set_text_color(&style_button, lv_color_white()); lv_style_set_radius(&style_button, 6); lv_style_set_border_width(&style_button, 1); lv_style_set_border_color(&style_button, lv_color_hex(0x555555)); lv_style_init(&style_pressed); lv_style_set_bg_color(&style_pressed, lv_color_hex(0x505050)); lv_style_set_translate_y(&style_pressed, 1); /* 按下时下沉 1 像素 */ lv_style_init(&style_checked); lv_style_set_bg_color(&style_checked, lv_color_hex(0x3080cc)); lv_style_set_text_color(&style_checked, lv_color_white()); lv_obj_add_style(btnm, &style_container, LV_PART_MAIN); lv_obj_add_style(btnm, &style_button, LV_PART_ITEMS); lv_obj_add_style(btnm, &style_pressed, LV_PART_ITEMS | LV_STATE_PRESSED); lv_obj_add_style(btnm, &style_checked, LV_PART_ITEMS | LV_STATE_CHECKED);注意 selector 的写法:LV_PART_ITEMS | LV_STATE_PRESSED表示“按钮处于按下状态时的样式”,LV_PART_ITEMS | LV_STATE_CHECKED表示“按钮被勾选时的样式”。两者可以叠加,优先级由 LVGL 内部的状态样式匹配规则决定。
如果按钮矩阵显示出来是一片空白,先检查两件事:一是LV_PART_ITEMS的样式是否设置了背景色;二是容器尺寸是否足够大,按钮矩阵不会自动撑开,对象尺寸太小可能把按钮挤压得只剩边框。
5. 实战中踩过的几个坑:从布局错乱到事件风暴
5.1 map 数组漏掉结尾,界面随机崩
这是我见过最多的问题。有次我在会议室调一块屏幕,键盘一直随机复位,一开始怀疑是底层驱动问题,查了半天发现是 map 数组最后一个""写丢了。LVGL 解析 map 时按顺序读取,找不到结束符就会越过数组边界,读到哪算哪。
排查方法其实很简单:把所有 map 定义打印出来,检查最后一个元素是不是""。养成习惯,凡是声明 map,第一眼先看结尾。别用NULL,我就是从NULL改成""才解决崩溃的。
5.2 set_btn_width 的单位理解错,整行按钮被挤变形
我早期以为set_btn_width的单位是像素,传了个80进去,结果那一行的按钮宽度比例直接变成 80:1:1:1,其他按钮被压成一条线。后来才意识到 width 是“份额”而不是像素。
正确理解方式:按钮矩阵在布局每一行时,把所有按钮的 width 值加起来作为总份额,然后按比例分配行宽。假设四个按钮 width 分别默认都是 1,你只需要把其中一个改成 2,其他保持 1,它自然会占两倍宽度。
设计宽比例时,建议先画个草图,标好每行的“总份额”。比如底行四个按钮希望是 2:1:1:2,那它们的 width 就设为 2、1、1、2,总份额 6。这样改完心理有数,不会再出现“我想让这个按钮宽一点结果其他按钮不见了”的事。
5.3 回调里做耗时操作,按键手感发“黏”
按钮矩阵的事件回调跑在 LVGL 的事件循环线程里。如果回调里做了耗时的事情,比如读写外部存储、刷新整个列表、触发网络请求,界面就会卡在当前帧,按下去的反应明显延迟,手指离开后事件还在处理。
表现是:按钮看起来“黏”住了,按下没有立刻弹起,或者连按两次只生效一次。
我的处理原则是:回调里只更新状态和缓存,耗时的操作放到lv_timer里延迟执行,或者用一个标志位,让主循环下一个 tick 去处理。嵌入式 UI 的交互质感,很大程度上就是这些细节决定的。
5.4 HIDDEN 按钮不占空间,但它的 ID 不会消失
隐藏按钮在布局上确实“消失”了,但它对应的 ID 依然占着 map 里的位置。这会导致一个很隐蔽的问题:如果你把某个按钮设置成 HIDDEN,其他按钮的 ID 并不会往前挪,你在回调里写的id == 7依然指向原来的位置,而不是“下一个可见按钮”。
我踩过的一次具体教训:一个支付键盘,付款金额为 0 时把“确认”按钮隐藏了,但布局刷新后剩下的按钮间距变大,看起来很不自然。后来我把隐藏改成禁用,按钮占着位置但置灰,视觉上稳定很多。
所以“隐藏”和“禁用”的选择不只是状态语义问题,还会直接影响布局。想完全不留痕迹就用 HIDDEN,想保持布局稳定就用 DISABLED。
5.5 默认主题下的按钮状态颜色,容易和自定义样式打架
LVGL 自带的主题对按钮矩阵的LV_PART_ITEMS会施加一套默认颜色和状态处理,包括 hover 变亮、pressed 变暗、checked 高亮等。如果你的程序里同时启用了主题,又自定义了一部分样式,可能会出现“明明设置了 checked 颜色,但不生效”的情况。
解决办法是:要么整个按钮矩阵完全自定义样式,把LV_PART_ITEMS在各个状态下的颜色都补齐;要么完全交给主题,不做局部修改。最怕的是这里设一个背景色,那里留一个默认状态色,最后显示出来的颜色你自己都说不清来源。
调试样式时,可以用 LVGL 的 API 把当前生效的状态打印出来,确认按钮处于哪个状态:
lv_state_t state = lv_obj_get_state(btnm);lv_obj_get_state()返回的是整体对象的状态位组合,按钮矩阵会把当前交互状态合并进去。看到具体状态后,再回去检查对应的样式 selector 是否匹配,大多数样式问题都能在这一步定位。
5.6 切换 map 后忘记重置宽度和 checked 状态
这个问题我在 4.3 提过一次,但它真的值得单独再讲。原因在于:按钮矩阵的按钮宽度设置、控制位状态,是和按钮 ID 绑定的。你换了一个 map,新 map 的按钮还是从 ID 0 开始编号,原来 ID 宽度配置会被错误地套到新 key 上。
我做符号键盘切换时就翻过车:切到符号页后,“OK”键变成了一半宽,因为数字键盘里它被设置成 width=2,而符号页里同样 ID 的按钮不是“OK”。加上切换布局时 checked 状态也残留,符号页里有一个按钮一直高亮,看起来像 bug 但其实是状态残留。
所以凡是动态换 map 的场景,我建议封装一个统一的“初始化按钮矩阵”函数,把 map 设置、宽度设置、按钮控制位设置全部放进去。每次切换 map 后调用一遍,保证状态可预期。
另外,事件回调里获取的按钮文本在切换 map 之后会立刻变化。如果你把滑动缓冲区里的按钮文本缓存下来,切换后就成了脏数据,最好在切换完 map 后主动清掉所有跟按钮 ID 相关的本地状态。
最后再分享一个小技巧
我在按钮矩阵上做得最多的一个优化是“视觉分组”:一个键盘界面,用pad_row和pad_column控制间距,再给不同区域的按钮设置不同的背景色,用间距和颜色把数字区、功能区分开,比用分隔线干净得多。做法很简单,把按钮矩阵拆成两个实例,一个管数字区,一个管功能键区,上下拼在一起;或者干脆在一个 map 里用空行"\n\n"制造间距,再配合整体 padding 调整。
如果只是需要一个标准键盘,直接考虑官方键盘控件更省事;但只要你的布局稍微特殊一点,比如加一排自定义快捷键、改某些键的宽度、加符号切换页,按钮矩阵就是更透明、更好掌控的方案。这个控件本质上没有太多玄学,map 清楚、ID 理清、状态控制位用对,剩下的就是布局细节的打磨。我自己在摸清这几个点之后,项目里凡是遇到按钮网格需求,第一时间就会想到它。