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人脸及位置有效性对比功能:joFace***pare
- 启动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.A***ESS_***WORK_STATE,
Manifest.permission.A***ESS_WIFI_STATE,
Manifest.permission.SYSTEM_ALERT_WINDOW,
Manifest.permission.A***ESS_COARSE_LOCATION,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.A***ESS_FINE_LOCATION,
Manifest.permission.A***ESS_LOCATION_EXTRA_***MANDS,
Manifest.permission.CHANGE_***WORK_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, Face***pareActivity::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 + "***mon/upload"; //文件上传接口
PreferencesUtils.putString(this@MainActivity, "FileUpApi", FileUpApi)
ImageUpApi= ApiUrl + "***mon/upload"; //图片上传接口
PreferencesUtils.putString(this@MainActivity, "ImageUpApi", ImageUpApi)
VideoUpApi= ApiUrl + "***mon/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 joFace***pare(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 == "***pareFace") {
JoPushWeb(
jsonData,
"joAppJs.***pareFace",
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.***)
附代码结构截图: