HarmonyOS APP<<古今职鉴定>>开源教程第14篇:碰一碰分享:NFC 近场通信

本篇学习 NFC 近场通信,实现碰一碰分享官职名片

图:古今职鉴开源教程封面。本篇围绕「碰一碰分享:NFC 近场通信」展开。

学习目标

完成本篇后,你将能够:

  • ✅ 理解 NFC 技术原理
  • ✅ 配置 NFC 权限
  • ✅ 读写 NFC 标签
  • ✅ 实现设备间数据传输

预计学习时间

约 90 分钟


实战一:理解 NFC 技术

第一步:什么是 NFC

NFC(Near Field Communication)近场通信:

  • 工作距离:约 10cm 以内
  • 通信频率:13.56MHz
  • 数据传输速率:106-424 Kbps

第二步:NFC 工作模式

模式 说明 应用场景
读卡器模式 读取 NFC 标签 门禁卡、公交卡
卡模拟模式 模拟 NFC 卡 手机支付
P2P 模式 设备间通信 碰一碰传输

第三步:鸿蒙 NFC 能力

  • 读取/写入 NFC 标签
  • 设备间数据传输
  • 碰一碰分享

实战二:配置 NFC 权限

第一步:声明权限

module.json5 中添加:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.NFC_TAG",
        "reason": "$string:nfc_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

第二步:添加权限说明

resources/base/element/string.json 中:

json 复制代码
{
  "string": [
    {
      "name": "nfc_permission_reason",
      "value": "用于读取NFC标签和碰一碰分享功能"
    }
  ]
}

第三步:检查 NFC 状态

typescript 复制代码
import { nfcController } from '@kit.ConnectivityKit';

function checkNfcState(): boolean {
  try {
    const state = nfcController.getNfcState();
    return state === nfcController.NfcState.STATE_ON;
  } catch (error) {
    console.error('检查NFC状态失败:', error);
    return false;
  }
}

实战三:读取 NFC 标签

第一步:导入模块

typescript 复制代码
import { tag } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';

第二步:监听标签发现

typescript 复制代码
@Entry
@Component
struct Lesson14Page {
  @State tagInfo: string = '等待扫描NFC标签...';
  @State nfcEnabled: boolean = false;

  aboutToAppear() {
    this.checkNfcState();
  }

  checkNfcState() {
    try {
      const state = nfcController.getNfcState();
      this.nfcEnabled = state === nfcController.NfcState.STATE_ON;
    } catch {
      this.nfcEnabled = false;
    }
  }
}

第三步:处理标签数据

typescript 复制代码
// 当系统检测到NFC标签时,会通过 Want 传递标签信息
// 在 EntryAbility 中处理

// EntryAbility.ets
onCreate(want: Want) {
  if (want.action === 'ohos.nfc.tag.action.TAG_FOUND') {
    // 获取标签信息
    const tagInfo = tag.getTagInfo(want);
    if (tagInfo) {
      this.handleNfcTag(tagInfo);
    }
  }
}

handleNfcTag(tagInfo: tag.TagInfo) {
  // 获取标签 UID
  const uid = tagInfo.uid;
  console.log('标签UID:', this.bytesToHex(uid));
  
  // 获取支持的技术类型
  const techList = tagInfo.technology;
  console.log('支持的技术:', techList);
}

bytesToHex(bytes: number[]): string {
  return bytes.map(b => b.toString(16).padStart(2, '0')).join(':');
}

实战四:写入 NFC 标签

第一步:创建 NDEF 消息

typescript 复制代码
import { ndef } from '@kit.ConnectivityKit';

function createNdefMessage(text: string): ndef.NdefMessage {
  // 创建文本记录
  const textRecord = ndef.createTextRecord(text, 'zh');
  
  // 创建 NDEF 消息
  const ndefMessage = ndef.createNdefMessage([textRecord]);
  
  return ndefMessage;
}

第二步:写入标签

typescript 复制代码
async function writeNdefTag(tagInfo: tag.TagInfo, message: ndef.NdefMessage): Promise<boolean> {
  try {
    // 获取 NDEF 标签对象
    const ndefTag = ndef.getNdef(tagInfo);
    if (!ndefTag) {
      console.error('不是NDEF标签');
      return false;
    }
    
    // 连接标签
    await ndefTag.connect();
    
    // 检查是否可写
    if (!ndefTag.isNdefWritable()) {
      console.error('标签不可写');
      await ndefTag.close();
      return false;
    }
    
    // 写入数据
    await ndefTag.writeNdef(message);
    
    // 关闭连接
    await ndefTag.close();
    
    return true;
  } catch (error) {
    console.error('写入失败:', error);
    return false;
  }
}

