1. 结论摘要
当前对外 SDK 已具备灵活布局能力。第三方不再只能提交固定控件列表,而是可以用 ROW、COLUMN、TEXT、IMAGE、VIDEO、BUTTON 等元素构建可嵌套布局树,在 icyliveX 统一视觉体系内自由组织图片、视频、文字、状态信息和操控按钮。
以魅蓝 blus4 样板图为例,图中的 lifeme 品牌标识、产品图、产品名、电量/EQ/模式信息、两排操作按钮,均可通过 Schema v4 的自由布局协议表达。SDK 开放的是受控布局能力,不是任意 View/Compose/WebView 注入;第三方可以设计结构和内容,最终视觉仍由 icyliveX 统一渲染。
本次修订同步了 SDK 稳定性与易用性口径:统一文档与宿主实现的数量限制,明确 Manifest 静态发现、服务连接完成后注册、本地发送结果与服务端异步返回码的区别,并补充 PendingIntent 与离线快照的降级要求。
| 评估项 | 结论 | 说明 |
|---|---|---|
| 图片/产品图 | 可灵活接入 | 通过 `IMAGE` 或 `VIDEO`,支持资源 URI、宽高比、静音循环等 |
| 文字/设备信息 | 可灵活接入 | 通过 `TEXT`、`INFO`、`STATUS`、`METER` 表达标题、摘要、状态值 |
| 按钮/操作区 | 可灵活接入 | 通过 `BUTTON`、`TOGGLE`、`SEGMENTED`,配合 `ROW` 实现按钮矩阵 |
| 布局自由度 | 已显著增强 | 通过 `ROW` / `COLUMN` / `children` / `weight` / `gapDp` 构建图文并排和分组结构 |
| 视觉一致性 | 由 icyliveX 控制 | 第三方不直接控制字体、颜色、圆角,避免生态 UI 失控 |
2. SDK 功能整合
| 模块 | 能力 | 关键字段/类型 |
|---|---|---|
| 服务绑定 | 第三方 App 绑定 icyliveX 控制面板服务 | `SERVICE_ACTION` |
| 面板注册 | 注册或更新一个第三方控制面板 | `MSG_REGISTER_OR_UPDATE_PANEL` |
| 面板删除 | 删除单个或全部面板 | `MSG_REMOVE_PANEL` / `MSG_REMOVE_ALL_PANELS` |
| 数据传输 | 支持 Parcelable、Bundle、JSON 三种载荷 | `KEY_PANEL` / `KEY_PANEL_BUNDLE` / `KEY_PANEL_JSON` |
| 交互回调 | 用户点击后通过 PendingIntent 回调第三方 App | `EXTRA_ELEMENT_ID` / `EXTRA_VALUE` / `EXTRA_SELECTED_OPTION_INDEX` |
| 媒体展示 | 图片、视频、静音循环动画 | `IMAGE` / `VIDEO` / `mediaUri` |
| 自由布局 | 嵌套行列、权重、间距、卡片表面 | `ROW` / `COLUMN` / `children` / `weight` |
| 连续触发 | 动作触发后可选择保持 SDK 面板展开 | `collapseAfterAction` |
| 静态发现 | 第三方 App 未启动时也可被宿主扫描到接入入口 | `PROVIDER_DISCOVERY_ACTION` / `META_PANEL_*` |
| 稳定性语义 | 区分绑定成功、本地发送成功和服务端处理成功 | `bind()` / Boolean / `ControlPanelResult` |
3. 协议版本与包信息
| 项目 | 值 |
|---|---|
| 当前协议版本 | `SCHEMA_VERSION = 4` |
| icyliveX 包名 | `com.icy.icylivex` |
| 绑定服务 Action | `com.icy.icylivex.action.BIND_CONTROL_PANEL_API` |
| 常量类 | `com.icy.icylivex.api.IcyControlPanelApi` |
| 推荐注册方式 | `IcyControlPanelClient.registerOrUpdatePanelBundle(panel)` |
| 推荐载荷格式 | Bundle 优先,JSON 适合跨语言/调试接入 |
| 静态发现 Action | `com.icy.icylivex.action.DISCOVER_CONTROL_PANEL_PROVIDER` |
| 文档版本 | `2026-07 / External Spec Stability Update` |
4. Schema v4 元素能力
Schema v4 的核心变化,是把面板内容从一维 elements 列表升级为可嵌套布局树。旧版元素仍可直接作为顶层列表使用,新版元素可以通过 children 继续组合子元素。
| 元素类型 | 用途 | 回调 |
|---|---|---|
| `BUTTON` | 单次点击操作,例如查找耳机、启动清洁 | 是,需要 `action` |
| `TOGGLE` | 二元状态,例如开关、低延迟 | 是,需要 `action` |
| `SLIDER` | 数值调节,例如音量、温度、亮度 | 是,需要 `action` |
| `SEGMENTED` | 分段选项,例如模式、EQ、风速 | 是,需要 `action` |
| `STATUS` / `METER` / `INFO` | 只读状态、数值、普通信息 | 否 |
| `IMAGE` | 产品图、品牌图、海报、说明图 | 否 |
| `VIDEO` | 短视频、循环动画、操作演示 | 否 |
| `TEXT` | 标题、摘要、说明文本 | 默认否;带 `action` 时可作为文本入口触发回调 |
| `ROW` | 横向布局容器 | 否 |
| `COLUMN` | 纵向布局容器 | 否 |
| `SPACER` | 留白、间隔、弹性空间 | 否 |
| 布局字段 | 适用对象 | 说明 |
|---|---|---|
| `children` | `ROW` / `COLUMN` | 子元素列表,用于构建嵌套布局 |
| `weight` | 容器子元素 | 按比例分配空间,例如三个按钮均设为 `1f` |
| `gapDp` | `ROW` / `COLUMN` | 控制子元素间距 |
| `paddingDp` | 全部元素 | 控制元素内部留白 |
| `minHeightDp` | 全部元素 | 控制元素最小高度 |
| `surfaceStyle` | 全部元素 | `AUTO` / `NONE` / `CARD`,控制是否显示卡片底板 |
| `collapseAfterAction` | 带 `action` 的可操作元素 | `true` 表示动作后按默认行为收起面板;`false` 表示动作后保持当前 SDK 面板展开,适用于钢琴键、鼓垫、连续调节器等快速连续触发控件。 |
5. 样板图布局映射
样板图中的目标布局可以拆解为四个层次:品牌与设备头部、产品图与信息框、模式按钮区、EQ 按钮区。Schema v4 中推荐使用一个顶层 COLUMN 承接整体结构,内部用 ROW 实现图文并排,用两组 ROW 实现按钮矩阵。

