碰一碰是鸿蒙近场分布式交互的标志性能力。到鸿蒙 7 时代,ShareKit 将碰一碰从「手机互传」升级为手机 / 平板 / PC / 2in1 全形态全域支持,同时新增触碰位置感知、沙箱直达接收、星闪链路原生加速等特性,底层依托分布式软总线 2.0 完成建连与传输,真正实现「轻贴即传、直达应用、无感流畅」的近场分享体验。
在这里插入图片描述

本文从新特性解析、前置配置、底层流程、完整双端代码、高级特性实战、避坑指南六个维度,带你从零完成一套可落地的全域碰一碰分享功能。

一、鸿蒙 7 核心升级:从「能传」到「精准全域传」

相比旧版本,鸿蒙 7 的碰一碰分享在覆盖范围、交互精度、传输性能三个维度完成了量级升级。

1. 全域设备覆盖

彻底打破设备形态限制,支持手机、平板、PC、2in1 设备之间的跨形态碰一碰:

  • 手机碰手机:传统近场分享
  • 手机碰平板:大屏接力、文件直传
  • 手机碰 PC:文件直达应用沙箱、内容插入光标位置
  • 所有形态复用同一套 API,一套代码全端生效

2. 触碰位置感知(SDK 26 新增)

接收端可获取触碰点在屏幕上的精确坐标,实现「碰哪里就插到哪里」的精准交互:

  • 文档编辑器:碰一下直接把图片/文字插入光标位置
  • 列表页面:碰对应区域直接把数据追加到对应条目
  • 画布场景:碰触位置直接生成素材元素
  • 完全由系统底层提供坐标,应用无需自行计算偏移

3. 沙箱直达接收

文件类分享不再经过系统下载目录中转,可直接写入目标应用的私有沙箱:

  • 传输完成直接通知应用,无需用户手动选择保存位置
  • 遵循沙箱安全规则,仅授权的文件可被访问
  • 减少中间环节,传输速度更快、隐私性更强

4. 星闪 + 软总线 2.0 底座

近场场景下自动切换星闪链路传输:

  • 空口延迟低至微秒级,大文件传输速度提升 2 倍以上
  • 建连速度从秒级压缩到百毫秒内,用户无感知等待
  • 多链路融合,Wi-Fi 信号差时自动切星闪保障传输稳定

二、前置准备与工程配置

2.1 开发与运行环境

  • SDK 版本:API 26(HarmonyOS 7.0)及以上
  • 工程模型:Stage 模型(FA 模型不支持完整碰一碰能力)
  • 开发工具:DevEco Studio 5.0.1 及以上
  • 硬件要求:两台带 NFC 模块的鸿蒙实体设备,模拟器无法调试
  • 账号要求:双端登录同一华为账号,开启 NFC、蓝牙、华为分享

2.2 权限声明

module.json5requestPermissions 中添加必要权限:

