顾名思义,开发一个套壳H5的安卓APP,需要Android实现基础功能包括:拍照、NFC等。
使用了WebView技术实现JavaScript和java的互相调用。
-
创建项目
image.png
FAE25D63-F015-4a97-8040-8A1423065E5F.png
2.等待gradle下载资源(可能很慢)
3.切换到Andriod视图
image.png
4.修改/manifests/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<!-- 访问网络的权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.NFC" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.EmptyView"
android:usesCleartextTraffic="true"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- 添加NFC意图过滤器 -->
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
</intent-filter>
<!-- 指定支持的NFC技术 -->
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>
- 修改res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 展示一个 WebView -->
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
6.修改java/com/example/emptyview/MainActivity.java
package com.example.emptyview;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.Ndef;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Base64;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.util.Arrays;
public class MainActivity extends AppCompatActivity {
private WebView webView;
private static final int REQUEST_CAMERA = 1;
private static final int REQUEST_FILE_CHOOSER = 2;
private static final int REQUEST_LOCATION = 3;
private static final int REQUEST_NFC = 4;
private ValueCallback<Uri[]> filePathCallback;
private LocationManager locationManager;
private NfcAdapter nfcAdapter;
private PendingIntent pendingIntent;
private IntentFilter[] intentFiltersArray;
private String[][] techListsArray;
private boolean isTakingPhoto = false;
private SwipeRefreshLayout swipeRefreshLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 接管返回键处理
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
WebView webView = findViewById(R.id.webview);
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
this.initWebView();
this.initNFC();
this.initLocation();
}
/* 创建 WebView 实例 */
@SuppressLint("SetJavaScriptEnabled")
private void initWebView() {
// 创建 WebView 实例并通过 id 绑定我们刚在布局中创建的 WebView 标签
// 这里的 R.id.webview 就是 activity_main.xml 中的 WebView 标签的 id
webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
// 设置 WebView 允许执行 JavaScript 脚本
webSettings.setJavaScriptEnabled(true);
// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 启用 DOM Storage API 的支持
webSettings.setDomStorageEnabled(true);
// 确保跳转到另一个网页时仍然在当前 WebView 中显示
// 而不是调用浏览器打开
webView.setWebViewClient(new WebViewClient());
// 添加WebView调试
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
// 添加JS接口
webView.addJavascriptInterface(new WebAppInterface(), "Android");
// 加载指定网页
String url = "http://192.168.100.46:5173/web/#/pages/tabBar/API";
webView.loadUrl(url);
// 由于设置了弹窗检验调用结果,所以需要支持js对话框
// webview只是载体,内容的渲染需要使用webviewChromClient类去实现
// 通过设置WebChromeClient对象处理JavaScript的对话框
//设置响应js 的Alert()函数
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
b.setCancelable(false);
b.create().show();
return true;
}
});
// 初始化下拉刷新
swipeRefreshLayout = findViewById(R.id.swipeRefresh);
swipeRefreshLayout.setOnRefreshListener(() -> {
webView.reload();
Toast.makeText(MainActivity.this, "正在刷新页面...", Toast.LENGTH_SHORT).show();
});
// 设置下拉刷新的颜色
swipeRefreshLayout.setColorSchemeResources(
android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light
);
// 在 WebViewClient 中添加
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载完成后,停止刷新动画
swipeRefreshLayout.setRefreshing(false);
Log.i("WebViewDebug", "Page loaded: " + url);
}
});
}
private void initLocation() {
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
}
private void initNFC() {
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
Log.i("NFC_DEBUG", "初始化NFC适配器: " + (nfcAdapter != null ? "成功" : "设备不支持NFC"));
// 创建PendingIntent
Intent intent = new Intent(this, getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Log.i("NFC_DEBUG", "创建PendingIntent成功");
// 创建多个意图过滤器
IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
IntentFilter ndefDetected = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
IntentFilter techDetected = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
try {
ndefDetected.addDataType("*/*");
Log.i("NFC_DEBUG", "设置NFC意图过滤器成功");
} catch (IntentFilter.MalformedMimeTypeException e) {
Log.e("NFC_DEBUG", "设置NFC意图过滤器失败", e);
}
// 使用所有过滤器
intentFiltersArray = new IntentFilter[]{
tagDetected,
ndefDetected,
techDetected
};
Log.i("NFC_DEBUG", "已设置意图过滤器: TAG, NDEF, TECH");
}
public class WebAppInterface {
@JavascriptInterface
public void takePhoto() {
if (checkPermission(android.Manifest.permission.CAMERA, REQUEST_CAMERA)) {
startCamera();
}
}
@JavascriptInterface
public void getLocation() {
if (checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, REQUEST_LOCATION)) {
startLocationUpdates();
}
}
@JavascriptInterface
public void readNFC() {
if (nfcAdapter == null) {
Toast.makeText(MainActivity.this, "设备不支持NFC", Toast.LENGTH_SHORT).show();
return;
}
if (!nfcAdapter.isEnabled()) {
Toast.makeText(MainActivity.this, "请开启NFC功能", Toast.LENGTH_SHORT).show();
return;
}
// 启动NFC读取
startNFCReading();
}
}
private boolean checkPermission(String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
return false;
}
return true;
}
private void startCamera() {
isTakingPhoto = true;
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(takePictureIntent, REQUEST_CAMERA);
}
}
private void openFileChooser() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_FILE_CHOOSER);
}
private void startLocationUpdates() {
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
1000, 1, new LocationListener() {
@Override
public void onLocationChanged(Location location) {
@SuppressLint("DefaultLocale") String locationData = String.format("经度:%f,纬度:%f",
location.getLongitude(), location.getLatitude());
// 将位置信息传递给Web页面
webView.evaluateJavascript(
"javascript:onLocationUpdate('" + locationData + "')",
null);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
});
} catch (SecurityException e) {
e.printStackTrace();
}
}
private void startNFCReading() {
if (nfcAdapter != null) {
try {
Log.i("NFC_DEBUG", "准备启动NFC前台调度系统...");
Log.i("NFC_DEBUG", "PendingIntent: " + (pendingIntent != null));
Log.i("NFC_DEBUG", "意图过滤器数量: " + intentFiltersArray.length);
nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
Log.i("NFC_DEBUG", "启动NFC前台调度系统成功");
// 提示用户
Toast.makeText(this, "请将NFC标签靠近设备背面", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("NFC_DEBUG", "启动NFC前台调度系统失败", e);
e.printStackTrace();
}
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.i("NFC_DEBUG", "收到新的Intent: " + intent.getAction());
try {
// 处理NFC标签
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction()) ||
NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction()) ||
NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag != null) {
try {
// 读取并打印所有可用的技术
String[] techList = tag.getTechList();
Log.i("NFC_DEBUG", "支持的技术: " + Arrays.toString(techList));
// 读取标签ID
String hexId = "";
byte[] tagId = tag.getId();
if (tagId != null) {
hexId = bytesToHexString(tagId);
Log.i("NFC_DEBUG", "NFC标签ID(HEX): " + hexId);
try {
BigInteger decimalId = new BigInteger(hexId, 16);
Log.i("NFC_DEBUG", "NFC标签ID(DEC): " + decimalId);
} catch (NumberFormatException e) {
Log.e("NFC_DEBUG", "转换十进制失败: " + e.getMessage());
}
} else {
Log.w("NFC_DEBUG", "无法获取标签ID");
}
// 尝试读取NDEF数据
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
try {
ndef.connect();
NdefMessage ndefMessage = ndef.getNdefMessage();
if (ndefMessage != null) {
for (NdefRecord record : ndefMessage.getRecords()) {
try {
Log.i("NFC_DEBUG", "NDEF记录类型: " + new String(record.getType()));
Log.i("NFC_DEBUG", "NDEF记录内容: " + new String(record.getPayload()));
} catch (Exception e) {
Log.e("NFC_DEBUG", "解析NDEF记录失败: " + e.getMessage());
}
}
} else {
Log.i("NFC_DEBUG", "没有NDEF消息");
}
} catch (Exception e) {
Log.e("NFC_DEBUG", "读取NDEF数据失败: " + e.getMessage());
} finally {
try {
ndef.close();
} catch (Exception e) {
Log.e("NFC_DEBUG", "关闭NDEF连接失败: " + e.getMessage());
}
}
}
// 尝试读取Mifare数据
MifareClassic mifareClassic = MifareClassic.get(tag);
if (mifareClassic != null) {
try {
mifareClassic.connect();
Log.i("NFC_DEBUG", "Mifare类型: " + mifareClassic.getType());
Log.i("NFC_DEBUG", "Mifare扇区数: " + mifareClassic.getSectorCount());
Log.i("NFC_DEBUG", "Mifare块数: " + mifareClassic.getBlockCount());
} catch (Exception e) {
Log.e("NFC_DEBUG", "读取Mifare数据失败: " + e.getMessage());
} finally {
try {
mifareClassic.close();
} catch (Exception e) {
Log.e("NFC_DEBUG", "关闭Mifare连接失败: " + e.getMessage());
}
}
}
// 将NFC数据传递给网页
final String finalHexId = hexId;
String nfcData = String.format("NFC标签信息:\nID: %s\n类型: %s", finalHexId, Arrays.toString(techList));
Log.i("NFC_DEBUG", "nfcData: " + nfcData);
runOnUiThread(() -> {
try {
String funcStr = "javascript:onNFCRead('" + finalHexId + "')";
webView.evaluateJavascript(funcStr, value -> {
//此处为 js 返回的结果
Log.i("NFC_DEBUG", "JavaScript回调结果: " + value);
});
} catch (Exception e) {
Log.e("NFC_DEBUG", "JavaScript调用失败: " + e.getMessage());
}
});
} catch (Exception e) {
Log.e("NFC_DEBUG", "处理NFC标签数据时发生错误: " + e.getMessage());
e.printStackTrace();
}
} else {
Log.w("NFC_DEBUG", "检测到NFC标签但无法读取标签信息");
}
}
} catch (Exception e) {
Log.e("NFC_DEBUG", "onNewIntent处理失败: " + e.getMessage());
e.printStackTrace();
}
}
@Override
protected void onPause() {
super.onPause();
Log.i("ACTIVITY_DEBUG", "Activity onPause - " + (isTakingPhoto ? "正在拍照" : "其他原因"));
if (nfcAdapter != null && !isTakingPhoto) {
try {
nfcAdapter.disableForegroundDispatch(this);
Log.i("NFC_DEBUG", "onPause: 临时禁用NFC前台调度");
} catch (Exception e) {
Log.e("NFC_DEBUG", "onPause: 禁用NFC前台调度失败", e);
}
}
}
@Override
protected void onResume() {
super.onResume();
Log.i("ACTIVITY_DEBUG", "Activity onResume - 恢复活动状态");
if (nfcAdapter != null) {
try {
nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
Log.i("NFC_DEBUG", "onResume: 重新启用NFC前台调度");
} catch (Exception e) {
Log.e("NFC_DEBUG", "onResume: 启用NFC前台调度失败", e);
}
}
}
// 辅助方法:将字节数组转换为十六进制字符串
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString().toUpperCase();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CAMERA) {
isTakingPhoto = false;
}
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_CAMERA:
// 处理拍照结果
Bundle extras = data.getExtras();
if (extras != null) {
Bitmap photo = (Bitmap) extras.get("data");
if (photo != null) {
// 将图片转换为Base64字符串
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
photo.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
String base64Image = Base64.encodeToString(byteArray, Base64.NO_WRAP);
// 添加日志
Log.i("PhotoDebug", "Photo taken, converting to base64 " + base64Image);
// 调用JavaScript
runOnUiThread(() -> {
try {
String funcStr = "javascript:onPhotoTaken('" + base64Image + "')";
webView.evaluateJavascript(funcStr, value -> {
//此处为 js 返回的结果
Log.i("PhotoDebug", "JavaScript回调结果: " + value);
});
} catch (Exception e) {
Log.e("PhotoDebug", "JavaScript调用失败: " + e.getMessage());
}
});
}
}
break;
case REQUEST_FILE_CHOOSER:
// 处理文件选择结果
if (filePathCallback != null) {
Uri[] results = new Uri[]{data.getData()};
filePathCallback.onReceiveValue(results);
filePathCallback = null;
}
break;
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
switch (requestCode) {
case REQUEST_CAMERA:
startCamera();
break;
case REQUEST_LOCATION:
startLocationUpdates();
break;
}
}
}
}
- H5测试代码(uniapp,vue3)
# view 代码
<view class="uni-title">
<img :src="photoSrc" v-if="photoSrc" style="max-width: 100%; height: auto;" />
</view>
<view>
<button onclick="Android.takePhoto()">拍照</button>
</view>
# data() 代码
photoSrc: ""
# method: 代码
// Android拍照回调函数,函数名不可随意更改需要和Android里面的指定回调函数名一致
onPhotoTaken(photo){
console.log("photo " + photo)
this.photoSrc = 'data:image/jpeg;base64,' + photo;
return "js调用成功photo";
},
// Android识别NFC回调函数,函数名不可随意更改需要和Android里面的指定回调函数名一致
onNFCRead(nfcData) {
console.log("nfcData--------------------------" + nfcData)
alert("nfcData--------------------------" + nfcData)
return "js调用成功nfc";
},
# mounted 代码
// 将要给原生调用的方法挂载到 window 上面,方便原生直接调用
window.onNFCRead = this.onNFCRead;
window.onPhotoTaken = this.onPhotoTaken;
eab606ea-7271-40cd-9ecb-8847ec83aee9