鸿蒙实战:NFC Kit 与碰一碰功能设计

前言(必读)

在这里插入图片描述

图:鸿蒙实战:NFC Kit 与碰一碰功能设计(第 64 篇) 运行效果截图(HarmonyOS NEXT)

NFC(Near Field Communication,近场通信)是鸿蒙生态中「多设备协同」的核心能力之一。在本项目中,NFC 承担着双人合盘场景下的便捷配对功能——用户碰一下对方的手机或将手机靠近 NFC 标签,即可分享笔迹分析结果。本文深入讲解 NFC Kit 的 Tag 读写能力与 TapSharePage 的交互设计。

NFC Kit碰一碰功能设计示意图

图:NFC Kit 架构——Tag 读写 / HCE 主机卡模拟 / 碰一碰三大能力

NFC Kit

Tag 读写

HCE 主机卡模拟

碰一碰 TapShare

NfcA / NfcB / IsoDep

NDEF 标准格式读写

MifareClassic 高级读写

模拟交通卡/银行卡

靠近触发 onTag 回调

JSON 数据封装传输

配对码校验与加密


一、鸿蒙 NFC 技术栈概览

1.1 NFC Kit 模块架构

@kit.ConnectedTagKit
├── @ohos.nfc.tag        ← Tag 发现与读写(核心)
├── @ohos.nfc.controller ← NFC 开关控制
├── @ohos.nfc.cardEmulation ← 卡模拟(本文不涉及)
└── 子模块(NDEF / NfcA / NfcB / IsoDep / MifareClassic)

1.2 核心 API 一览

API 功能 使用场景
tag.on('tagDiscovery') 监听 Tag 被发现 碰一碰触发分享
tag.getNdefTag(tagInfo) 获取 NDEF Tag 实例 读写 NDEF 数据
ndefTag.connect() 连接 Tag 读写前准备
ndefTag.readNdef() 读取 NDEF 记录 读取对方信息
ndefTag.writeNdef(records) 写入 NDEF 记录 写入笔迹分享数据
ndefTag.close() 关闭连接 资源释放
nfcController.isNfcOpen() 检查 NFC 是否开启 引导用户开启 NFC
nfcController.openNfc() 打开 NFC(API 12+) 一键开启

二、TapSharePage 碰一碰交互设计

2.1 页面架构

// 文件:pages/TapSharePage.ets
@Entry
@Component
struct TapSharePage {
  @State userName: string = '小李'
  @State personalityType: string = '温和理性型'
  @State shareContent: string = '雷达·情绪·解读·拍摄原图'
  // 动画状态
  @State rippleScale: number = 1.0
  @State rippleOpacity: number = 0.3
  @State cardOpacity: number = 0
  @State cardOffset: number = 30
  @State btnScales: number[] = [1.0, 1.0]
  @State isSharing: boolean = false
  @State shareSuccess: boolean = false
  @State pulseActive: boolean = true

  aboutToAppear() {
    // 读取用户信息
    const name = AppStorage.get<string>('user_name')
    if (name) this.userName = name

    // 读取分析结果
    const result = AppStorage.get<Record<string, Object>>('analysis_result')
    if (result) {
      const personality = result['personality_type']
      if (typeof personality === 'string') this.personalityType = personality
    }

    // 入场动画
    setTimeout(() => {
      animateTo({ duration: 600, curve: Curve.FastOutSlowIn }, () => {
        this.cardOpacity = 1
        this.cardOffset = 0
      })
    }, 200)

    // 启动脉冲动画
    this.startPulseAnimation()
  }
}

2.2 脉冲动画(NFC 信号提示)

private startPulseAnimation(): void {
  const animate = () => {
    if (!this.pulseActive) return

    // 外层波纹环放大淡出
    animateTo({ duration: 1500, curve: Curve.EaseInOut }, () => {
      this.rippleScale = 1.3
      this.rippleOpacity = 0
    })

    // 重置
    setTimeout(() => {
      animateTo({ duration: 0 }, () => {
        this.rippleScale = 1.0
        this.rippleOpacity = 0.3
      })
      setTimeout(() => animate(), 200)
    }, 1500)
  }

  setTimeout(() => animate(), 500)
}