第三步:读取标签内容

typescript 复制代码
async function readNdefTag(tagInfo: tag.TagInfo): Promise<string | null> {
  try {
    const ndefTag = ndef.getNdef(tagInfo);
    if (!ndefTag) return null;
    
    await ndefTag.connect();
    
    // 读取 NDEF 消息
    const ndefMessage = await ndefTag.readNdef();
    
    await ndefTag.close();
    
    if (ndefMessage) {
      // 解析记录
      const records = ndefMessage.getNdefRecords();
      if (records.length > 0) {
        // 获取第一条记录的内容
        const payload = records[0].payload;
        // 解析文本(跳过语言代码)
        const langCodeLen = payload[0];
        const text = String.fromCharCode(...payload.slice(1 + langCodeLen));
        return text;
      }
    }
    
    return null;
  } catch (error) {
    console.error('读取失败:', error);
    return null;
  }
}

实战五:碰一碰分享官职名片

第一步:定义名片数据结构

typescript 复制代码
interface OfficialCard {
  name: string;        // 官职名称
  dynasty: string;     // 朝代
  rank: string;        // 品级
  description: string; // 职责描述
}

第二步:序列化名片数据

typescript 复制代码
function serializeCard(card: OfficialCard): string {
  return JSON.stringify(card);
}

function deserializeCard(data: string): OfficialCard | null {
  try {
    return JSON.parse(data) as OfficialCard;
  } catch {
    return null;
  }
}

第三步:创建完整页面

typescript 复制代码
import { nfcController } from '@kit.ConnectivityKit';

interface OfficialCard {
  name: string;
  dynasty: string;
  rank: string;
  description: string;
}

@Entry
@Component
struct Lesson14Page {
  @State nfcEnabled: boolean = false;
  @State statusMessage: string = '检查NFC状态...';
  @State selectedCard: OfficialCard | null = null;

  private officialCards: OfficialCard[] = [
    {
      name: '丞相',
      dynasty: '秦',
      rank: '正一品',
      description: '百官之长,辅佐皇帝处理政务'
    },
    {
      name: '太尉',
      dynasty: '秦',
      rank: '正一品',
      description: '掌管全国军事'
    },
    {
      name: '御史大夫',
      dynasty: '秦',
      rank: '从一品',
      description: '监察百官,掌管图籍'
    }
  ];

  aboutToAppear() {
    this.checkNfcState();
    this.selectedCard = this.officialCards[0];
  }

  checkNfcState() {
    try {
      const state = nfcController.getNfcState();
      this.nfcEnabled = state === nfcController.NfcState.STATE_ON;
      this.statusMessage = this.nfcEnabled ? 'NFC已开启,可以碰一碰分享' : 'NFC未开启';
    } catch {
      this.nfcEnabled = false;
      this.statusMessage = '设备不支持NFC';
    }
  }

