0


【JoAPP】Android WebView与H5交互实现(JAVA+KOTLIN)

1、前言

   最近一个应急平台的项目移动端开发,原计划用UNI-APP实现,客户想着要集成语音、视频通话功能,基于经验判断需要买一套IM原生移动端框架去结合H5整合比较合适,没想到最后客户不想采购,而且语音视频通话功能也迟迟未能完全确认,H5部分所开发的业务功能已经实现,但原生端开发模式迟迟未定,紧急时刻,决定启动前几年一直使用的一组android原生APP+H5(WEB)实现移动端开发,随即找了前几年的原生框架代码,发现与新的版本已不兼容,索性重新梳理,整理一套新的代码,也决定对外开放给朋友们使用,暂时延续之前内部框架名称JoApp,目前只整理了android+h5代码,后续还会将IOS版整理出来。

    恰逢2024年第一天元旦,祝福各位朋友新年快乐!这个节假日老哥我最大收获就是这个框架中实现了人脸识别、人脸对比的API,满足各类应用系统手机APP中实现人脸识别、位置校验的需要,方便大家哪里即用。

本文涉及代码开发工具如下:

Android Studio Giraffe | 2022.3.1 Patch 3、VSCode

语言及管理:

Java Jdk(OpenJDK17)、Kotlin、Gradle-8.4

2、原生APP与H5交互的核心实现

   基于JS方法在在APP与WebView内的H5间进行调用实现,这里主要演示Kotilin的代码,如需要JAVA版,可以使用文心一言等智能工具进行转换。

   原生APP端核心原理代码如下(写在 MainActivity内):
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 隐藏状态栏和导航栏
        requestWindowFeature(Window.FEATURE_NO_TITLE)

        // 设置窗口全屏
        window.setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
        )

        // 获取 WebView 组件
        webview = findViewById<WebView>(R.id.web_view)

        // 获取并设置 Web 设置
        val settings = webview?.settings
        settings?.javaScriptEnabled = true   // 支持 JavaScript
        // 设置是否启用 DOM 存储
        // DOM 存储是一种在 Web 应用程序中存储数据的机制,它使用 JavaScript 对象和属性来存储和检索数据
        settings?.domStorageEnabled = true
        // 设置 WebView 是否启用内置缩放控件 ( 自选 非必要 )
        //settings.builtInZoomControls = true

        // 5.0 以上需要设置允许 http 和 https 混合加载
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            settings?.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        } else {
            // 5.0 以下不用考虑  http 和 https 混合加载 问题
            settings?.mixedContentMode = WebSettings.LOAD_NORMAL
        }

        // 设置页面自适应
        // Viewport 元标记是指在 HTML 页面中的 <meta> 标签 , 可以设置网页在移动端设备上的显示方式和缩放比例
        // 设置是否支持 Viewport 元标记的宽度
        settings?.useWideViewPort = true

        // 设置 WebView 是否使用宽视图端口模式
        // 宽视图端口模式下 , WebView 会将页面缩小到适应屏幕的宽度
        // 没有经过移动端适配的网页 , 不要启用该设置
        settings?.loadWithOverviewMode = true

        // 设置 WebView 是否可以获取焦点 ( 自选 非必要 )
        webview?.isFocusable = true
        // 设置 WebView 是否启用绘图缓存 位图缓存可加速绘图过程 ( 自选 非必要 )
        webview?.isDrawingCacheEnabled = true
        // 设置 WebView 中的滚动条样式 ( 自选 非必要 )
        // SCROLLBARS_INSIDE_OVERLAY - 在内容上覆盖滚动条 ( 默认 )
        webview?.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY

        // WebViewClient 是一个用于处理 WebView 页面加载事件的类
        webview?.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                // 4.0 之后必须添加该设置
                // 只能加载 http:// 和 https:// 页面 , 不能加载其它协议链接
                if (url.startsWith("http://") || url.startsWith("https://")) {
                    view.loadUrl(url)
                    return true
                }
                return false
            }

            // SSL 证书校验出现异常
            override fun onReceivedSslError(
                view: WebView,
                handler: SslErrorHandler,
                error: SslError
            ) {
                when (error.primaryError) {
                    SslError.SSL_INVALID, SslError.SSL_UNTRUSTED -> {
                        handler.proceed()
                    }
                    else -> handler.cancel()
                }
            }

        }

        // WebChromeClient 是一个用于处理 WebView 界面交互事件的类
        webview?.webChromeClient =  MyWebChromeClient()
        // 加载网页
        webview?.loadUrl(WebUrl)

        // js调用安卓方法支持(第二个参数是js代码中调用APP中的交互桥类定义的名,需保持一致)
        webview?.addJavascriptInterface(JoAppObject(),"joApp")
        
        // 原生调用js中的方法(不带参数版) 
        // 这里joAppJs与H5 web端中定义的被原生调用JS类new的变量名一致,方便统一调用
        joAppJs("joAppJs.test")
        // 原生调用js中的方法(带参数版)
        joAppJs("joAppJs.testData","一只可爱的对号")

    }

    // 原生调用JS方法,方法名
    fun joAppJs(funName: String){
        JoDebug.show(this@MainActivity,  " - " + funName, Toast.LENGTH_LONG)
        if (Build.VERSION.SDK_INT< 18) {
          webview?.loadUrl("javascript:$funName()")

        } else {
            // 安卓调用js方法 4.4以上
            webview?.evaluateJavascript(
                "javascript:$funName()",
                object : ValueCallback<String> {
                    override fun onReceiveValue(res: String?) {
                        //此处为 js 返回的结果
                        //System.out.print(res)
                        //return res
                    }
                })
        }
    }
    // 原生调用JS方法,参数1:JS方法名、参数2:传给JS方法的参数(支持json字符串)
    fun joAppJs(funName: String, data: String){
        // 旧版android支持
        if (Build.VERSION.SDK_INT< 18) {
            if(data==null) {
                webview?.loadUrl("javascript:$funName()")
            }else{
                webview?.loadUrl("javascript:$funName('$data')")
            }
        } else {
            // 安卓调用js方法 4.4以上
            if(data==null) {
                webview?.evaluateJavascript(
                    "javascript:$funName()",
                    object : ValueCallback<String> {
                        override fun onReceiveValue(res: String?) {
                            //此处为 js 返回的结果
                            //System.out.print(res)
                            //return res
                        }
                    })
            }else{
                webview?.evaluateJavascript("javascript:$funName('$data')", object : ValueCallback<String> {
                    override fun onReceiveValue(res: String?) {
                        //此处为 js 返回的结果
                        //System.out.print(res)
                        //return res
                    }
                })
            }

        }
    }

    /*
    * JoApp 原生提供给H5可被JS调用的桥类库,真实的原生实现方法类库
      需要将与原生交互的各种API类写在这里,实现H5的方便调用
    * */
    inner  class JoAppObject {

        //测试jsAndroid调用
        @JavascriptInterface
        fun jsAndroid(msg: String) {
            //点击html的Button调用Android的Toast代码
            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, msg, Toast.LENGTH_LONG)
        }
    }
    嵌入的H5 WEB中配套代码如下:
...

<button type="button" onclick="clickAndroid()">无回传调用安卓方法</button>

...

 <script type="text/javascript">

     /*
    JoAppJs 安卓调用的JS方法库
    */
    class JoAppJs {
        //测试不带参数
        test () {
            alert("Android调用了JS代码")
            document.getElementById("showres").innerHTML = "Android调用了JS代码"
        }
        //测试不带参数
        testData (data) {
             alert("Android调用了JS代码" + data) 
             document.getElementById("showres").innerHTML = data
        }

        
    }

    //定义被APP原生调用的H5中JS类库变量名,方便统一调用
    const joAppJs = new JoAppJs()

    //测试调用原生APP
    function clickAndroid(){
        //用joapp.调用映射的对象    这里的androids是addJavascriptInterface()的第二个参数
        joApp.jsAndroid("我是JS,我调用了Android的方法")
    }
    
</script>

3、JoAPP已实现的交互API方法库

   在JoApp中已经实现了一些原生APP与WebView H5中js的交互方法,以下列出当前关键方法,后续会逐步新增在JoApp Git仓库中,也会在后续文章中逐个解析重点API实现原理。

    APP已实现的API包括:
  • 配置信息:joConfig
  • APP接收WEB中token:joToen
  • 向WEB发送APP中token:joTokenToWeb
  • 启动原生文件上传:joFile
  • 启动原生图片上传(浏览相册+拍照):joImage
  • 获取原生APP位置信息(经纬度):joLocation
  • APP接收位置有效性检测参照信息:joCheckLocation
  • APP接收人脸有效性检测参照信息:joCheckFace
  • 启动APP人脸及位置有效性对比功能:joFaceCompare
  • 启动APP设置界面(配置WEB网址):joSetting
    具体代码如下,请根据需要自行依据注释进行使用:

    //权限
    var permissions = arrayOf(
        Manifest.permission.READ_PHONE_STATE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS,
        Manifest.permission.ACCESS_NETWORK_STATE,
        Manifest.permission.ACCESS_WIFI_STATE,
        Manifest.permission.SYSTEM_ALERT_WINDOW,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.CHANGE_WIFI_STATE,
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS,
        Manifest.permission.CHANGE_NETWORK_STATE,
        Manifest.permission.GET_TASKS,
        Manifest.permission.VIBRATE,
        Manifest.permission.CAMERA,
    )
    private fun initPermission() {
        MPermissionUtils.requestPermissionsResult(
            this@MainActivity,
            1,
            permissions,
            object : MPermissionUtils.OnPermissionListener {
                override fun onPermissionGranted() {}
                override fun onPermissionDenied() {
                    MPermissionUtils.showTipsDialog(this@MainActivity)
                }
            })
    }

    // 加载完成后自动调取的js
    fun onLoagJs() {

        //joAppJs("joAppJs.test")
        //joAppJs("joAppJs.testData","我的神")

        //获取H5中包括接口地址在内的设置等信息,用于传递H5中的默认信息给原生app
        //改由web页面加载后向原生单向推送
        //joAppJs("joAppJs.config")

        //向web传入app缓存中的token
        //改由web页面加载后向原生推送
        //joAppJs("joAppJs.token");

    }

    // 调用JS方法, 方法名、参数(支持json字符串)
    fun joAppJs(funName: String){
        JoDebug.show(this@MainActivity,  " - " + funName, Toast.LENGTH_LONG)
        if (Build.VERSION.SDK_INT< 18) {
          webview?.loadUrl("javascript:$funName()")

        } else {
            // 安卓调用js方法 4.4以上
            webview?.evaluateJavascript(
                "javascript:$funName()",
                object : ValueCallback<String> {
                    override fun onReceiveValue(res: String?) {
                        //此处为 js 返回的结果
                        //System.out.print(res)
                        //return res
                    }
                })
        }
    }
    fun joAppJs(funName: String, data: String){
        if (Build.VERSION.SDK_INT< 18) {
            if(data==null) {
                webview?.loadUrl("javascript:$funName()")
            }else{
                webview?.loadUrl("javascript:$funName('$data')")
            }
        } else {
            // 安卓调用js方法 4.4以上
            if(data==null) {
                webview?.evaluateJavascript(
                    "javascript:$funName()",
                    object : ValueCallback<String> {
                        override fun onReceiveValue(res: String?) {
                            //此处为 js 返回的结果
                            //System.out.print(res)
                            //return res
                        }
                    })
            }else{
                webview?.evaluateJavascript("javascript:$funName('$data')", object : ValueCallback<String> {
                    override fun onReceiveValue(res: String?) {
                        //此处为 js 返回的结果
                        //System.out.print(res)
                        //return res
                    }
                })
            }

        }
    }

    //跳转到下一个页面
    fun OpenSetting() {
        val intent = Intent(this, SettingActivity::class.java)
        startActivity(intent)
        finish()
    }

    //启动人脸对比窗口
    fun onFaceStart() {
        val intent = Intent();
        //intent.setClass(this@MainActivity, FaceCheckActivity::class.java)
        intent.setClass(this@MainActivity, FaceCompareActivity::class.java)
        startActivity(intent)
    }

    // 接收文件选择器回传信息
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            FilePickerManager.REQUEST_CODE -> {
                if (resultCode == Activity.RESULT_OK) {

                    // 收到选择文件列表
                    val list = FilePickerManager.obtainData()
                    // 执行上传等工作
                    Toast.makeText(this@MainActivity, "你选择了文件数" + list.size, Toast.LENGTH_SHORT).show()

                } else {
                    Toast.makeText(this@MainActivity, "你未执行任何选择", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    /*
    * JoApp JS调用原生桥类库
    * */
    inner  class JoAppObject {

        //测试jsAndroid调用
        @JavascriptInterface
        fun jsAndroid(msg: String) {
            //点击html的Button调用Android的Toast代码
            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, msg, Toast.LENGTH_LONG)
        }

        //接收js传回的web端配置,统一app端原生与嵌套H5的接口
        @JavascriptInterface
        fun joConfig(config: String) {
            //解析json字符串
            val jsonObject = JSONObject(config)
            val joApiUrl: String = jsonObject.getString("ApiUrl")
            val joAppTitle: String = jsonObject.getString("AppTitle")
            val joUpBucketName: String = jsonObject.getString("UpBucketName")
            val joUpFileName: String = jsonObject.getString("UpFileName")
            val joAuthorization: String = jsonObject.getString("Authorization")
            IsDebug = jsonObject.getString("IsDebug")
            PreferencesUtils.putString(this@MainActivity, "IsDebug", IsDebug)
            ApiUrl = joApiUrl
            PreferencesUtils.putString(this@MainActivity, "ApiUrl", ApiUrl)
            FileUpApi= ApiUrl + "common/upload"; //文件上传接口
            PreferencesUtils.putString(this@MainActivity, "FileUpApi", FileUpApi)
            ImageUpApi= ApiUrl + "common/upload"; //图片上传接口
            PreferencesUtils.putString(this@MainActivity, "ImageUpApi", ImageUpApi)
            VideoUpApi= ApiUrl + "common/upload"; //视频上传接口
            PreferencesUtils.putString(this@MainActivity, "VideoUpApi", VideoUpApi)
            Authorization = joAuthorization
            PreferencesUtils.putString(this@MainActivity, "Authorization", Authorization)

            Applicationcode = jsonObject.getString("Applicationcode")
            PreferencesUtils.putString(this@MainActivity, "Applicationcode", Applicationcode)
            ApplicationcodeValue = jsonObject.getString("ApplicationcodeValue")
            PreferencesUtils.putString(this@MainActivity, "ApplicationcodeValue", ApplicationcodeValue)

            AppTitle = joAppTitle
            PreferencesUtils.putString(this@MainActivity, "AppTitle", AppTitle)
            UpBucketName = joUpBucketName; //上传默认盒
            PreferencesUtils.putString(this@MainActivity, "UpBucketName", UpBucketName)
            UpFileName = joUpFileName;  //上传模拟文件字段名
            PreferencesUtils.putString(this@MainActivity, "UpFileName", UpFileName)

            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, ApiUrl + " - " + joAppTitle, Toast.LENGTH_LONG)
        }

        //接收js传回的web端token,统一app端原生与嵌套H5的token验证
        @JavascriptInterface
        fun joToken(token: String) {
            JoDebug.show(this@MainActivity,  " Token1 - " + Token, Toast.LENGTH_LONG)
            // 存储token
            PreferencesUtils.putString(this@MainActivity, "token", token)

            //解析json字符串
            Token = PreferencesUtils.getString(this@MainActivity, "token");
            JoDebug.show(this@MainActivity,  " Token - " + Token, Toast.LENGTH_LONG)
        }

        //将APP中token传入web,实现web根据app存储的token自动登录
        @JavascriptInterface
        fun joTokenToWeb() {
            Token = PreferencesUtils.getString(this@MainActivity, "token");
            joAppJs("joAppJs.setToken", Token);
        }

        //文件选择、上传
        @JavascriptInterface
        fun joFile(returnFunName: String, data: String) {
            //点击html的Button调用Android的Toast代码
            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
            //调用上传方法
            //JoFile.joFile(this@MainActivity, webview, returnFunName, data)
        }

        //图片选择、上传
        @JavascriptInterface
        fun joImage(returnFunName: String, data: String) {
            //点击html的Button调用Android的Toast代码
            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
            //调用上传方法
            JoImage.joImage(this@MainActivity, webview, returnFunName, data)
        }

        //位置信息获取经纬度
        @JavascriptInterface
        fun joLocation(returnFunName: String, data: String) {
            JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
            //调用位置获取方法
            JoLocation.LatLng(this@MainActivity, webview, returnFunName, data)
        }

        //写入位置范围检测信息,参照点位经度、维度、距离
        @JavascriptInterface
        fun joCheckLocation(data: String) {
            // 存储token
            PreferencesUtils.putString(this@MainActivity, "CheckLocation", data)
        }

        //写入人脸比对校验信息,参照人脸URL,姓名,达标相似度
        @JavascriptInterface
        fun joCheckFace(data: String) {
            // 存储token
            PreferencesUtils.putString(this@MainActivity, "CheckFace", data)
        }

        //人脸信息对比
        @JavascriptInterface
        fun joFaceCompare(returnFunName: String, data: String) {
            JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
            //人脸对比获取方法
            onFaceStart();
        }

        //打开本地人脸库
        @JavascriptInterface
        fun joFaceData() {
            //JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
            //人脸对比获取方法
            val intent = Intent();
            intent.setClass(this@MainActivity, SearchNaviActivity::class.java)
            startActivity(intent)

        }

        //打开APP设置界面
        @JavascriptInterface
        fun joSetting() {
            OpenSetting()
        }

        @JavascriptInterface
        fun jsAndroidRes(msg: String, resJsFun: String) {

            //[email protected]?.loadUrl("javascript:$resJsFun()")

            //回传数据给js //, "数据回来啦!"
            JoDebug.show(this@MainActivity,  " - " + resJsFun, Toast.LENGTH_LONG)

            //点击html的Button调用Android的Toast代码
            //我这里让Toast居中显示了
            JoDebug.show(this@MainActivity, msg + " - " + resJsFun, Toast.LENGTH_LONG)

        }

    }

    // 重定义web弹窗
    inner class MyWebChromeClient:WebChromeClient(){

        // 显示 网页加载 进度条
        override fun onProgressChanged(view: WebView?, newProgress: Int) {
            Log.d("JoApp","${newProgress}")
            super.onProgressChanged(view, newProgress)

            if (newProgress == 100) {
                //加载100%
                Log.d(TAG, "onProgressChanged: " + "webView---100%");

                //执行加载完成调用js,如:传入token等
                onLoagJs()
//                if (!isWebViewloadError && View.VISIBLE == btnRetry.getVisibility()){
//                    btnRetry.setVisibility(View.GONE);//重新加载按钮
//                }
            }
        }

        // 处理 WebView 对地理位置权限的请求
        override fun onGeolocationPermissionsShowPrompt(
            origin: String,
            callback: GeolocationPermissions.Callback) {
            super.onGeolocationPermissionsShowPrompt(origin, callback)
            callback.invoke(origin, true, false)
        }
        override fun onJsAlert(
            view: WebView?,
            url: String?,
            message: String?,
            result: JsResult?
        ): Boolean {
            Log.d("JoApp","$message + $result")
            return super.onJsAlert(view, url, message, result)
        }

        override fun onJsPrompt(
            view: WebView?,
            url: String?,
            message: String?,
            defaultValue: String?,
            result: JsPromptResult?
        ): Boolean {
            Log.d("JoApp","$message + $result")
            return super.onJsPrompt(view, url, message, defaultValue, result)
        }

        override fun onJsConfirm(
            view: WebView?,
            url: String?,
            message: String?,
            result: JsResult?
        ): Boolean {
            Log.d("JoApp","$message + $result")
            return super.onJsConfirm(view, url, message, result)
        }

        override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
            Log.d("JoApp","${consoleMessage?.message()}")
            return super.onConsoleMessage(consoleMessage)
        }

        lateinit var webkitPermissionRequest: PermissionRequest

        override fun onPermissionRequest(request: PermissionRequest) {
            webkitPermissionRequest = request
            val requestedResources = request.resources
            for (r in requestedResources) {
                if (r == PermissionRequest.RESOURCE_VIDEO_CAPTURE) {
                    request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
                    break
                }
            }
        }

    }

    /*
    * 监听窗体间信息传递
    * */
    inner class MyBroadcastReceive : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Log.e(TAG,"开始接收.....");
            val result = intent.getStringExtra("result")
            val data = intent.getStringExtra("data")

            if (result != null) {
                Log.e(TAG,"result:" + result);
                val jsonData = "{\"code\":\"200\",\"data\":\"$data\"}"

                //人脸检测结果返回
                if (result == "compareFace") {
                    JoPushWeb(
                        jsonData,
                        "joAppJs.compareFace",
                        webview
                    )
                }

                //打开设置窗口
                if (result == "openSetting") {
                    OpenSetting()
                }

                //保存设置
                if (result == "saveSetting") {
                    webViewReload()
                }

                //打开进度条
                if (result == "progressBar" || result === "progressBar") {
                    val progressBar: ProgressBar = findViewById<ProgressBar>(R.id.progressBar)
                    val pre = data!!.toInt()
                    if (pre >= 100) { //关闭
                        progressBar.visibility = View.GONE
                    } else {
                        progressBar.visibility = View.VISIBLE
                        progressBar.progress = data.toInt()
                    }
                }

//                Log.e(MainActivity.TAG, result)
            }
        }
    }

4、结尾

   一定要赶在新年第一天内完成本篇发布,更加详细代码本文暂不作详细讲解。后续将持续发文讲解,并将代码放到这里。本人安卓水平优先,文章适用于众多新手,老手可直接绕过!!!

    所有代码免费分享给大家随便使用,无需考虑版权和收费问题,完整代码放在下面的连接中了,请拿走。

joapp: 一个用于原生APP与内嵌WEB间进行交互的代码集合,方便实现H5中对原生APP各种能力的调用,简单易用。 (gitee.com)

    附代码结构截图:

标签: android

本文转载自: https://blog.csdn.net/duihao/article/details/135326654
版权归原作者 对号东 所有, 如有侵权,请联系我们删除。

“【JoAPP】Android WebView与H5交互实现(JAVA+KOTLIN)”的评论:

还没有评论