2.3 三层波纹 UI 结构

// 三层波纹环的 UI 层叠
Stack() {
  // 外层波纹环(240px · 虚线边框 · 脉冲缩放)
  Column()
    .width(240).height(240).borderRadius(120)
    .border({
      width: 1,
      color: `rgba(168,144,122,${this.rippleOpacity})`,
      style: BorderStyle.Dashed
    })
    .scale({ x: this.rippleScale, y: this.rippleScale })

  // 中层波纹环(190px · 略有相位偏移)
  Column()
    .width(190).height(190).borderRadius(95)
    .border({
      width: 1,
      color: `rgba(168,144,122,${this.rippleOpacity + 0.1})`,
      style: BorderStyle.Dashed
    })
    .scale({ x: this.rippleScale * 0.9, y: this.rippleScale * 0.9 })

  // 中心渐变圆(WARM → PRIMARY 渐变)
  Column() {
    Text('⊕').fontSize(42).fontColor(Color.White)
  }
  .width(140).height(140).borderRadius(70)
  .linearGradient({
    angle: 0,
    colors: [[AppColors.WARM, 0], [AppColors.PRIMARY, 1]]
  })
  .scale({ x: this.rippleScale * 0.7, y: this.rippleScale * 0.7 })
}

三、NFC Tag 读写核心代码

3.1 NDEF 数据格式

NDEF(NFC Data Exchange Format)是 NFC Forum 定义的标准数据格式:

NDEF 记录结构:
┌─────────┬──────────┬──────────┬──────────┐
│ TNF(3)  │  Type    │  ID      │  Payload │
│ 0x01    │  'U'     │  (空)    │  URI     │
└─────────┴──────────┴──────────┴──────────┘

TNF (Type Name Format):
  0x00 = Empty
  0x01 = NFC Forum well-known type (NFC 标准类型)
  0x02 = Media-type (RFC 2046)
  0x03 = Absolute URI (RFC 3986)
  0x04 = External type (自定义类型)

3.2 Tag 发现与读取

import { tag } from '@ohos.nfc.tag'
import { nfcController } from '@ohos.nfc.controller'

const TAG_NAME = 'NfcShareService'

/**
 * NFC Tag 发现监听
 */
function startTagDiscovery(): void {
  tag.on('tagDiscovery', (tagInfo: tag.TagInfo) => {
    console.log(`${TAG_NAME}: Tag 发现 - ${tagInfo.uid}`)

    // 尝试获取 NDEF Tag 实例
    const ndefTag = tag.getNdefTag(tagInfo)
    if (!ndefTag) {
      console.warn(`${TAG_NAME}: 非 NDEF Tag`)
      return
    }

    // 异步读取
    readNdefTag(ndefTag)
  })
}

/**
 * 读取 NDEF Tag 内容
 */
async function readNdefTag(ndefTag: tag.NdefTag): Promise<void> {
  try {
    // 1. 连接 Tag
    await ndefTag.connect()

    // 2. 读取 NDEF 记录
    const ndefMessages = await ndefTag.readNdef()

    if (ndefMessages && ndefMessages.length > 0) {
      for (const msg of ndefMessages) {
        // 解码 payload(UTF-8 文本)
        const decoder = new util.TextDecoder('utf-8')
        const payload = decoder.decodeWithStream(msg.payload)
        console.log(`${TAG_NAME}: NDEF 内容 - ${payload}`)

        // 解析自定义协议数据
        if (payload.startsWith('lulu://')) {
          const shareData = parseShareProtocol(payload)
          handleIncomingShare(shareData)
        }
      }
    }

    // 3. 关闭连接
    ndefTag.close()

  } catch (error) {
    console.error(`${TAG_NAME}: 读取失败 - ${JSON.stringify(error)}`)
  }
}

3.3 写入 NDEF 记录

/**
 * 将笔迹分析数据写入 NFC Tag
 */
async function writeShareToTag(
  ndefTag: tag.NdefTag,
  shareData: ShareProtocol
): Promise<void> {
  try {
    await ndefTag.connect()

    // 构建自定义协议文本
    const protocolText = buildShareProtocol(shareData)
    // 示例: "lulu://share?name=小李&type=温和理性型&eid=xxx"

    // 创建文本 NDEF 记录
    // TNF=0x01 (NFC Forum well-known), Type='T' (Text Record)
    const textEncoder = new util.TextEncoder()
    const payload = textEncoder.encodeInto(protocolText)

    // NDEF Record: TNF(1 byte) + Type Length(1 byte) + 
    //              Payload Length(2 bytes) + Type + Payload
    const record = {
      tnf: 0x01,                         // NFC Forum well-known type
      type: new Uint8Array([0x54]),      // 'T' = Text Record
      id: new Uint8Array([]),
      payload: payload.buffer as ArrayBuffer
    }

    // 写入 Tag
    await ndefTag.writeNdef([record])
    console.log(`${TAG_NAME}: NDEF 写入成功`)

    ndefTag.close()

  } catch (error) {
    console.error(`${TAG_NAME}: NDEF 写入失败 - ${JSON.stringify(error)}`)
  }
}

3.4 自定义分享协议

interface ShareProtocol {
  userName: string
  personalityType: string
  energyScore: number
  archiveId: number
  timestamp: number
}

/**
 * 构建分享协议文本
 * 格式:lulu://share?name=<name>&type=<type>&energy=<e>&id=<id>&t=<timestamp>
 */
function buildShareProtocol(data: ShareProtocol): string {
  const params = new URLSearchParams()
  params.append('name', data.userName)
  params.append('type', data.personalityType)
  params.append('energy', data.energyScore.toString())
  params.append('id', data.archiveId.toString())
  params.append('t', data.timestamp.toString())
  return `lulu://share?${params.toString()}`
}

/**
 * 解析分享协议
 */
function parseShareProtocol(text: string): ShareProtocol {
  const url = new URL(text)
  const params = url.searchParams
  return {
    userName: params.get('name') ?? '',
    personalityType: params.get('type') ?? '',
    energyScore: parseInt(params.get('energy') ?? '0'),
    archiveId: parseInt(params.get('id') ?? '0'),
    timestamp: parseInt(params.get('t') ?? '0')
  }
}

四、NFC 权限与配置

4.1 module.json5 配置

{
  module: {
    // ...
    requestPermissions: [
      {
        name: 'ohos.permission.NFC_TAG',
        reason: '$string:permission_nfc_tag_reason',
        usedScene: {
          abilities: ['EntryAbility'],
          when: 'inuse'
        }
      }
    ]
  }
}

4.2 NFC 状态检查与引导

/**
 * 检查 NFC 状态并引导用户开启
 */
async function ensureNfcEnabled(): Promise<boolean> {
  if (nfcController.isNfcOpen()) {
    return true
  }

  // 显示引导提示
  promptAction.showToast({
    message: '请开启 NFC 功能以使用碰一碰分享',
    duration: 3000
  })

  // 尝试自动开启(API 12+)
  try {
    await nfcController.openNfc()
    return true
  } catch {
    // 自动开启失败,引导用户手动开启
    return false
  }
}

五、多通道分享策略

TapSharePage 不仅支持 NFC,还提供了 QR 码和超级终端作为备选:

分享方式 触发方式 适用距离 是否需要 NFC
NFC 碰一碰 手机碰手机/NFC 标签 接触(< 4cm) ✅ 需要
二维码 对方扫码 远程 ❌ 不需要
超级终端 多设备推送 同账号/同网络 ❌ 不需要
剪贴板 粘贴文本 任意 ❌ 不需要
// TapSharePage.ets 中的操作按钮
Row() {
  // 通道 1:生成二维码
  Button('生成二维码')
    .onClick(() => this.generateQRCode())

  // 通道 2:超级终端推送
  Button('超级终端推送')
    .onClick(() => this.pushViaSuperTerminal())
}

六、NFC 安全设计

安全风险 缓解措施
数据窃听 NDEF 数据不含个人身份信息,仅含协议码
重放攻击 每条分享包含时间戳 t,接收方校验
未授权读取 NFC 仅在应用前台时启用 Tag 监听
数据完整性 应用内校验协议格式,非法数据丢弃

前台分发策略

