使用AndroidStdio开发APP套壳H5

顾名思义,开发一个套壳H5的安卓APP,需要Android实现基础功能包括:拍照、NFC等。
使用了WebView技术实现JavaScript和java的互相调用。

  1. 创建项目


    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>
  1. 修改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;
            }
        }
    }
}
  1. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容