"requestPermissions": [
  {
    "name": "ohos.permission.NFC",
    "reason": "$string:permission_nfc_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.READ_IMAGEVIDEO",
    "reason": "$string:permission_storage_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

说明:纯文本/链接分享无需存储权限,分享文件/图片时按需申请对应媒体权限。

2.3 接收能力注册(接收端必填)

如果应用需要作为接收方,需要在 Ability 的 skills 中声明支持的分享意图与数据类型,系统才能正确匹配并拉起你的应用。

"abilities": [
  {
    "name": "EntryAbility",
    "exported": true,
    "skills": [
      {
        "actions": ["ohos.want.action.sendData"],
        "uris": [
          {
            "scheme": "file",
            "type": "image/*"
          },
          {
            "scheme": "http",
            "type": "text/plain"
          }
        ]
      }
    ]
  }
]
  • actions 固定为 ohos.want.action.sendData,表示接收分享数据
  • uris 按需声明所有支持的数据类型,不匹配的类型不会拉起你的应用

2.4 重要说明:签名要求

碰一碰拉起目标应用依赖应用签名校验,DevEco Studio 自动调试签名无法正常拉起,必须使用与应用包名匹配的正式签名文件,否则会出现「碰了没反应」「无可用打开方式」的问题。

三、底层工作全流程

一次完整的碰一碰分享,从触碰触发到数据落地共经历 5 个阶段,全程由系统统一调度,应用无需关心底层通信细节。

  1. 触碰触发:两台设备 NFC 近场检测,交换设备 ID、账号指纹、能力信息
  2. 极速建连:零感双向认证,自动协商最优传输链路(Wi-Fi / 星闪),建立软总线通道
  3. 应用匹配:系统根据数据类型匹配目标应用,校验签名与权限
  4. 数据传输:系统统一完成数据传输,支持流式进度上报、断点续传
  5. 直达应用:拉起目标应用对应页面,将数据、触碰位置等信息通过 Want 传入

四、完整实战:双端碰一碰分享

下面实现一套完整的「图片 + 文本 + 链接」三类型碰一碰分享,包含发送端和接收端的完整代码。

4.1 发送端实现:构造数据 + 注册触发

发送端核心逻辑:选择分享内容 → 构造标准化分享数据 → 注册碰一碰监听 → 触碰后自动发送。

import { harmonyShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { PhotoViewPicker, photoAccessHelper } from '@kit.ArkUI';

const TAG = 'KnockShareSender';
const DOMAIN = 0x0000;

@Entry
@Component
struct KnockSharePage {
  @State shareType: 'text' | 'url' | 'image' = 'text';
  @State textContent: string = '这是碰一碰分享的测试文本';
  @State urlContent: string = 'https://developer.huawei.com';
  @State imageUri: string = '';
  @State isSharingReady: boolean = false;

  private photoPicker = new PhotoViewPicker();

  aboutToAppear() {
    this.initKnockShare();
  }

  aboutToDisappear() {
    // 页面退出注销监听,避免资源泄漏
    harmonyShare.off('knockShare');
  }

  /**
   * 初始化碰一碰发送能力
   */
  private initKnockShare() {
    // 先检测设备是否支持碰一碰
    if (!harmonyShare.isKnockShareSupported()) {
      promptAction.showToast({ message: '当前设备不支持碰一碰' });
      return;
    }

    // 注册碰一碰触发回调:用户轻碰设备时执行
    harmonyShare.on('knockShare', () => {
      hilog.info(DOMAIN, TAG, '碰一碰触发,开始发送');
      this.doShare();
    });

    this.isSharingReady = true;
    promptAction.showToast({ message: '碰一碰已就绪,轻触另一台设备即可分享' });
  }

  /**
   * 执行分享:根据类型构造数据
   */
  private doShare() {
    let shareData: harmonyShare.ShareData;

    switch (this.shareType) {
      case 'text':
        shareData = {
          utd: utd.UniformDataType.TEXT,
          data: this.textContent,
          title: '文本分享',
          description: '来自碰一碰的文本内容'
        };
        break;

      case 'url':
        shareData = {
          utd: utd.UniformDataType.URL,
          data: this.urlContent,
          title: '链接分享',
          thumbnailUri: '' // 可传入预览图,显示沉浸式大卡
        };
        break;

      case 'image':
        if (!this.imageUri) {
          promptAction.showToast({ message: '请先选择图片' });
          return;
        }
        shareData = {
          utd: utd.UniformDataType.IMAGE,
          uri: this.imageUri,
          title: '图片分享',
          thumbnailUri: this.imageUri // 图片预览
        };
        break;
    }

    // 发送分享数据
    harmonyShare.sendShareData(shareData).then(() => {
      hilog.info(DOMAIN, TAG, '分享发送成功');
      promptAction.showToast({ message: '分享已发送' });
    }).catch((err) => {
      hilog.error(DOMAIN, TAG, `分享失败: ${err.message}`);
      promptAction.showToast({ message: '分享失败,请重试' });
    });
  }

  /**
   * 选择分享图片
   */
  private async pickImage() {
    try {
      const res = await this.photoPicker.select({
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      });
      if (res.photoUris && res.photoUris.length > 0) {
        this.imageUri = res.photoUris[0];
      }
    } catch (e) {
      hilog.error(DOMAIN, TAG, `选图失败: ${JSON.stringify(e)}`);
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('碰一碰分享发送端')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .width('90%')
        .margin({ top: 20 });

      // 类型选择
      Flex({ wrap: FlexWrap.Wrap, gap: 10 }) {
        ['text', 'url', 'image'].forEach(type => {
          Button(type === 'text' ? '文本' : type === 'url' ? '链接' : '图片')
            .height(36)
            .padding({ left: 16, right: 16 })
            .borderRadius(18)
            .backgroundColor(this.shareType === type ? '#0A59F7' : '#F5F5F5')
            .fontColor(this.shareType === type ? '#FFFFFF' : '#333333')
            .onClick(() => this.shareType = type as any);
        });
      }
      .width('90%');

      // 内容输入区
      if (this.shareType === 'text') {
        TextArea({ text: this.textContent })
          .width('90%')
          .height(120)
          .borderRadius(8)
          .padding(12)
          .backgroundColor('#F5F5F5')
          .onChange(val => this.textContent = val);
      } else if (this.shareType === 'url') {
        TextInput({ text: this.urlContent })
          .width('90%')
          .height(44)
          .borderRadius(8)
          .backgroundColor('#F5F5F5')
          .onChange(val => this.urlContent = val);
      } else {
        Button('选择图片')
          .width('90%')
          .height(44)
          .borderRadius(8)
          .onClick(() => this.pickImage());

        if (this.imageUri) {
          Image(this.imageUri)
            .width('90%')
            .height(200)
            .objectFit(ImageFit.Contain)
            .borderRadius(8);
        }
      }

      Blank();

      Text(this.isSharingReady ? '✅ 碰一碰已就绪,将设备背部轻碰另一台设备' : '⏳ 初始化中...')
        .fontSize(14)
        .fontColor(this.isSharingReady ? '#00B578' : '#888888')
        .margin({ bottom: 40 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F9FA');
  }
}

4.2 接收端实现:接收数据 + 获取触碰位置

接收端分两步:首先在 Ability 中接收分享意图,然后在页面中处理数据并获取触碰位置。

第一步:Ability 中接收分享意图

在这里插入图片描述

import { UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'KnockShareReceiver';
const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/ReceivePage', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, TAG, `页面加载失败: ${err.message}`);
      }
    });
  }

  // 碰一碰拉起应用时触发,获取分享数据
  onNewWant(want: Want): void {
    hilog.info(DOMAIN, TAG, `收到分享: ${JSON.stringify(want)}`);
    // 将分享数据存入全局状态,通知页面更新
    AppStorage.setOrCreate('shareWant', want);
  }
}
第二步:页面处理分享数据 + 获取触碰位置

在这里插入图片描述

import { harmonyShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'ReceivePage';
const DOMAIN = 0x0000;

@Entry
@Component
struct ReceivePage {
  @State receivedContent: string = '';
  @State receivedType: string = '';
  @State touchX: number = 0;
  @State touchY: number = 0;

  aboutToAppear() {
    // 注册碰一碰接收回调
    this.registerReceiver();
    // 检查是否是分享拉起的
    const want = AppStorage.get<Want>('shareWant');
    if (want) {
      this.handleShareData(want);
    }
  }

  aboutToDisappear() {
    harmonyShare.off('shareDataReceive');
  }

  private registerReceiver() {
    if (!harmonyShare.isKnockShareSupported()) {
      return;
    }

    // 注册数据接收回调
    harmonyShare.on('shareDataReceive', (event: harmonyShare.ShareReceiveEvent) => {
      hilog.info(DOMAIN, TAG, `收到分享数据: ${JSON.stringify(event)}`);
      
      // ========== 鸿蒙7新特性:获取触碰点坐标 ==========
      if (event.touchPosition) {
        this.touchX = event.touchPosition.x;
        this.touchY = event.touchPosition.y;
        hilog.info(DOMAIN, TAG, `触碰位置: ${this.touchX}, ${this.touchY}`);
      }

      this.handleShareContent(event.data);
    });
  }

  private handleShareContent(data: harmonyShare.ShareData) {
    this.receivedType = data.utd;

    switch (data.utd) {
      case utd.UniformDataType.TEXT:
        this.receivedContent = data.data as string;
        break;
      case utd.UniformDataType.URL:
        this.receivedContent = data.data as string;
        break;
      case utd.UniformDataType.IMAGE:
        this.receivedContent = data.uri || '';
        break;
    }

    promptAction.showToast({ message: '分享接收成功' });
  }

  private handleShareData(want: Want) {
    // 从Want中解析分享数据
    // 实际项目中根据want参数解析对应内容
    promptAction.showToast({ message: '应用被分享拉起' });
  }

  build() {
    Column({ space: 20 }) {
      Text('碰一碰分享接收端')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .width('90%')
        .margin({ top: 20 });

      if (this.touchX > 0) {
        Row() {
          Text('触碰位置:')
            .fontSize(14)
            .fontColor('#888888');
          Text(`X: ${this.touchX.toFixed(0)}, Y: ${this.touchY.toFixed(0)}`)
            .fontSize(14)
            .fontFamily('monospace')
            .fontColor('#0A59F7');
        }
        .width('90%')
        .padding(12)
        .backgroundColor('#F0F7FF')
        .borderRadius(8);
      }

      // 接收内容展示
      if (this.receivedContent) {
        Column({ space: 10 }) {
          Text(`接收类型:${this.receivedType}`)
            .fontSize(14)
            .fontColor('#888888')
            .width('100%');

          if (this.receivedType === utd.UniformDataType.IMAGE) {
            Image(this.receivedContent)
              .width('100%')
              .height(200)
              .objectFit(ImageFit.Contain)
              .borderRadius(8);
          } else {
            Text(this.receivedContent)
              .width('100%')
              .padding(12)
              .backgroundColor('#F5F5F5')
              .borderRadius(8)
              .fontSize(14);
          }
        }
        .width('90%')
        .padding(16)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .shadow({ radius: 4, color: '#1A000000' });
      }

      Blank();
      Text('💡 将另一台设备轻触本机背部即可接收分享')
        .fontSize(12)
        .fontColor('#AAAAAA')
        .margin({ bottom: 30 });
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F9FA');
  }
}

五、高级特性实战:触碰位置精准插入

鸿蒙 7 最具交互价值的升级就是触碰位置感知,我们以「编辑器插入图片」为例,实现「碰哪里就插到哪里」的精准交互。

核心逻辑:

  1. 记录编辑器中每行的位置范围
  2. 收到分享时,根据触碰 Y 坐标计算对应的行索引
  3. 将分享内容插入到对应行的位置
// 根据触碰Y坐标计算插入行号
private calcInsertLine(touchY: number, lineHeight: number, topOffset: number): number {
  const relativeY = touchY - topOffset;
  const lineIndex = Math.floor(relativeY / lineHeight);
  return Math.max(0, lineIndex);
}

// 接收分享后执行插入
private insertAtPosition(content: string, touchY: number) {
  const lineIndex = this.calcInsertLine(touchY, 48, 100);
  this.contentList.splice(lineIndex, 0, content);
  // 插入动效反馈
  this.highlightLine(lineIndex);
}

这种交互在文档协作、素材管理、多端编辑场景下体验提升非常明显,完全摆脱了「分享 → 复制 → 找位置粘贴」的繁琐流程。

六、调试验证步骤

  1. 配置签名:使用正式签名文件,分别编译发送端和接收端安装包
  2. 设备准备:两台设备登录同一华为账号,开启 NFC、蓝牙、华为分享,保持亮屏解锁
  3. 发送端操作:打开发送页面,选择分享类型,输入内容
  4. 触发分享:将两台设备背部轻贴(NFC 区域通常在摄像头附近)
  5. 验证结果:接收端自动弹出应用,显示分享内容与触碰位置坐标
  6. 多类型测试:分别测试文本、链接、图片三种类型,验证接收是否正常

七、高频踩坑避坑指南

1. 碰了完全没反应

排查优先级:

  • 双端是否都开启 NFC、华为分享、蓝牙
  • 是否登录同一华为账号,是否亮屏解锁
  • 应用是否使用正式签名,包名与签名是否匹配
  • 接收端 skills 配置是否正确,数据类型是否匹配

2. 提示「暂无可用打开方式」

  • 接收端没有配置正确的 skills,系统找不到能处理该类型的应用
  • 数据类型与声明的 MIME 类型不匹配
  • 应用未安装,或签名不匹配被系统拦截

3. 触碰位置获取不到

  • 确认 SDK 版本 ≥ API 26,低版本无该字段
  • 只有碰一碰触发的分享才带位置信息,系统分享面板触发的没有
  • 坐标是屏幕绝对坐标,需要减去页面顶部偏移和状态栏高度

4. 文件分享失败

  • 单个文件大小不要超过系统限制,大文件建议走 ShareKit 文件专属通道
  • 文件 URI 必须是系统认可的合法 URI,不能直接传沙箱路径
  • 接收端需要对应文件类型的权限声明

5. 模拟器无法调试

碰一碰依赖 NFC 硬件与系统华为分享服务,模拟器不支持,必须使用真机调试。

八、最佳实践

  1. 卡片体验优化:根据内容类型选择对应卡片模板,配图 + 标题的沉浸式大卡体验远好于纯文本
  2. 优雅降级:不支持碰一碰的设备,降级为系统分享面板,保证功能可用
  3. 单向发送配置:如果双方都在发送页面,可设置 sendOnly: true 避免双向冲突
  4. 进度反馈:大文件分享时展示进度条与取消按钮,不要让用户无意义等待
  5. 隐私合规:分享敏感数据前增加二次确认,避免误触泄露隐私

总结

ShareKit 全域碰一碰,是鸿蒙近场分布式体验的典型缩影:它把复杂的设备发现、安全认证、传输调度全部封装在系统底层,开发者只需要构造标准化的分享数据,就能获得全设备形态一致的近场分享体验。

鸿蒙 7 新增的触碰位置感知与沙箱直达能力,进一步把分享的精度和深度往前推了一步,从「能传过去」变成「精准传到指定位置、直接进入业务流程」,让近场协同的体验更加无感自然。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