// 仅在应用前台时接收 Tag 事件
import { uiAbility } from '@kit.AbilityKit'

class EntryAbility extends uiAbility.UIAbility {
  onForeground() {
    // 应用进入前台 → 开启 Tag 分发
    tag.on('tagDiscovery', this.handleTagDiscovery)
  }

  onBackground() {
    // 应用进入后台 → 停止 Tag 分发
    tag.off('tagDiscovery', this.handleTagDiscovery)
  }
}

七、NFC vs 其他短距通信技术

特性 NFC 蓝牙 Wi-Fi Direct
通信距离 < 4cm ≤ 10m ≤ 200m
传输速率 106-424kbps 1-3Mbps 250Mbps
配对时间 < 0.1s 2-10s 3-10s
功耗 极低 中等
无需用户显式配对
鸿蒙 API 支持 @kit.ConnectedTagKit @kit.ConnectivityKit @kit.ConnectivityKit

八、FAQ 常见问题

Q1:NFC 碰一碰必须开启前台分发吗?

是的。如果不启用前台分发,Tag 事件会被系统捕获并打开默认应用。通过 tag.on('tagDiscovery') 在前台注册监听,可以确保应用自己处理 Tag 事件。

Q2:NDEF 写入失败怎么办?

常见原因:① Tag 处于写保护状态;② Tag 容量不足;③ 连接超时。建议使用可擦写的 NTAG213/215/216 标签进行开发测试。

Q3:如何测试 NFC 功能?

在 DevEco Studio 中可以使用 Huawei DevEco Tool 中的 NFC Tag 模拟器。或者使用支持 NFC 的真机 + 空白 NTAG213 标签。

Q4:自定义协议数据有长度限制吗?

NDEF 标准单条记录的最大长度为 2^32-1 bytes,但实际 Tag 硬件有物理限制:NTAG213 仅 144 bytes,NTAG216 约 888 bytes。因此建议协议数据控制在 100 bytes 以内。


九、注意事项与常见问题

9.1 开发注意事项

说明: 以下注意事项基于 HarmonyOS NEXT 实际项目开发经验整理,建议在动手开发前仔细阅读,可有效避免常见坑点。

在正式开发前,建议按以下步骤完成环境准备与前置检查:

  1. 版本确认:检查 DevEco Studio 与 SDK 版本,确保满足目标 API Level 要求
  2. 权限声明:在 module.json5requestPermissions 字段中提前声明所有需要的系统权限
  3. 设备能力检查:调用前验证设备是否支持目标能力(相机、NFC、传感器等)
  4. 异步封装:所有耗时操作(数据库、文件 I/O、网络请求)统一使用 async/await 处理
  5. 资源释放:在组件 aboutToDisappear() 生命周期钩子中及时释放系统资源,防止内存泄漏

9.2 常见错误与解决方案

常见问题快速排查表:

问题类型 排查方向 参考方法
应用崩溃 查看 hilog 错误日志 hilog.error(TAG, "...", e.message)
状态丢失 检查 AppStorage 键名拼写 统一使用常量管理键名
动画不流畅 避免在 animateTo 回调中执行 I/O 动画与数据操作分离

提示: 建议在 DevEco Studio 中开启 ArkTS Lint 静态检查,大部分编译期问题可在开发阶段发现。

总结

本文详细讲解了 NFC Kit 与碰一碰功能的设计与实现:

  • NFC 技术栈@kit.ConnectedTagKit 的 Tag 发现、NDEF 读写 API
  • TapSharePage 交互:三层波纹脉冲动画 + 入场动画 + 分享卡片
  • NDEF 数据格式:TNF + Type + ID + Payload 的协议结构
  • 自定义分享协议lulu://share? 格式 + 解析器
  • 权限与配置:module.json5 权限声明 + NFC 状态检查
  • 安全设计:前台分发策略、时间戳防重放、数据校验
  • 多通道策略:NFC / QR / 超级终端 / 剪贴板四通道互补

下一篇将深入 pasteboard 剪贴板分享实现


📌 收藏提示:如果您觉得本系列对您有帮助,欢迎点赞 + 关注,也欢迎在评论区交流鸿蒙 NFC 开发的问题!


相关资源:

Logo

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

更多推荐