技术栈:HarmonyOS 7、ArkTS、Share Kit、坐标命中测试、幂等门禁

Demo:HitShare;任务:HS-606;本文坐标、日志和回调数量为构造示例,不是实际碰一碰传输结果。

一、一次轻碰,先回答“碰中的是什么”

在大屏上摆出一排照片,用户拿手机轻轻碰向其中一张,希望分享的就是那张,而不是当前列表默认选中的照片。精准碰一碰把一个看似简单的业务问题推到了前面:系统回调给了触点,应用怎样把触点稳妥地映射到业务对象?如果没有命中,能否明确拒绝,而不是退回“分享最后一次选中项”?

华为开发者联盟的 HarmonyOS 7(API 26)精准分享技术话题明确介绍了“碰哪传哪”的场景,并提到发送端 sharableTarget.getInfo()、接收端 receivableTarget.getInfo() 可用于获取触点位置。相关能力与设备、系统版本及场景有关,尤其强调 API 26 及以上电脑设备的适配条件。本文不假装有配对设备,也不拼凑尚未核对签名的监听接口;只把收到合法坐标之后的应用侧决策写完整。

Demo 名为 HitShare。假设发送侧共享画布大小为 800×520 逻辑展示区域,触点位置是 (420,236);画布有两张可分享卡片,分别对应 IMG-025 与 IMG-024,当前坐标只落在 IMG-024 内。任务编号 HS-606,一次触发标识 EV-021 被业务适配层重复提交三次,演示预期是接受一次、拦截重复两次。准备状态写为 READY_TO_SHARE,实际系统分享状态明确标记 NOT_SENT。

二、坐标不能直接等于列表索引

如果产品仍用“触点 x 小于半屏就是第一个,否则就是第二个”来算,遇到侧边栏、折叠屏、列表滚动或缩放就会错。触点可能相对于整块屏幕,而业务卡片的 left/top 可能相对于内容容器;坐标空间不统一,再精妙的命中算法也无法补救。先进行坐标换算,才讨论命中测试。

本例暂时固定在同一坐标系:触点和卡片边界都被转换为应用画布内的同一单位。示例卡片 IMG-025 的矩形是 [40,160,340,400),IMG-024 的矩形是 [360,160,660,400);采用左闭右开的边界规则。触点 (420,236) 必须命中后者,不能因为它在视觉上更靠右,就把系统默认分享对象切成“最近一次点击”。

第一段代码解决“同一坐标空间如何确定唯一命中”的问题。它不依赖 Share Kit 私有接口,可作为收到回调后的纯业务函数,方便单测;它也不执行任何设备发现或文件发送。

export interface TouchPoint {
  x: number;
  y: number;
}

export interface HitBox {
  assetId: string;
  left: number;
  top: number;
  right: number;
  bottom: number;
}

export function pickAsset(point: TouchPoint, boxes: HitBox[]): string | null {
  if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
  for (const box of boxes) {
    if (point.x >= box.left && point.x < box.right &&
        point.y >= box.top && point.y < box.bottom) {
      return box.assetId;
    }
  }
  return null;
}

这段代码的循环顺序就是一项产品决策:当卡片重叠时,谁先进入数组,谁可能先命中。正式项目应按可见层级、遮挡和交互禁用状态组织目标集合,而不是让“数组顺序”暗中决定最终分享内容。它还需要排除透明遮挡层、动画尚未结束的坐标与滚出可见区域的对象。对没有命中的触点返回 null,不允许悄悄替换为默认图片。

三、同一次触发的重复回调,要挡在分享前

精准分享的另一个难点是回调重入。用户只碰了一次,业务层可能由于事件转发、状态恢复或自己的监听重复注册,把同一个触发标识交给处理器多次。这里不必先讨论无线连接层做了几次握手,单独在应用层就应该保证“同一个事件只准备一个分享内容”。

我把门禁写成带过期时间的映射,而不是只存一个全局 lastTap。键是由调用方提供的稳定触发标识,值是过期时间。三秒只是本文用于演示的清理窗口,不代表系统触碰事件的官方有效期,更不能充当鉴权凭据。

export class EventGate {
  private expires: Map<string, number> = new Map<string, number>();

  accept(token: string, nowMs: number): boolean {
    if (token.length === 0) return false;
    const deadline: number | undefined = this.expires.get(token);
    if (deadline !== undefined && deadline > nowMs) return false;
    this.expires.set(token, nowMs + 3000);
    return true;
  }

  clear(): void {
    this.expires.clear();
  }
}

它能防止一个会话实例的短时间重复消费,但不能证明“不同设备一定不会发出重复文件”。如果事件到达时间出现回拨,或者进程在准备过程中退出重建,仅靠内存 Map 不具备跨重启幂等能力。生产级场景需要由 Share Kit 事件标识、分享任务标识、业务持久化状态共同定义一个去重键;没有真实回调字段文档,就不应把本例 EV-021 冒充为系统原生字段。

过期的 key 还应定期清理。Demo 在页面离开时 clear(),适合短生命周期调试,但正式业务里若分享过程尚未确认终止,清空门禁可能允许旧事件再次进入。生命周期处置需要与分享任务状态协调,不能只是图省事地清空集合。

四、命中后是“准备分享”,不是“已经传送”

