按键功能设置
按键功能设置(Key Settings)是 PiHome(杰理蓝牙 App)中用于查看和配置耳机物理按键与功能映射的能力。本文档基于 KeyBean 数据模型、HeadsetKeyAdapter 列表适配器及对应布局资源,说明该功能从前端列表渲染到 JL 蓝牙 SDK 属性协议(ADV_TYPE_KEY_SETTINGS)之间的完整实现链路。
Purpose and Scope
本页覆盖「按键功能设置」这一设备功能条目的完整实现:
- 数据模型
KeyBean:一个按键配置行的字段、构造方式与 Parcelable 序列化; - 列表渲染
HeadsetKeyAdapter:基于 Section 列表展示「动作(header)+ 按键/功能(item)」的分组结构; - 布局资源:
item_key_settings_header.xml、item_key_settings_one.xml、item_key_settings_two.xml、view_keyboard_input.xml; - 与 JL 蓝牙 SDK 属性协议(
AttrAndFunCode.ADV_TYPE_KEY_SETTINGS)的关联方式。
本目录下的其他设备功能(如音量/音效、EQ、LED 等其他功能设置)属于各自的目录条目,本页不做展开;蓝牙连接、属性读写协议本身也由设备连接相关页面负责,此处仅说明按键设置对它们的依赖方向。
Overview
在杰理蓝牙耳机生态中,耳机上的物理按键(如音量加/减、多功能键)可以通过设备属性协议上报和配置。App 端「按键功能设置」负责把这些按键-动作-功能的映射以可读的方式呈现给用户,并允许用户修改映射后写回设备。
实现上,该功能采用「分组列表」的展示模式:
- 每个**分组头(header)**表示一种按键动作(单击、双击、长按等),对应
KeyBean的action字段; - 分组下的**条目(item)**表示该动作下每个物理按键绑定的功能,对应
key(按键描述)与function(功能描述)字段; - 每个条目携带
resId图标与isShowIcon开关,用于在列表中绘制功能图标。
KeyBean 同时实现 Parcelable 并继承 JSectionEntity,因此既能跨组件(如 Activity/弹窗/Bundle)传递,也能直接作为 BaseSectionQuickAdapter 的分组列表数据源。数据中的 keyId、actionId、funcId 三个 ID 与 SDK 属性协议中的按键/动作/功能枚举一一对应,attrType 默认取 AttrAndFunCode.ADV_TYPE_KEY_SETTINGS,表明该数据属于「按键设置」这一高级属性类型。
Architecture
flowchart TD
subgraph sg_UI["UI 层(承载按键设置页/弹窗)"]
KeyPage["按键设置界面<br/>解析 SDK 数据并构造列表"]
end
subgraph sg_Adapter["列表渲染层"]
Adapter["HeadsetKeyAdapter<br/>(BaseSectionQuickAdapter)"]
end
subgraph sg_Model["数据模型层"]
Bean["KeyBean"]
JSection["JSectionEntity(分组头能力)"]
Parcel["Parcelable(跨组件传递)"]
end
subgraph sg_Layout["布局资源"]
LHeader["item_key_settings_header.xml"]
LOne["item_key_settings_one.xml"]
LTwo["item_key_settings_two.xml"]
LKB["view_keyboard_input.xml"]
end
subgraph sg_SDK["JL 蓝牙 SDK"]
Attr["AttrAndFunCode.ADV_TYPE_KEY_SETTINGS"]
end
KeyPage -->|"setNewData"| Adapter
Adapter -->|"convertHeader / convert"| Bean
Adapter --> LHeader
Adapter --> LOne
Adapter --> LTwo
Bean -->|"继承"| JSection
Bean -->|"实现"| Parcel
Bean -->|"attrType 默认值"| Attr
架构说明:
- 按键设置界面是功能入口(Activity/弹窗层),它从设备或本地缓存取得按键属性数据,解析成
KeyBean列表后交给适配器;该层在本页阅读预算内未展开,其职责由KeyBean/适配器的使用方式可推断。 - HeadsetKeyAdapter 是核心渲染组件,继承
BaseSectionQuickAdapter<KeyBean, BaseViewHolder>,根据KeyBean.isHeader()自动在「分组头布局」与「条目布局」之间分发。 - KeyBean 是唯一的数据载体:既描述分组头(
action),也描述条目(key+function+ 图标),并通过attrType与 SDK 属性协议绑定。 - 布局资源决定两种条目的视觉形态:单行布局(
item_key_settings_one.xml)与双行布局(item_key_settings_two.xml),另有头布局与键盘输入弹窗布局(view_keyboard_input.xml)。
数据模型:KeyBean
KeyBean 位于 data/model/settings 包下,是「按键功能设置」唯一的数据模型。它继承了 com.chad.library.adapter.base.entity.JSectionEntity(RecyclerView 分组列表的基类,提供 isHeader() 抽象),并实现了 android.os.Parcelable,可以在 Intent/Bundle 之间传递。
字段一览
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
keyId | int | 0 | 按键 ID,标识耳机上的某个物理按键 |
actionId | int | 0 | 动作 ID,标识单击/双击/长按等操作 |
funcId | int | 0 | 功能 ID,标识该按键动作绑定的功能(音量+、上一曲等) |
keyName | String | null | 真实按键名 |
key | String | null | 按键描述(列表条目左侧文本) |
action | String | null | 动作描述(分组头文本) |
function | String | null | 功能描述(列表条目右侧文本) |
resId | int | 0 | 功能图标资源 ID |
isShowIcon | boolean | false | 是否显示图标 |
isHideLine | boolean | false | 是否隐藏分隔线 |
attrType | int | ADV_TYPE_KEY_SETTINGS | 高级属性类型,绑定 SDK 协议 |
isHeader | boolean | false | 是否为分组头(由 JSectionEntity 机制使用) |
LAYOUT_ONE / LAYOUT_TWO | int | 0 / 1 | 布局类型常量,供界面区分单行/双行条目 |
字段设计意图:keyId/actionId/funcId 三个 ID 是设备侧协议层的标识,而 key/action/function 三个字符串是App 侧展示层的描述,二者通过构造函数成对传入,避免界面代码直接依赖协议枚举。attrType 允许同一模型复用于其他高级属性(默认即为按键设置),这是为未来扩展预留的钩子。
构造函数
public KeyBean() {
}
public KeyBean(int keyId, String keyName, int actionId, String action, int funcId, String function) {
this(keyId, keyName, actionId, action, funcId, function, AttrAndFunCode.ADV_TYPE_KEY_SETTINGS);
}
public KeyBean(int keyId, String keyName, int actionId, String action, int funcId, String function, int attrType) {
setKeyId(keyId);
setKeyName(keyName);
setActionId(actionId);
setAction(action);
setFuncId(funcId);
setFunction(function);
setAttrType(attrType);
}
Source: KeyBean.java
6 参数构造器默认把 attrType 设为 AttrAndFunCode.ADV_TYPE_KEY_SETTINGS;7 参数构造器允许显式覆盖,用于复用同一模型处理其他属性类型。所有 setter 均返回 this(流畅接口风格,类上标注了 @SuppressWarnings("UnusedReturnValue")),便于链式组装列表数据。
Parcelable 序列化
protected KeyBean(Parcel in) {
keyId = in.readInt();
actionId = in.readInt();
funcId = in.readInt();
keyName = in.readString();
key = in.readString();
action = in.readString();
function = in.readString();
resId = in.readInt();
isHeader = in.readByte() != 0;
isShowIcon = in.readByte() != 0;
isHideLine = in.readByte() != 0;
attrType = in.readInt();
}
Source: KeyBean.java
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(keyId);
dest.writeInt(actionId);
dest.writeInt(funcId);
dest.writeString(keyName);
dest.writeString(key);
dest.writeString(action);
dest.writeString(function);
dest.writeInt(resId);
dest.writeByte((byte) (isHeader ? 1 : 0));
dest.writeByte((byte) (isShowIcon ? 1 : 0));
dest.writeByte((byte) (isHideLine ? 1 : 0));
dest.writeInt(attrType);
}
Source: KeyBean.java
序列化顺序与反序列化顺序严格一致(先 3 个 int ID,再 4 个 String,再 1 个 int 与 3 个 boolean,最后 attrType)。布尔值按字节写入/读取,这是 Android Parcelable 的标准写法;CREATOR 静态工厂提供 createFromParcel 与 newArray。序列化支持的存在说明按键设置列表可能跨组件(页面/弹窗)传递,例如在选择功能后把结果带回列表页。
分组头机制
KeyBean 末尾覆写了 JSectionEntity 的 isHeader():
@Override
public boolean isHeader() {
return isHeader;
}
Source: KeyBean.java
配合 setHeader(boolean)(fluent 返回 this),数据源构造者可以在一份 List<KeyBean> 中混排「分组头」与「条目」两种节点,由 BaseSectionQuickAdapter 自动识别并分发到不同视图类型——这正是下面适配器的设计基础。
列表渲染:HeadsetKeyAdapter
HeadsetKeyAdapter 位于 data/adapter 包,是按键设置列表的渲染核心。它继承 com.chad.library.adapter.base.BaseSectionQuickAdapter<KeyBean, BaseViewHolder>(BRVAH 框架的 Section 适配器),因此无需手动区分头/条目,框架会依据 KeyBean.isHeader() 自动调用不同的绑定方法。
public class HeadsetKeyAdapter extends BaseSectionQuickAdapter<KeyBean, BaseViewHolder> {
@Override
protected void convertHeader(@NotNull BaseViewHolder baseViewHolder, @NotNull KeyBean keyBean) {
baseViewHolder.setText(R.id.tv_key_settings_action, keyBean.getAction());
}
@Override
protected void convert(@NotNull BaseViewHolder baseViewHolder, KeyBean keyBean) {
baseViewHolder.setText(R.id.tv_key_settings_two_key, keyBean.getKey());
baseViewHolder.setText(R.id.tv_key_settings_two_value, keyBean.getFunction());
baseViewHolder.setImageResource(R.id.iv_key_settings_two_img, keyBean.getResId());
}
}
Source: HeadsetKeyAdapter.java
绑定逻辑分析
- convertHeader:只绑定
tv_key_settings_action一个控件,文本来自keyBean.getAction()。分组头在视觉上是动作说明(如「单击」「双击」「长按」),不承载功能信息。 - convert:绑定三处——
tv_key_settings_two_key(按键描述)、tv_key_settings_two_value(功能描述)、iv_key_settings_two_img(功能图标,resId)。条目同时展示「按键 → 功能」的映射关系和功能图标,用户一眼即可确认每个按键当前绑定什么功能。
从控件命名(_two_)可见条目使用双行布局;convertHeader 使用 tv_key_settings_action,对应头布局 item_key_settings_header.xml。两个方法的分工体现了 Section 适配器的核心约定:头部渲染动作、条目渲染映射。
布局资源
| 布局文件 | 用途 |
|---|---|
item_key_settings_header.xml | 分组头布局,承载 tv_key_settings_action |
item_key_settings_one.xml | 单行条目布局(LAYOUT_ONE 场景) |
item_key_settings_two.xml | 双行条目布局(LAYOUT_TWO 场景),承载 tv_key_settings_two_key、tv_key_settings_two_value、iv_key_settings_two_img |
view_keyboard_input.xml | 键盘输入视图布局,用于自定义按键/功能的输入场景 |
布局文件位于 res/layout/ 下,与适配器中的控件 ID 一一对应。LAYOUT_ONE/LAYOUT_TWO 两个常量(KeyBean.LAYOUT_ONE = 0、LAYOUT_TWO = 1)提示界面可在两种条目形态间切换:单行适合紧凑展示,双行适合展示「按键描述 + 功能描述」的完整映射。
与 JL 蓝牙 SDK 的关联
KeyBean 的 attrType 默认值直接引用了 SDK 常量:
private int attrType = AttrAndFunCode.ADV_TYPE_KEY_SETTINGS;
Source: KeyBean.java
该常量来自 com.jieli.bluetooth.constant.AttrAndFunCode(JL 蓝牙 SDK 的「高级属性与功能码」定义)。含义如下:
- ADV_TYPE_KEY_SETTINGS:标识「按键设置」这一高级属性(ADV 属性)类型,是设备与 App 协商该功能的协议入口;
- keyId / actionId / funcId:对应协议中按键、动作、功能三要素的枚举值,App 通过这三元组与设备交换按键配置;
- 设备读写该属性时,App 将 SDK 回调的数据解析为若干组
(keyId, actionId, funcId),再翻译成KeyBean展示;用户修改后反向编码写回设备。
需要说明:本次文档编写受阅读预算限制,未直接读取承载该功能的 Activity/弹窗与 SDK 属性读写回调代码;上述协议交互方向由 KeyBean 的字段设计与 AttrAndFunCode 引用推断得出,具体读写时序请以设备连接/SDK 相关页面为准。
核心流程
按键功能设置的数据流从设备属性到界面渲染,再到用户交互后的写回,整体可概括如下:
sequenceDiagram
participant SDK as JL SDK<br/>(设备属性 ADV_TYPE_KEY_SETTINGS)
participant UI as 按键设置界面
participant KB as KeyBean 列表
participant AD as HeadsetKeyAdapter
participant LV as 列表视图
SDK->>UI: 设备上报按键属性<br/>(keyId/actionId/funcId)
UI->>UI: 解析并翻译为<br/>按键/动作/功能描述
UI->>KB: 构造 KeyBean(含 header 项)
KB->>AD: setNewData / addData
AD->>AD: isHeader() 分发视图类型
AD->>LV: convertHeader 绑定动作文本
AD->>LV: convert 绑定按键/功能/图标
LV-->>AD: 点击条目(选择新功能)
AD-->>UI: 回调选中结果
UI-->>SDK: 编码写回设备按键配置
流程分步说明:
- 数据来源:耳机连接成功后,设备通过高级属性
ADV_TYPE_KEY_SETTINGS上报当前按键配置;SDK 将原始数据交给 App。 - 解析与翻译:App 把协议三元组
(keyId, actionId, funcId)翻译为可读文本(按键名、动作、功能),这是KeyBean6 参数构造函数存在的意义——协议 ID 与展示文本成对传入。 - 构造列表:按动作分组,每个动作生成一个
isHeader=true的KeyBean(只填action),其下为若干条目KeyBean(填key+function+resId)。 - 渲染:
HeadsetKeyAdapter由 BRVAH 框架自动按isHeader()分发到convertHeader/convert,分别填充头布局与双行条目布局。 - 交互与写回:用户点击条目选择新功能后,界面把新
funcId编码回属性协议写回设备;该步骤依赖 SDK 的写属性接口(本次未直接读取,方向由模型设计推断)。
使用示例
示例一:构造按键设置列表数据
以下代码展示了如何利用 6 参数构造器与 setHeader 组装一份「动作头 + 条目」混合列表(结构依据 KeyBean 的公开 API 编写):
List<KeyBean> list = new ArrayList<>();
// 分组头:单击
list.add(new KeyBean().setHeader(true).setAction("单击"));
// 条目:音量加键 → 音量+
list.add(new KeyBean(0x01, "音量加键", 0x01, "单击", 0x10, "音量+"));
// 条目:音量减键 → 音量-
list.add(new KeyBean(0x02, "音量减键", 0x01, "单击", 0x11, "音量-"));
Source: KeyBean.java(基于公开构造函数与
setHeader的用法示例;具体枚举值取决于设备协议)
示例二:Parcelable 跨组件传递
KeyBean 实现 Parcelable 后可直接放入 Bundle:
Intent intent = new Intent(context, KeySettingsActivity.class);
intent.putParcelableArrayListExtra("key_list", (ArrayList<KeyBean>) list);
Source: KeyBean.java(
CREATOR工厂保证createFromParcel可用)
示例三:适配器绑定(真实源码)
见上文「列表渲染」一节:convertHeader 绑定动作文本、convert 绑定按键/功能/图标,直接复用 BRVAH 的 BaseSectionQuickAdapter 机制,页面只需 setNewData(list) 即可完成分组渲染。
配置选项
KeyBean 的字段即本功能的「配置项」——每个字段控制列表渲染的一个方面:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
keyId | int | 0 | 物理按键协议 ID |
actionId | int | 0 | 动作协议 ID(单击/双击/长按) |
funcId | int | 0 | 功能协议 ID |
keyName | String | null | 真实按键名(日志/调试用) |
key | String | null | 列表条目按键描述文本 |
action | String | null | 分组头动作文本 |
function | String | null | 列表条目功能描述文本 |
resId | int | 0 | 功能图标资源 |
isShowIcon | boolean | false | 是否显示图标 |
isHideLine | boolean | false | 是否隐藏分割线 |
attrType | int | ADV_TYPE_KEY_SETTINGS | 高级属性类型,决定数据归属协议 |
isHeader | boolean | false | 标记该节点为分组头 |
API Reference
KeyBean(数据模型)
构造器:
KeyBean()— 空构造,配合 fluent setter 使用KeyBean(int keyId, String keyName, int actionId, String action, int funcId, String function)— 完整构造,attrType取默认ADV_TYPE_KEY_SETTINGSKeyBean(int keyId, String keyName, int actionId, String action, int funcId, String function, int attrType)— 完整构造,可覆盖attrType
Getter / Setter(fluent,返回 this):
| 方法 | 参数类型 | 返回 | 说明 |
|---|---|---|---|
getKeyId() / setKeyId(int) | int | KeyBean | 按键 ID |
getActionId() / setActionId(int) | int | KeyBean | 动作 ID |
getFuncId() / setFuncId(int) | int | KeyBean | 功能 ID |
getKeyName() / setKeyName(String) | String | KeyBean | 真实按键名 |
getKey() / setKey(String) | String | KeyBean | 按键描述 |
getAction() / setAction(String) | String | KeyBean | 动作描述 |
getFunction() / setFunction(String) | String | KeyBean | 功能描述 |
getResId() / setResId(int) | int | KeyBean | 图标资源 |
isShowIcon() / setShowIcon(boolean) | boolean | KeyBean | 图标开关 |
isHideLine() / setHideLine(boolean) | boolean | KeyBean | 分割线开关 |
getAttrType() / setAttrType(int) | int | KeyBean | 高级属性类型 |
setHeader(boolean) | boolean | KeyBean | 标记分组头 |
isHeader() | — | boolean | 是否为分组头(覆写 JSectionEntity) |
describeContents() | — | int | Parcelable 约定,返回 0 |
writeToParcel(Parcel, int) | Parcel, int | void | 序列化全部字段 |
静态常量: CREATOR(Parcelable 工厂)、LAYOUT_ONE = 0、LAYOUT_TWO = 1。
HeadsetKeyAdapter(列表适配器)
HeadsetKeyAdapter()— 构造器,传入布局资源初始化 Section 适配器convertHeader(BaseViewHolder, KeyBean)— 绑定分组头tv_key_settings_actionconvert(BaseViewHolder, KeyBean)— 绑定条目tv_key_settings_two_key/tv_key_settings_two_value/iv_key_settings_two_img
Source: KeyBean.java 与 HeadsetKeyAdapter.java
失败模式与边界情况
- Parcelable 读写不一致:
writeToParcel与Parcel构造器的字段顺序必须严格对称(int → String → boolean → int)。新增字段时若只改一侧,跨组件传递会得到错位数据——这是KeyBean类上标注@SuppressWarnings("UnusedReturnValue")之外最需要警惕的维护点。 resId非法或为 0:convert中无条件调用setImageResource(..., resId);若resId无效会抛出资源异常或显示空白。isShowIcon字段的存在暗示界面可据此跳过图标绑定,但适配器当前实现未做分支(以源码为准)。- 分组头数据缺失:header 节点只填
action,若构造时漏填,头布局文本为空但仍占位,列表分组结构不完整。构造器强制要求keyName/action/function成对传入,可在一定程度上规避该问题。 - 设备不支持按键属性:若设备未实现
ADV_TYPE_KEY_SETTINGS,SDK 不会回调数据,列表为空。该场景的 UI 兜底(空态提示)未在本次读取的源码中出现。
并发与一致性
KeyBean 是纯内存 POJO,无共享可变状态,天然线程安全;列表数据在主线程构造并一次性提交给适配器,符合 BRVAH 的使用约定。写回设备时若与设备断连,写属性请求会失败,App 需要处理回调错误(具体重试/提示逻辑不在本页源码范围内)。
性能与可扩展性
- 性能:Section 适配器复用
BaseViewHolder,convert仅做文本与图片绑定,开销极低;KeyBean无资源句柄持有,列表项即使上百条也不会造成明显内存压力。 - 扩展点:
attrType可扩展——同一KeyBean结构可复用于其他高级属性类型;LAYOUT_ONE/LAYOUT_TWO常量与三套条目布局暗示界面可切换紧凑/详细两种展示形态;isShowIcon/isHideLine为图标显隐、分割线控制预留了开关,便于后续样式定制;- 自定义按键输入弹窗布局
view_keyboard_input.xml表明功能选择支持键盘输入路径,可在此基础上扩展自定义功能码。
测试
本次阅读预算内未发现针对 KeyBean/HeadsetKeyAdapter 的单元测试文件;KeyBean 的 toString() 覆写(输出全部字段)为调试与日志排查提供了便利。建议补充的测试点:Parcelable 序列化往返一致性、header/item 混合列表的分组顺序、attrType 默认值断言。
相关链接
- KeyBean.java(数据模型)
- HeadsetKeyAdapter.java(列表适配器)
- item_key_settings_header.xml(分组头布局)
- item_key_settings_one.xml(单行条目布局)
- item_key_settings_two.xml(双行条目布局)
- view_keyboard_input.xml(键盘输入视图)
- SDK 属性常量
AttrAndFunCode.ADV_TYPE_KEY_SETTINGS定义于 JL 蓝牙 SDK(com.jieli.bluetooth.constant),本仓库内为外部依赖,未随仓库源码提供