当前位置:首页 » 《随便一记》 » 正文

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

1 人参与  2024年02月14日 13:01  分类 : 《随便一记》  评论

点击全文阅读


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包括:

配置信息:joConfigAPP接收WEB中token:joToen向WEB发送APP中token:joTokenToWeb启动原生文件上传:joFile启动原生图片上传(浏览相册+拍照):joImage获取原生APP位置信息(经纬度):joLocationAPP接收位置有效性检测参照信息:joCheckLocationAPP接收人脸有效性检测参照信息: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) {            //this@MainActivity.webview?.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)

        附代码结构截图:


点击全文阅读


本文链接:http://zhangshiyu.com/post/68461.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1