把 pickAsset 和 EventGate 连起来,业务中间层就有了清晰的出口:没有命中返回空;事件重复返回空;只有对象存在且事件未被消费过,才形成 READY_TO_SHARE 的准备记录。注意真正的 sharableTarget.share() 触发、分享数据构建及发送完成事件应按官方指南在 Share Kit 适配层实现,不在这个未经设备验证的演示里伪造。

export interface PreparedTap {
  taskId: string;
  assetId: string;
  state: string;
}

export class SharePreparation {
  private gate: EventGate = new EventGate();

  prepare(token: string, point: TouchPoint,
          targets: HitBox[], nowMs: number): PreparedTap | null {
    const assetId: string | null = pickAsset(point, targets);
    if (assetId === null) return null;
    if (!this.gate.accept(token, nowMs)) return null;
    return { taskId: 'HS-606', assetId: assetId, state: 'READY_TO_SHARE' };
  }
}

状态 READY_TO_SHARE 的含义仅是本地业务已经确定候选内容,它不表示用户授权、系统分享面板已弹出,更不代表对端已接收。很多“分享成功率”统计之所以失真,就是把点击、准备、提交、对端确认这些不同阶段都塞进一个 success 字段。这里主动留出 NOT_SENT,是为了让后续真机验收可以单独覆盖无线链路与权限处理。

另外,不应该在取到一个 assetId 之后立即信任它。真正构造可分享数据之前,还需确认这个资源仍存在、调用方有访问授权、路径未失效、共享范围是否合适。卡片命中只解决“选中了谁”,并没有解决“是否有权发送”和“接收端如何落地”。

五、用一个明确的例子核对文图数据

本轮统一演示 HS-606 / EV-021。触点 (420,236) 命中 IMG-024;在三个具有相同 token 的模型调用中,第一次形成 READY_TO_SHARE,后两次由 EventGate 拒绝。预设统计为 accepted=1、duplicates=2、noMatch=0,系统发送保持 NOT_SENT。对应的演示日志严格使用:14:41:10 [HitShare] HS-606 point=(420,236) token=EV-021、14:41:11 [HitShare] HS-606 hit=IMG-024 accepted=1、14:41:11 [HitShare] HS-606 duplicate=2 state=READY_TO_SHARE。

这组数字只描述模型输入与预期,不应被写成“在某型号平板上碰了三次,系统实际只触发一次”。真正设备侧至少需要记录屏幕方向、密度倍率、卡片布局矩形、系统事件发生时间、最终提交到 Share Kit 的数据标识,以及对端收到的实际内容。现有示例完全没有实机触碰和传输证据。

六、工程验收要覆盖无匹配和临界点

命中成功只是最轻松的一项。更值得测试的是:用户碰在两张卡片之间怎么办?卡片刚滚出视口怎么办?屏幕旋转之后旧矩形还有效吗?共享区域上盖着弹层时,底下的图片还能被分享吗?同一个 token 分别落在两个位置时,应该采用首次位置还是拒绝整组事件?这些不是算法语言能自动回答的,需要产品、系统交互与风控规则一起确定。

我会把测试拆成纯函数与系统集成两份。纯函数测试给固定矩形喂边界坐标,验证 [left,right) 与 [top,bottom) 规则、坐标无穷值拒绝、重叠层级和无匹配;门禁测试验证同 token 的一次接受与重复拒绝,以及过期处理。集成测试必须在符合支持范围的设备上逐项确认坐标来源、坐标系换算、回调生命周期和实际分享行为,再讨论能否给产品展示“碰哪传哪”的交付效果。

还有一类错误常常隐藏在页面收尾:若页面在分享准备期销毁,旧的 callback 不应该继续读取已经销毁的卡片树。比较稳妥的实现方式是把当前布局快照转成纯数据传给准备器,任务结束后由独立服务释放快照;返回页面时重新计算命中区域,而不是复用旧的矩形缓存。

七、先把确定性做在业务层,再接设备能力

精准碰一碰的价值是让内容分享更有指向性,但“精确”的前提不是把触点映射得更快,而是定义错误时不做什么。没有命中就不发送,坐标空间不一致就先转换,重复事件不重复准备,页面不存在就不回写 UI,资源无权限就停在校验阶段。只要这几个门槛能被独立验证,日后接入具体设备与 Share Kit 回调时,问题边界会清晰得多。

本例故意不展示一个“已发送成功”的绿色终态,也没有编造某个 SDK 的回调参数。它示范的是应用准备层可直接复用的三段 ArkTS 业务逻辑,以及从坐标到业务对象的可审查过程。真正交付还需结合官方 Share Kit 指南实现设备端连接、数据构造、发送回执和事件监听释放。

资料核对: 华为开发者联盟《HarmonyOS 7(API 26)碰一碰能力增强解读》(2026年,含精准触点位置及 sharableTarget.getInfo() / receivableTarget.getInfo() 的场景说明),https://developer.huawei.com/consumer/cn/forum/topic/0208223729721325967 ;其指向的官方开发指南为 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/knock-share-pc-phones-mutually 。该指南正文在本轮读取时超时,因此未声称核实其精确事件回调签名。本例的 TouchPoint、EventGate、SharePreparation 均为应用自定义类型。没有进行实际碰一碰、Share Kit 调用或真机测试。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