最近整理接手项目代码,其中有用到 NFC 功能,今天抽了个时间了解了一下,简单使用应该不难,高级扩展我就不知道喽,下面小结一番!
知识了解
网上看到一篇博客介绍 NFC 功能的,分了理论篇和实践篇,理论篇就是大致了解一下 NFC 这个功能,我看了觉得有了挺详细的概念,但是他那篇文章写的有点乱,后面看了下官方文档,发现官方文档也不错,而且有中文版,其实也是该先看官方文档:
https://developer.android.google.cn/guide/topics/connectivity/nfc
下面是参考的那篇博客:
东西太多了,其实我们只要掌握如何使用就可以的,下面讲解,有两种方式。
前台使用
1、配置权限
配置AndroidMenifest.xml:
<!--NFC权限-->
<uses-permission android:name="android.permission.NFC" />
<!-- 要求当前设备必须要有NFC芯片 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />
2、获取 NfcAdapter 对象
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate();
mNfcAdapter= NfcAdapter.getDefaultAdapter(this);//设备的NfcAdapter对象
if(mNfcAdapter==null){//判断设备是否支持NFC功能
Toast.makeText(this,"设备不支持NFC功能!",Toast.LENGTH_SHORT);
finish();
return;
}
if (!mNfcAdapter.isEnabled()){//判断设备NFC功能是否打开
Toast.makeText(this,"请到系统设置中打开NFC功能!",Toast.LENGTH_SHORT);
finish();
return;
}
mPendingIntent=PendingIntent.getActivity(this,0,new Intent(this,getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),0);//创建PendingIntent对象,当检测到一个Tag标签就会执行此Intent,设置 SINGLE_TOP 标志,只打开当前活动
}
这里还是在 onCreate 中写吧!
3、开启/关闭前台发布系统
//页面获取焦点
@Override
protected void onResume() {
super.onResume();
if (mNfcAdapter!=null){ mNfcAdapter.enableForegroundDispatch(this,mPendingIntent,null,null);//打开前台发布系统,使页面优于其它nfc处理.当检测到一个Tag标签就会执行mPendingItent
}
}
//页面失去焦点
@Override
protected void onPause() {
super.onPause();
if(mNfcAdapter!=null){
mNfcAdapter.disableForegroundDispatch(this);//关闭前台发布系统
}
}
只在当前页面显示的时候使用 NFC,是很合理的。
4、获取 TAG 对象
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mTag=intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//获取到Tag标签对象
String[] techList=mTag.getTechList();
System.out.println("标签支持的tachnology类型:");
for (String tech:techList){
System.out.println(tech);
}
}
因为设置的 SINGLE_TOP 标志,所以在 onNewIntent 中获取 TAG,类型就是 TAG。
解析 TAG 数据
这里要看我们支持的技术标准了,看下面图:
Class | Description |
---|---|
TagTechnology | 这个接口是下面所有tag technology类必须实现的。 |
NfcA | 支持ISO 14443-3A 标准的操作。Provides access to NFC-A (ISO 14443-3A) properties and I/O operations. |
NfcB | Provides access to NFC-B (ISO 14443-3B) properties and I/O operations. |
NfcF | Provides access to NFC-F (JIS 6319-4) properties and I/O operations. |
NfcV | Provides access to NFC-V (ISO 15693) properties and I/O operations. |
IsoDep | Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations. |
Ndef | 提供对那些被格式化为NDEF的tag的数据的访问和其他操作。 Provides access to NDEF data and operations on NFC tags that have been formatted as NDEF. |
NdefFormatable | 对那些可以被格式化成NDEF格式的tag提供一个格式化的操作 |
MifareClassic | 如果android设备支持MIFARE,提供对MIFARE Classic目标的属性和I/O操作。 |
MifareUltralight | 如果android设备支持MIFARE,提供对MIFARE Ultralight目标的属性和I/O操作。 |
可以通过 Tag 对象的 getTechList() 取到标签的技术类型,下面写一个 NDEF 格式读取:
//读取Ndef标签中数据
private void readNdef(){
if (mTag==null){
Toast.makeText(this,"不能识别的标签类型!",Toast.LENGTH_SHORT);
finish();
return;
}
Ndef ndef=Ndef.get(mTag);//获取ndef对象
try {
ndef.connect();//连接
NdefMessage ndefMessage=ndef.getNdefMessage();//获取NdefMessage对象
if (ndefMessage!=null) readEdt.setText(parseTextRecord(ndefMessage.getRecords()[0]));
Toast.makeText(this,"数据读取成功!",Toast.LENGTH_SHORT);
} catch (IOException e) {
e.printStackTrace();
} catch (FormatException e) {
e.printStackTrace();
}finally {
try {
ndef.close();//关闭链接
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 解析NDEF文本数据,从第三个字节开始,后面的文本数据
* @param ndefRecord
* @return
*/
public static String parseTextRecord(NdefRecord ndefRecord) {
/**
* 判断数据是否为NDEF格式
*/
//判断TNF
if (ndefRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
return null;
}
//判断可变的长度的类型
if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
return null;
}
try {
//获得字节数组,然后进行分析
byte[] payload = ndefRecord.getPayload();
//下面开始NDEF文本数据第一个字节,状态字节
//判断文本是基于UTF-8还是UTF-16的,取第一个字节"位与"上16进制的80,16进制的80也就是最高位是1,
//其他位都是0,所以进行"位与"运算后就会保留最高位
String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";
//3f最高两位是0,第六位是1,所以进行"位与"运算后获得第六位
int languageCodeLength = payload[0] & 0x3f;
//下面开始NDEF文本数据第二个字节,语言编码
//获得语言编码
String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
//下面开始NDEF文本数据后面的字节,解析出文本
String textRecord = new String(payload, languageCodeLength + 1,
payload.length - languageCodeLength - 1, textEncoding);
return textRecord;
} catch (Exception e) {
throw new IllegalArgumentException();
}
}
如果想要详细使用,可以看看官方文档,到这应该就获取到数据了。
Intent 形式使用
Intent 形式就是通过 intent-filter 来匹配 NFC 的 intent 实现获取数据的,流程和上面类似:
1、权限配置
同上
2、AndroidManifest.xml相关配置
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
这里就是三个 NCF 相关的 intent-filter,并指定我们的 technologies 列表。
3、配置 technologies 列表
<resources>
<!-- 可以处理所有Android支持的NFC类型 -->
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.NfcB</tech>
<tech>android.nfc.tech.NfcF</tech>
<tech>android.nfc.tech.NfcV</tech>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
</resources>
因为我们的 intent-filter 里面用到了 TECH_DISCOVERED,它要求我们在资源文件里指定要支持technologies 列表。
4、获取 NfcAdapter 对象
// 1.初始化NFC适配器
private void initNfc() {
// 获取系统默认的NFC适配器
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
tv_nfc_result.setText("当前手机不支持NFC");
} else if (!mNfcAdapter.isEnabled()) {
tv_nfc_result.setText("请先在系统设置中启用NFC功能");
} else {
tv_nfc_result.setText("当前手机支持NFC");
}
}
和上面一样在 onCreate 方法里获得 NfcAdapter 对象。
5、检查 NFC Intent 启动
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
...
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
Parcelable[] rawMessages =
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMessages != null) {
NdefMessage[] messages = new NdefMessage[rawMessages.length];
for (int i = 0; i < rawMessages.length; i++) {
messages[i] = (NdefMessage) rawMessages[i];
}
// Process the messages array.
...
}
}
}
Intent 可以包含以下 extra,具体取决于扫描到的标签:
EXTRA_TAG
(必需):一个Tag
对象,表示扫描到的标签。EXTRA_NDEF_MESSAGES
(可选):从标签中解析出的一组 NDEF 消息。此 extra 对于ACTION_NDEF_DISCOVERED
Intent 而言是必需的。EXTRA_ID
(可选):标签的低级别 ID。
这里拿的官网的代码,通过 intent-filter 打开活动可能在 onCreate 中,也可能在 onNewIntent 中,同时需要对 action 进行一次判断。
6、获取 TAG 对象
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
这里可以在 Intent 中,拿到 TAG 对象。
7、解析 TAG 数据
这里换一个,写一下 MifareClassic 类的。
// 读取小区门禁卡信息
public String readGuardCard(Tag tag) {
MifareClassic classic = MifareClassic.get(tag);
for (String tech : tag.getTechList()) {
Log.d(TAG, "当前设备支持" + tech); //显示设备支持技术
}
String info;
try {
classic.connect(); // 连接卡片数据
int type = classic.getType(); //获取TAG的类型
String typeDesc;
if (type == MifareClassic.TYPE_CLASSIC) {
typeDesc = "传统类型";
} else if (type == MifareClassic.TYPE_PLUS) {
typeDesc = "增强类型";
} else if (type == MifareClassic.TYPE_PRO) {
typeDesc = "专业类型";
} else {
typeDesc = "未知类型";
}
info = String.format("\t卡片类型:%s\n\t扇区数量:%d\n\t分块个数:%d\n\t存储空间:%d字节",
typeDesc, classic.getSectorCount(), classic.getBlockCount(), classic.getSize());
} catch (Exception e) {
e.printStackTrace();
info = e.getMessage();
} finally { // 无论是否发生异常,都要释放资源
try {
classic.close(); // 释放卡片数据
} catch (Exception e) {
e.printStackTrace();
info = e.getMessage();
}
}
return info;
}
public static String ByteArrayToHexString(byte[] bytesId) {
int i, j, in;
String[] hex = {
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"
};
String output = "";
for (j = 0; j < bytesId.length; ++j) {
in = bytesId[j] & 0xff;
i = (in >> 4) & 0x0f;
output += hex[i];
i = in & 0x0f;
output += hex[i];
}
return output;
}
下面是MifareClassic类的方法说明:
- get:从Tag对象中获取卡片对象的信息。该方法为静态方法。
- connect:链接卡片数据。
- close:释放卡片数据。
- getType:获取卡片的类型。TYPE_CLASSIC表示传统类型,TYPE_PLUS表示增强类型,
- TYPE_PRO表示专业类型。
- getSectorCount:获取卡片的扇区数量。
- getBlockCount:获取卡片的分块个数。
- getSize:获取卡片的存储空间大小,单位字节。
结语
这里还是推荐官方文档吧,内容全一点,上面两种方式应该都可以获取到数据,当然只是简单的使用了,复杂的还得自己研究。
end