icyliveX SDK Integration

冰云SDK接入文档

面向第三方 Android App、设备厂商、IoT 平台与生态合作伙伴的冰云面板接入规范。

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 实现按钮矩阵。

冰云SDK接入文档 图示 1

图 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. 接入流程

  1. 第三方 App 在 AndroidManifest.xml 中声明 icyliveX 包可见性,并推荐声明静态发现 Service。
  2. 绑定 com.icy.icylivex.action.BIND_CONTROL_PANEL_API 服务;bindService 或 IcyControlPanelClient.bind() 返回 true 只表示系统接受绑定请求,不表示服务已经连接。
  3. 等待 onServiceConnected 或 SDK 客户端连接完成后,再构造 ThirdPartyControlPanel,推荐使用 schemaVersion = IcyControlPanelApi.SCHEMA_VERSION。
  4. 通过 registerOrUpdatePanelBundle(panel) 注册或更新面板;该方法返回 false 表示本地发送失败,服务端最终结果以异步 ControlPanelResult 为准。
  5. 用户在冰云键的第三方面板接入页面启用该面板。
  6. 用户点击按钮、开关、滑杆或分段选项后,icyliveX 通过 PendingIntent 回调第三方 App。
  7. 第三方 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 字符,不能为空
mediaAspectRatio0.75 到 2.4
weight0 到 6
gapDp0 到 24
paddingDp0 到 32
minHeightDp0 到 240
collapseAfterActionBoolean,默认 `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 宿主实现该字段后,冰云乐盘、鼓垫、快速点按测试等面板可以在不关闭弹窗的情况下连续触发。