图 1:魅蓝 blus4 样板图,用于说明第三方面板自由布局映射方式
| 样板图区域 | Schema v4 推荐表达 | 说明 |
|---|---|---|
| lifeme logo | `TEXT` 或合成进 `IMAGE` | 当前不强制独立 logo 槽位,推荐用文本或头图表达品牌 |
| 产品图 | `IMAGE` | 使用 `mediaUri` 指向产品图资源 |
| 产品名 | `TEXT` 或面板 `title` | 推荐面板标题使用产品名,卡片内可补充型号 |
| 电量/EQ/模式信息框 | `COLUMN` + `TEXT` / `INFO` | 右侧信息块可独立成一个 `COLUMN` |
| 通透/正常/降噪 | `ROW` + 3 个 `BUTTON` 或 `SEGMENTED` | 需要矩阵视觉时用 `ROW + BUTTON`;需要互斥语义时用 `SEGMENTED` |
| 古典/流行/人声 | `ROW` + 3 个 `BUTTON` 或 `SEGMENTED` | 同上,按业务语义选择 |
6. 魅蓝 blus4 Kotlin 接入样例
val callback = PendingIntent.getBroadcast(
context,
1001,
Intent(context, Blus4ControlReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val panel = ThirdPartyControlPanel(
panelId = "meizu_blus4_main",
title = "魅蓝 blus4",
subtitle = "lifeme 耳机控制",
schemaVersion = IcyControlPanelApi.SCHEMA_VERSION,
elements = listOf(
ThirdPartyControlElement(
id = "device_card",
type = ThirdPartyControlElementType.COLUMN,
surfaceStyle = ThirdPartyControlSurfaceStyle.CARD,
paddingDp = 12,
gapDp = 10,
children = listOf(
ThirdPartyControlElement(
id = "brand_name",
type = ThirdPartyControlElementType.TEXT,
title = "lifeme",
subtitle = "魅蓝 blus4"
),
ThirdPartyControlElement(
id = "hero_row",
type = ThirdPartyControlElementType.ROW,
gapDp = 12,
children = listOf(
ThirdPartyControlElement(
id = "product_image",
type = ThirdPartyControlElementType.IMAGE,
title = "",
weight = 1f,
mediaUri = "content://com.lifeme.assets/blus4/product",
mediaMimeType = "image/webp",
mediaAspectRatio = 1f,
surfaceStyle = ThirdPartyControlSurfaceStyle.NONE
),
ThirdPartyControlElement(
id = "device_info",
type = ThirdPartyControlElementType.TEXT,
title = "电量:90%",
subtitle = "左耳:60% 右耳:70%",
valueText = "EQ:流行\n模式:降噪",
weight = 1.25f,
surfaceStyle = ThirdPartyControlSurfaceStyle.CARD
)
)
)
)
)
)
)
icyClient.registerOrUpdatePanelBundle(panel)
7. JSON 载荷示例
{
"panelId": "meizu_blus4_main",
"title": "魅蓝 blus4",
"subtitle": "lifeme 耳机控制",
"schemaVersion": 4,
"elements": [
{
"id": "device_card",
"type": "COLUMN",
"surfaceStyle": "CARD",
"paddingDp": 12,
"gapDp": 10,
"children": [
{ "id": "brand_name", "type": "TEXT", "title": "lifeme", "subtitle": "魅蓝 blus4" },
{
"id": "hero_row",
"type": "ROW",
"gapDp": 12,
"children": [
{
"id": "product_image",
"type": "IMAGE",
"title": "",
"weight": 1.0,
"surfaceStyle": "NONE",
"mediaUri": "content://com.lifeme.assets/blus4/product",
"mediaMimeType": "image/webp",
"mediaAspectRatio": 1.0
},
{
"id": "device_info",
"type": "TEXT",
"title": "电量:90%",
"subtitle": "左耳:60% 右耳:70%",
"valueText": "EQ:流行\n模式:降噪",
"weight": 1.25,
"surfaceStyle": "CARD"
}
]
}
]
}
]
}
8. 接入流程
- 第三方 App 在 AndroidManifest.xml 中声明 icyliveX 包可见性,并推荐声明静态发现 Service。
- 绑定 com.icy.icylivex.action.BIND_CONTROL_PANEL_API 服务;bindService 或 IcyControlPanelClient.bind() 返回 true 只表示系统接受绑定请求,不表示服务已经连接。
- 等待 onServiceConnected 或 SDK 客户端连接完成后,再构造 ThirdPartyControlPanel,推荐使用 schemaVersion = IcyControlPanelApi.SCHEMA_VERSION。
- 通过 registerOrUpdatePanelBundle(panel) 注册或更新面板;该方法返回 false 表示本地发送失败,服务端最终结果以异步 ControlPanelResult 为准。
- 用户在冰云键的第三方面板接入页面启用该面板。
- 用户点击按钮、开关、滑杆或分段选项后,icyliveX 通过 PendingIntent 回调第三方 App。
- 第三方 App 完成真实设备控制后重新注册面板,同步最新状态。
9. Android Manifest 与 PendingIntent 要求
<queries> <package android:name="com.icy.icylivex" /> </queries>
val callback = PendingIntent.getBroadcast(
context,
1001,
Intent(context, DeviceControlReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
Android 12+ 上,可操作元素必须使用 FLAG_MUTABLE,因为 icyliveX 需要向 fill-in intent 注入 EXTRA_ELEMENT_ID、EXTRA_VALUE、EXTRA_SELECTED_OPTION_INDEX 等操作参数。
绑定请求成功不等于服务已连接。第三方应在 onServiceConnected 后发送注册请求;如果 SDK 注册方法返回 false,应视为当前未连接或本地发送失败,并在 App 前台启动、设备状态恢复或下一次控制回调后重试。
建议为不同元素使用稳定且可区分的 requestCode、Intent action 或 data,避免多个控件复用同一个 PendingIntent 时被系统覆盖。
9.1 静态发现协议(推荐,提升扫描可靠性)
为避免第三方 App 尚未启动、进程被系统回收或未及时主动注册时无法出现在“SDK互联窗”列表中,icyliveX 支持 Manifest 静态发现。第三方 App 除主动 bind 注册外,推荐额外声明一个发现 Service。宿主会通过 PackageManager.queryIntentServices 扫描该声明,并在列表中显示“已发现,等待注册”。
发现 Service 只用于包可见性和接入身份声明,可以不实现业务逻辑;完整面板内容仍通过 IcyControlPanelClient.registerOrUpdatePanelBundle(panel) 或 registerOrUpdatePanelJson(panelJson) 注册。
<service android:name=".IcyControlPanelProviderService" android:exported="true"> <intent-filter> <action android:name="com.icy.icylivex.action.DISCOVER_CONTROL_PANEL_PROVIDER" /> </intent-filter> <meta-data android:name="com.icy.icylivex.metadata.PANEL_PROVIDER_VERSION" android:value="1" /> <meta-data android:name="com.icy.icylivex.metadata.PANEL_ID" android:value="vendor_main" /> <meta-data android:name="com.icy.icylivex.metadata.PANEL_TITLE" android:value="Device Control" /> <meta-data android:name="com.icy.icylivex.metadata.PANEL_SUBTITLE" android:value="Waiting for app registration" /> </service>
宿主列表状态说明:在线表示第三方 App 当前已主动注册完整面板,可启用并可触发带 PendingIntent 的操作;离线快照表示宿主保留了上次注册的只读快照,用户仍可查看降级内容,但快照中的操作通常不可点击;已发现,等待注册表示仅扫描到 Manifest 声明,尚未收到完整面板,暂不能加入冰云控件池。
第三方应在 Application.onCreate、主要前台页面启动、设备状态变化、以及收到开机/连接恢复等关键时机重新调用 registerOrUpdatePanelBundle(panel),确保从“已发现”尽快变为“在线”。
10. 回调参数
| 参数 | 说明 |
|---|---|
| `EXTRA_OWNER_PACKAGE` | 第三方面板所属包名 |
| `EXTRA_PANEL_ID` | 面板 ID |
| `EXTRA_ELEMENT_ID` | 用户操作的元素 ID |
| `EXTRA_ELEMENT_TYPE` | 元素类型 |
| `EXTRA_CHECKED` | TOGGLE 操作后的目标状态 |
| `EXTRA_VALUE` | SLIDER 操作后的目标值 |
| `EXTRA_SELECTED_OPTION_INDEX` | SEGMENTED 操作后的选项下标 |
11. 约束与归一化规则
| 对象/字段 | 当前限制 |
|---|---|
| 单个 App 面板数 | 最多 32 个 |
| 全局第三方面板数 | 最多 128 个 |
| 单个 App 请求速率 | 30 秒内最多 60 次注册、更新、移除类请求;已被用户启用并加入冰云控件池的现有面板,注册/更新可在前台使用场景下超额放行。 |
| 顶层元素数 | 最多 512 个 |
| 容器子元素数 | 每个 ROW/COLUMN 最多 512 个 |
| 布局嵌套深度 | 最多 4 层,根元素按 0 计 |
| Element ID | 最长 64 字符,不能为空 |
| mediaAspectRatio | 0.75 到 2.4 |
| weight | 0 到 6 |
| gapDp | 0 到 24 |
| paddingDp | 0 到 32 |
| minHeightDp | 0 到 240 |
| collapseAfterAction | Boolean,默认 `true`;旧宿主未识别该字段时按 `true` 处理。 |
| Panel subtitle | 最长 120 字符 |
| Element subtitle | 最长 120 字符 |
| mediaMimeType | 最长 80 字符 |
| mediaContentDescription | 最长 80 字符 |
| Panel title | 最长 60 字符 |
| Element title | 最长 64 字符 |
| valueText | 最长 160 字符 |
| mediaUri | 最长 512 字符 |
11.1 稳定性与易用性约定
- 协议硬限制以 SDK 常量和本文档为准;UI 建议值只用于保证面板好看,不作为服务端拒绝条件。
- 服务端对同一第三方 App 的面板请求限流为 30 秒 60 次。已被用户启用并加入冰云控件池的现有面板,在前台使用场景下允许继续提交超额注册/更新请求以保证实时体验;该豁免不适用于首次注册、未启用面板、删除面板或移除全部面板,且仍需遵守载荷大小、元素数量和面板数量限制。
- 绑定、发送、服务端处理结果分三层表达:绑定成功不代表已连接,本地发送成功不代表服务端接受,RESULT_OK 才代表宿主已完成处理。
- 对接入方暴露的 API 包名、字段名、Action、Extra 和返回码应保持长期稳定;内部模块重构不应影响外部接入。
- 新字段应优先采用可忽略的兼容扩展方式,旧宿主未识别时按默认行为处理。
- 宿主应在解析失败、超出限制、权限异常和内部异常时返回明确错误码;第三方应记录错误并提供可诊断日志。
12. 设计与产品建议
- 第三方可自由组织布局结构,但应把高频操作放在最容易触达的位置。
- 品牌 logo、产品图和复杂设备摘要推荐用 ROW/COLUMN/TEXT/IMAGE 组合表达。
- 互斥状态优先用 SEGMENTED;需要按钮矩阵视觉时可用 ROW + BUTTON。
- 媒体资源建议使用稳定 content:// 或 android.resource:// URI,不要使用短生命周期临时缓存 URI。
- 危险操作如开锁、开门、支付,应由第三方 App 自行进行二次确认或风控。
- 面板不是完整 App 页面,不建议承载过长说明、复杂表单或多步骤任务。
13. 安全边界
SDK 开放的是结构化布局能力,不开放任意 View、Compose、WebView 或脚本执行。这样既保证第三方有足够表达力,也保证冰云键整体视觉一致、交互稳定、资源可控。
- 不要在标题、状态文本、媒体 URI 中暴露 token、手机号、门锁密码等敏感信息。
- 回调组件建议 android:exported="false",并使用显式 Intent 构造 PendingIntent。
- 第三方应校验 EXTRA_PANEL_ID 与 EXTRA_ELEMENT_ID,避免误处理其他操作。
- 图片和视频素材需具备合法版权和分发授权。
- 回调到达只表示 PendingIntent 已被宿主派发,不表示第三方业务执行成功;真实设备控制、网络请求、权限校验和失败重试都由第三方负责。
- 第三方应校验 EXTRA_OWNER_PACKAGE、EXTRA_PANEL_ID 与 EXTRA_ELEMENT_ID,并保证关键操作具备幂等保护。
14. 上线检查清单
- Android 11+ 包可见性已配置,并已声明 com.icy.icylivex.action.DISCOVER_CONTROL_PANEL_PROVIDER 静态发现 Service。
- 可操作元素均配置 FLAG_MUTABLE PendingIntent,且不同元素的 requestCode、Intent action 或 data 不会互相覆盖。
- 所有 panelId 与 element.id 稳定,不随启动随机变化。
- 服务连接完成后再注册面板;注册方法返回 false 时按本地发送失败处理,异步 ControlPanelResult 返回非 OK 时按服务端拒绝处理。
- 图片/视频资源在冷启动、进程重启、无网场景下有可接受降级表现。
- 控制执行后会重新注册面板,确保状态回写;纯瞬时连续触发控件可按业务需要跳过刷新以降低延迟。
- 已覆盖 icyliveX 重启、第三方 App 重启、绑定尚未完成即注册、宿主服务断开后重连、设备离线、设备重连、App 未启动但可被静态发现、PendingIntent 被取消或 FLAG_IMMUTABLE 误用等场景。
15. 版本演进建议
建议对外将接入能力分成两档:基础接入使用标准控件流,高级接入使用 Schema v4 自由布局。普通合作方可以快速上线,高价值合作方可以获得更强的品牌化和产品化表达空间。
下一阶段可继续扩展模板命名、资源缓存策略、品牌头图语义、按钮自动换行网格、挂起式注册 API、requestId/超时回调和示例工程等能力;当前 Schema v4 已足够支撑样板图所代表的耳机类面板设计,并通过本次修订明确了稳定接入边界。
16. Schema v4.1 / v4.2 SDK 适配细节
本节为 Schema v4 的向后兼容扩展说明。当前 `IcyControlPanelApi.SCHEMA_VERSION` 仍保持为 4,v4.1 / v4.2 表示宿主与第三方 SDK 在同一协议版本内新增的兼容字段和渲染约定。旧版第三方面板不需要修改即可继续工作。
16.1 面板标题与内容区
`ThirdPartyControlPanel.title` 是 SDK 面板的唯一主标题来源。当用户切换到 SDK 接入面板时,宿主左上角标题应从“冰云控件”切换为当前面板名称。该 title 同时用于 SDK 互联窗开关列表展示;如果 title 为空,宿主回退为“第三方面板”。
`hideHeader` 是 Schema v4.1 兼容字段。当前控件池内容区不再绘制面板内置标题和标题下方小字;`subtitle` 仅用于列表、配置页或旧版宿主兼容信息。第三方不应在 `elements` 内额外放置重复标题或说明文本块。
val panel = ThirdPartyControlPanel(
panelId = "vendor_status_grid",
title = "设备状态", // 宿主左上角标题和 SDK 互联窗列表名称
subtitle = null,
elements = listOf(statusGrid)
).withHiddenHeader()
client.registerOrUpdatePanelBundle(panel)
16.2 标准两行三列信息面板
两行三列信息网格使用 icyliveX 原电量面板 / 天气面板同款结构。宿主已经提供最外侧控件弹窗边缘和 SDK 面板外框,第三方内容应直接从干净的六宫格开始:顶层 `COLUMN` 使用 `surfaceStyle = NONE`、`paddingDp = 0`、`gapDp = 12`;两个子 `ROW` 使用 `weight = 1f`、`gapDp = 10`、`surfaceStyle = NONE`;六个 `INFO` 单元使用 `weight = 1f`、`surfaceStyle = CARD`。
- 每个普通功能默认对应一个 SDK 面板;电量功能可以额外提供一个折线图面板。
- 除折线图等明确需要自定义内容的面板外,第三方面板应优先使用标准六宫格。
- SDK 内容区不提供纵向滚动;六宫格应控制为 6 个简短信息点,避免长段文本。
- 不要在六宫格外再包一层自定义外框;最终视觉应只有宿主弹窗边缘和一个 SDK 面板外框。
val rowTop = ThirdPartyControlElement(
id = "row_top",
type = ThirdPartyControlElementType.ROW,
title = "",
gapDp = 10,
weight = 1f,
surfaceStyle = ThirdPartyControlSurfaceStyle.NONE,
children = listOf(
cell("level", "电量", "60%"),
cell("status", "状态", "充电中"),
cell("power", "功率", "+28.27W")
)
)
val rowBottom = rowTop.copy(
id = "row_bottom",
children = listOf(
cell("voltage", "电压", "4.29V"),
cell("current", "电流", "+6593mA"),
cell("headset", "耳机", "--%")
)
)
val statusGrid = ThirdPartyControlElement(
id = "status_grid",
type = ThirdPartyControlElementType.COLUMN,
title = "",
gapDp = 12,
paddingDp = 0,
surfaceStyle = ThirdPartyControlSurfaceStyle.NONE,
children = listOf(rowTop, rowBottom)
)
fun cell(id: String, title: String, value: String) = ThirdPartyControlElement(
id = id,
type = ThirdPartyControlElementType.INFO,
title = title,
valueText = value,
weight = 1f,
surfaceStyle = ThirdPartyControlSurfaceStyle.CARD
)
16.3 动作后收起策略:collapseAfterAction
`collapseAfterAction` 是 Schema v4.2 兼容扩展字段,定义在 `ThirdPartyControlElement` 上,仅对带 `action` 的可操作元素生效。默认值为 `true`,保持旧版行为:按钮、开关、滑杆或分段选项触发回调后,宿主可以收起 SDK 面板。当字段设置为 `false` 时,宿主在发送 `PendingIntent` 后应保持当前 SDK 面板展开。
- `BUTTON` / `TOGGLE` / `SLIDER` / `SEGMENTED` / 带 action 的 `TEXT` 均可使用该字段。
- 连续触发类控件,例如钢琴键、鼓垫、节拍器、点按测试、连续步进器,应设置 `collapseAfterAction = false`。
- 普通跳转、一次性命令、危险操作或需要第三方 App 承接完整流程的按钮,应保持默认 `true`。
- 字段会通过 Parcelable / Bundle 路径完整传递;JSON 路径可以解析 `collapseAfterAction` 这类普通布尔字段,但无法承载 Android `PendingIntent` 对象。带 `action` 的可操作控件,尤其是需要连续触发的钢琴键、鼓垫、步进按钮等,应优先使用 `registerOrUpdatePanelBundle(panel)` 或 Parcelable 注册。旧宿主未识别时按默认收起处理。
ThirdPartyControlElement(
id = "music_pad_mid_1",
type = ThirdPartyControlElementType.BUTTON,
title = "中音1",
weight = 1f,
minHeightDp = 42,
surfaceStyle = ThirdPartyControlSurfaceStyle.CARD,
collapseAfterAction = false,
action = notePendingIntent("mid_1")
)
16.4 冰云乐盘类连续弹奏面板
冰云乐盘使用一个顶层 `COLUMN` 承载三行 `ROW`,每行 7 个 `BUTTON`,共 21 个音阶:高音 1-7、中音 1-7、低音 1-7。每个琴键都必须配置独立稳定的 `element.id` 和 `PendingIntent`,并设置 `collapseAfterAction = false`,这样用户可以在 SDK 互联窗中连续点击并弹奏成谱。
声音播放由第三方 App 自行实现。推荐第三方使用 `SoundPool` 或同等低延迟音频方案,提前加载短音频样本,并设置足够的 `maxStreams` 以支持多音叠混。icyliveX 宿主只负责渲染结构化面板和派发动作,不合成第三方音频。
val rows = listOf(
"高音" to "high",
"中音" to "mid",
"低音" to "low"
)
val keyboard = ThirdPartyControlElement(
id = "music_pad_keyboard",
type = ThirdPartyControlElementType.COLUMN,
title = "",
gapDp = 6,
paddingDp = 0,
surfaceStyle = ThirdPartyControlSurfaceStyle.NONE,
children = rows.map { (label, idPrefix) ->
ThirdPartyControlElement(
id = "music_pad_${idPrefix}_row",
type = ThirdPartyControlElementType.ROW,
title = "",
gapDp = 4,
paddingDp = 0,
weight = 1f,
surfaceStyle = ThirdPartyControlSurfaceStyle.NONE,
children = (1..7).map { index ->
ThirdPartyControlElement(
id = "music_pad_${idPrefix}_$index",
type = ThirdPartyControlElementType.BUTTON,
title = "$label$index",
weight = 1f,
minHeightDp = 42,
paddingDp = 6,
surfaceStyle = ThirdPartyControlSurfaceStyle.CARD,
collapseAfterAction = false,
action = notePendingIntent("${idPrefix}_$index")
)
}
)
}
)
16.5 宿主与第三方职责边界
- 宿主职责:解析 `collapseAfterAction`,在 false 时发送 action 后保持当前 SDK 面板展开;在 true 或字段缺失时保持旧版收起行为。
- 第三方职责:为连续控件设置稳定 ID、独立 PendingIntent、`FLAG_MUTABLE`,并在回调组件中处理真实业务或音频播放。
- 状态类操作完成后,第三方应重新注册面板同步最新状态;纯瞬时音符类操作可以不刷新面板,避免引入额外延迟。
- 宿主不会保证第三方音频播放、网络请求或设备控制成功;第三方必须自行做失败处理、节流和安全校验。
16.6 兼容性与升级要求
- 现有第三方面板默认 `collapseAfterAction = true`,不需要修改即可继续工作。
- 新 SDK 通过 Bundle / Parcelable 路径写入 `collapseAfterAction`;JSON 注册路径中字段名同样为 `collapseAfterAction`,但 JSON 只能表达普通字段,不能携带 `PendingIntent`。需要点击回调、开关状态写回或连续触发控件时,第三方应使用 Bundle / Parcelable 注册。
- 如果宿主版本较旧且未实现该字段,连续控件仍会在点击后收起;第三方应在接入说明中标注最低宿主版本要求。
- icyliveX 宿主实现该字段后,冰云乐盘、鼓垫、快速点按测试等面板可以在不关闭弹窗的情况下连续触发。