  build() {
    Column() {
      // 头部
      Row() {
        Text('碰一碰分享')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1e293b')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      Scroll() {
        Column({ space: 20 }) {
          // NFC 状态
          Row({ space: 8 }) {
            Circle()
              .width(12)
              .height(12)
              .fill(this.nfcEnabled ? '#22c55e' : '#ef4444')

            Text(this.statusMessage)
              .fontSize(14)
              .fontColor('#64748b')
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)

          // 选择名片
          Column({ space: 12 }) {
            Text('选择要分享的官职名片')
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .fontColor('#1e293b')

            ForEach(this.officialCards, (card: OfficialCard, index: number) => {
              Row() {
                Column({ space: 4 }) {
                  Text(card.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                    .fontColor('#1e293b')

                  Text(`${card.dynasty} · ${card.rank}`)
                    .fontSize(13)
                    .fontColor('#64748b')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                if (this.selectedCard?.name === card.name) {
                  Image($r('app.media.ic_check_circle'))
                    .width(24)
                    .height(24)
                    .fillColor('#c41e3a')
                }
              }
              .width('100%')
              .padding(12)
              .backgroundColor(this.selectedCard?.name === card.name ? '#fef2f2' : '#f8f8f8')
              .borderRadius(8)
              .onClick(() => {
                this.selectedCard = card;
              })
            })
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)
          .alignItems(HorizontalAlign.Start)

          // 名片预览
          if (this.selectedCard) {
            Column({ space: 12 }) {
              Text('名片预览')
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#1e293b')

              Column({ space: 8 }) {
                Text(this.selectedCard.name)
                  .fontSize(24)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#c41e3a')

                Row({ space: 8 }) {
                  Text(this.selectedCard.dynasty)
                    .fontSize(12)
                    .fontColor(Color.White)
                    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                    .backgroundColor('#c41e3a')
                    .borderRadius(4)

                  Text(this.selectedCard.rank)
                    .fontSize(12)
                    .fontColor('#c41e3a')
                    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                    .backgroundColor('#fef2f2')
                    .borderRadius(4)
                }

                Text(this.selectedCard.description)
                  .fontSize(14)
                  .fontColor('#64748b')
                  .margin({ top: 8 })
              }
              .width('100%')
              .padding(20)
              .backgroundColor('#fffbeb')
              .borderRadius(12)
              .alignItems(HorizontalAlign.Center)
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .alignItems(HorizontalAlign.Start)
          }

          // 使用说明
          Column({ space: 8 }) {
            Text('使用方法')
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .fontColor('#1e293b')

            Text('1. 确保两台设备都开启了NFC')
              .fontSize(13)
              .fontColor('#64748b')

            Text('2. 选择要分享的官职名片')
              .fontSize(13)
              .fontColor('#64748b')

            Text('3. 将两台设备背靠背轻触')
              .fontSize(13)
              .fontColor('#64748b')

            Text('4. 听到提示音后完成传输')
              .fontSize(13)
              .fontColor('#64748b')
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)
          .alignItems(HorizontalAlign.Start)
        }
        .padding(16)
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f8f6f5')
  }
}

@Builder
export function Lesson14PageBuilder() {
  Lesson14Page()
}

第四步:运行验证

bash 复制代码
hvigorw assembleHap --no-daemon

本课小结

核心知识点

知识点 说明
NFC 模式 读卡器、卡模拟、P2P
nfcController NFC 状态管理
tag 模块 标签读写操作
ndef 模块 NDEF 消息处理
NdefMessage NDEF 数据格式

NFC 开发流程

  1. 声明权限
  2. 检查 NFC 状态
  3. 监听标签发现
  4. 读取/写入数据
  5. 关闭连接

课后练习

练习1:实现名片接收

接收其他设备发送的官职名片并显示。

练习2:添加历史记录

记录分享和接收的名片历史。


下一课预告

第15课我们将学习小艺智能体,包括:

  • Intents Kit 意图框架
  • 语音交互集成
  • 智能问答能力

项目开源地址

https://gitcode.com/daleishen/gujinzhijian

相关推荐
鸿蒙开发11 小时前
鸿蒙(HarmonyOS NEXT)表单校验别再手撸正则了 —— 我写了个 ArkTS 版 zod
harmonyos
TrisighT11 小时前
ArkTS 的 @BuilderParam 你八成只用了皮毛——那个尾随闭包写法差点被我当 bug 删了
harmonyos·arkts·arkui
ONEDAY1 天前
HarmonyOS 多 Product 构建实践:一套代码生成多个产物
harmonyos
TT_Close1 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
TrisighT1 天前
ArkTS 列表滚动时为什么会闪现旧数据?我扒了 LazyForEach 的复用逻辑
harmonyos·arkts·arkui
MonkeyKing1 天前
鸿蒙ArkTS深度剖析:ArkTS与TS/JS核心差异、静态强类型实战优势
typescript·harmonyos
TrisighT1 天前
Electron鸿蒙PC上写日志文件,我被权限和路径坑了两次
electron·harmonyos
TrisighT2 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
花椒技术5 天前
HJPusher / HJPlayer SDK 实践:我们为什么把直播推播链路拆成一套可复用能力
设计模式·harmonyos·直播
一维Ace5 天前
HarmonyOS ArkTS 按钮组件全解:Button、Toggle 状态交互实战
harmonyos