1、前言
? ? ? ?最近一個應(yīng)急平臺的項目移動端開發(fā),原計劃用UNI-APP實現(xiàn),客戶想著要集成語音、視頻通話功能,基于經(jīng)驗判斷需要買一套IM原生移動端框架去結(jié)合H5整合比較合適,沒想到最后客戶不想采購,而且語音視頻通話功能也遲遲未能完全確認(rèn),H5部分所開發(fā)的業(yè)務(wù)功能已經(jīng)實現(xiàn),但原生端開發(fā)模式遲遲未定,緊急時刻,決定啟動前幾年一直使用的一組android原生APP+H5(WEB)實現(xiàn)移動端開發(fā),隨即找了前幾年的原生框架代碼,發(fā)現(xiàn)與新的版本已不兼容,索性重新梳理,整理一套新的代碼,也決定對外開放給朋友們使用,暫時延續(xù)之前內(nèi)部框架名稱JoApp,目前只整理了android+h5代碼,后續(xù)還會將IOS版整理出來。
? ? ? ? 恰逢2024年第一天元旦,祝福各位朋友新年快樂!這個節(jié)假日老哥我最大收獲就是這個框架中實現(xiàn)了人臉識別、人臉對比的API,滿足各類應(yīng)用系統(tǒng)手機(jī)APP中實現(xiàn)人臉識別、位置校驗的需要,方便大家哪里即用。
本文涉及代碼開發(fā)工具如下:
Android Studio Giraffe | 2022.3.1 Patch 3、VSCode
語言及管理:
Java Jdk(OpenJDK17)、Kotlin、Gradle-8.4
2、原生APP與H5交互的核心實現(xiàn)
? ? ? ?基于JS方法在在APP與WebView內(nèi)的H5間進(jìn)行調(diào)用實現(xiàn),這里主要演示Kotilin的代碼,如需要JAVA版,可以使用文心一言等智能工具進(jìn)行轉(zhuǎn)換。
? ? ? ?原生APP端核心原理代碼如下(寫在 MainActivity內(nèi)):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 隱藏狀態(tài)欄和導(dǎo)航欄
requestWindowFeature(Window.FEATURE_NO_TITLE)
// 設(shè)置窗口全屏
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// 獲取 WebView 組件
webview = findViewById<WebView>(R.id.web_view)
// 獲取并設(shè)置 Web 設(shè)置
val settings = webview?.settings
settings?.javaScriptEnabled = true // 支持 JavaScript
// 設(shè)置是否啟用 DOM 存儲
// DOM 存儲是一種在 Web 應(yīng)用程序中存儲數(shù)據(jù)的機(jī)制,它使用 JavaScript 對象和屬性來存儲和檢索數(shù)據(jù)
settings?.domStorageEnabled = true
// 設(shè)置 WebView 是否啟用內(nèi)置縮放控件 ( 自選 非必要 )
//settings.builtInZoomControls = true
// 5.0 以上需要設(shè)置允許 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
}
// 設(shè)置頁面自適應(yīng)
// Viewport 元標(biāo)記是指在 HTML 頁面中的 <meta> 標(biāo)簽 , 可以設(shè)置網(wǎng)頁在移動端設(shè)備上的顯示方式和縮放比例
// 設(shè)置是否支持 Viewport 元標(biāo)記的寬度
settings?.useWideViewPort = true
// 設(shè)置 WebView 是否使用寬視圖端口模式
// 寬視圖端口模式下 , WebView 會將頁面縮小到適應(yīng)屏幕的寬度
// 沒有經(jīng)過移動端適配的網(wǎng)頁 , 不要啟用該設(shè)置
settings?.loadWithOverviewMode = true
// 設(shè)置 WebView 是否可以獲取焦點 ( 自選 非必要 )
webview?.isFocusable = true
// 設(shè)置 WebView 是否啟用繪圖緩存 位圖緩存可加速繪圖過程 ( 自選 非必要 )
webview?.isDrawingCacheEnabled = true
// 設(shè)置 WebView 中的滾動條樣式 ( 自選 非必要 )
// SCROLLBARS_INSIDE_OVERLAY - 在內(nèi)容上覆蓋滾動條 ( 默認(rèn) )
webview?.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
// WebViewClient 是一個用于處理 WebView 頁面加載事件的類
webview?.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
// 4.0 之后必須添加該設(shè)置
// 只能加載 http:// 和 https:// 頁面 , 不能加載其它協(xié)議鏈接
if (url.startsWith("http://") || url.startsWith("https://")) {
view.loadUrl(url)
return true
}
return false
}
// SSL 證書校驗出現(xiàn)異常
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()
// 加載網(wǎng)頁
webview?.loadUrl(WebUrl)
// js調(diào)用安卓方法支持(第二個參數(shù)是js代碼中調(diào)用APP中的交互橋類定義的名,需保持一致)
webview?.addJavascriptInterface(JoAppObject(),"joApp")
// 原生調(diào)用js中的方法(不帶參數(shù)版)
// 這里joAppJs與H5 web端中定義的被原生調(diào)用JS類new的變量名一致,方便統(tǒng)一調(diào)用
joAppJs("joAppJs.test")
// 原生調(diào)用js中的方法(帶參數(shù)版)
joAppJs("joAppJs.testData","一只可愛的對號")
}
// 原生調(diào)用JS方法,方法名
fun joAppJs(funName: String){
JoDebug.show(this@MainActivity, " - " + funName, Toast.LENGTH_LONG)
if (Build.VERSION.SDK_INT< 18) {
webview?.loadUrl("javascript:$funName()")
} else {
// 安卓調(diào)用js方法 4.4以上
webview?.evaluateJavascript(
"javascript:$funName()",
object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//System.out.print(res)
//return res
}
})
}
}
// 原生調(diào)用JS方法,參數(shù)1:JS方法名、參數(shù)2:傳給JS方法的參數(shù)(支持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 {
// 安卓調(diào)用js方法 4.4以上
if(data==null) {
webview?.evaluateJavascript(
"javascript:$funName()",
object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//System.out.print(res)
//return res
}
})
}else{
webview?.evaluateJavascript("javascript:$funName('$data')", object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//System.out.print(res)
//return res
}
})
}
}
}
/*
* JoApp 原生提供給H5可被JS調(diào)用的橋類庫,真實的原生實現(xiàn)方法類庫
需要將與原生交互的各種API類寫在這里,實現(xiàn)H5的方便調(diào)用
* */
inner class JoAppObject {
//測試jsAndroid調(diào)用
@JavascriptInterface
fun jsAndroid(msg: String) {
//點擊html的Button調(diào)用Android的Toast代碼
//我這里讓Toast居中顯示了
JoDebug.show(this@MainActivity, msg, Toast.LENGTH_LONG)
}
}
? ? ? ? 嵌入的H5 WEB中配套代碼如下:
...
<button type="button" onclick="clickAndroid()">無回傳調(diào)用安卓方法</button>
...
<script type="text/javascript">
/*
JoAppJs 安卓調(diào)用的JS方法庫
*/
class JoAppJs {
//測試不帶參數(shù)
test () {
alert("Android調(diào)用了JS代碼")
document.getElementById("showres").innerHTML = "Android調(diào)用了JS代碼"
}
//測試不帶參數(shù)
testData (data) {
alert("Android調(diào)用了JS代碼" + data)
document.getElementById("showres").innerHTML = data
}
}
//定義被APP原生調(diào)用的H5中JS類庫變量名,方便統(tǒng)一調(diào)用
const joAppJs = new JoAppJs()
//測試調(diào)用原生APP
function clickAndroid(){
//用joapp.調(diào)用映射的對象 這里的androids是addJavascriptInterface()的第二個參數(shù)
joApp.jsAndroid("我是JS,我調(diào)用了Android的方法")
}
</script>
3、JoAPP已實現(xiàn)的交互API方法庫
? ? ? ?在JoApp中已經(jīng)實現(xiàn)了一些原生APP與WebView H5中js的交互方法,以下列出當(dāng)前關(guān)鍵方法,后續(xù)會逐步新增在JoApp Git倉庫中,也會在后續(xù)文章中逐個解析重點API實現(xiàn)原理。
? ? ? ? APP已實現(xiàn)的API包括:
- 配置信息:joConfig
- APP接收WEB中token:joToen
- 向WEB發(fā)送APP中token:joTokenToWeb
- 啟動原生文件上傳:joFile
- 啟動原生圖片上傳(瀏覽相冊+拍照):joImage
- 獲取原生APP位置信息(經(jīng)緯度):joLocation
- APP接收位置有效性檢測參照信息:joCheckLocation
- APP接收人臉有效性檢測參照信息:joCheckFace
- 啟動APP人臉及位置有效性對比功能:joFaceCompare
- 啟動APP設(shè)置界面(配置WEB網(wǎng)址):joSetting
? ? ? ? 具體代碼如下,請根據(jù)需要自行依據(jù)注釋進(jìn)行使用:
//權(quán)限
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)
}
})
}
// 加載完成后自動調(diào)取的js
fun onLoagJs() {
//joAppJs("joAppJs.test")
//joAppJs("joAppJs.testData","我的神")
//獲取H5中包括接口地址在內(nèi)的設(shè)置等信息,用于傳遞H5中的默認(rèn)信息給原生app
//改由web頁面加載后向原生單向推送
//joAppJs("joAppJs.config")
//向web傳入app緩存中的token
//改由web頁面加載后向原生推送
//joAppJs("joAppJs.token");
}
// 調(diào)用JS方法, 方法名、參數(shù)(支持json字符串)
fun joAppJs(funName: String){
JoDebug.show(this@MainActivity, " - " + funName, Toast.LENGTH_LONG)
if (Build.VERSION.SDK_INT< 18) {
webview?.loadUrl("javascript:$funName()")
} else {
// 安卓調(diào)用js方法 4.4以上
webview?.evaluateJavascript(
"javascript:$funName()",
object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//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 {
// 安卓調(diào)用js方法 4.4以上
if(data==null) {
webview?.evaluateJavascript(
"javascript:$funName()",
object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//System.out.print(res)
//return res
}
})
}else{
webview?.evaluateJavascript("javascript:$funName('$data')", object : ValueCallback<String> {
override fun onReceiveValue(res: String?) {
//此處為 js 返回的結(jié)果
//System.out.print(res)
//return res
}
})
}
}
}
//跳轉(zhuǎn)到下一個頁面
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()
// 執(zhí)行上傳等工作
Toast.makeText(this@MainActivity, "你選擇了文件數(shù)" + list.size, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "你未執(zhí)行任何選擇", Toast.LENGTH_SHORT).show()
}
}
}
}
/*
* JoApp JS調(diào)用原生橋類庫
* */
inner class JoAppObject {
//測試jsAndroid調(diào)用
@JavascriptInterface
fun jsAndroid(msg: String) {
//點擊html的Button調(diào)用Android的Toast代碼
//我這里讓Toast居中顯示了
JoDebug.show(this@MainActivity, msg, Toast.LENGTH_LONG)
}
//接收js傳回的web端配置,統(tǒng)一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; //上傳默認(rèn)盒
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,統(tǒng)一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,實現(xiàn)web根據(jù)app存儲的token自動登錄
@JavascriptInterface
fun joTokenToWeb() {
Token = PreferencesUtils.getString(this@MainActivity, "token");
joAppJs("joAppJs.setToken", Token);
}
//文件選擇、上傳
@JavascriptInterface
fun joFile(returnFunName: String, data: String) {
//點擊html的Button調(diào)用Android的Toast代碼
//我這里讓Toast居中顯示了
JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
//調(diào)用上傳方法
//JoFile.joFile(this@MainActivity, webview, returnFunName, data)
}
//圖片選擇、上傳
@JavascriptInterface
fun joImage(returnFunName: String, data: String) {
//點擊html的Button調(diào)用Android的Toast代碼
//我這里讓Toast居中顯示了
JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
//調(diào)用上傳方法
JoImage.joImage(this@MainActivity, webview, returnFunName, data)
}
//位置信息獲取經(jīng)緯度
@JavascriptInterface
fun joLocation(returnFunName: String, data: String) {
JoDebug.show(this@MainActivity, returnFunName + " - " + data, Toast.LENGTH_LONG)
//調(diào)用位置獲取方法
JoLocation.LatLng(this@MainActivity, webview, returnFunName, data)
}
//寫入位置范圍檢測信息,參照點位經(jīng)度、維度、距離
@JavascriptInterface
fun joCheckLocation(data: String) {
// 存儲token
PreferencesUtils.putString(this@MainActivity, "CheckLocation", data)
}
//寫入人臉比對校驗信息,參照人臉URL,姓名,達(dá)標(biāo)相似度
@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設(shè)置界面
@JavascriptInterface
fun joSetting() {
OpenSetting()
}
@JavascriptInterface
fun jsAndroidRes(msg: String, resJsFun: String) {
//this@MainActivity.webview?.loadUrl("javascript:$resJsFun()")
//回傳數(shù)據(jù)給js //, "數(shù)據(jù)回來啦!"
JoDebug.show(this@MainActivity, " - " + resJsFun, Toast.LENGTH_LONG)
//點擊html的Button調(diào)用Android的Toast代碼
//我這里讓Toast居中顯示了
JoDebug.show(this@MainActivity, msg + " - " + resJsFun, Toast.LENGTH_LONG)
}
}
// 重定義web彈窗
inner class MyWebChromeClient:WebChromeClient(){
// 顯示 網(wǎng)頁加載 進(jìn)度條
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%");
//執(zhí)行加載完成調(diào)用js,如:傳入token等
onLoagJs()
// if (!isWebViewloadError && View.VISIBLE == btnRetry.getVisibility()){
// btnRetry.setVisibility(View.GONE);//重新加載按鈕
// }
}
}
// 處理 WebView 對地理位置權(quán)限的請求
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
}
}
}
}
/*
* 監(jiān)聽窗體間信息傳遞
* */
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\"}"
//人臉檢測結(jié)果返回
if (result == "compareFace") {
JoPushWeb(
jsonData,
"joAppJs.compareFace",
webview
)
}
//打開設(shè)置窗口
if (result == "openSetting") {
OpenSetting()
}
//保存設(shè)置
if (result == "saveSetting") {
webViewReload()
}
//打開進(jìn)度條
if (result == "progressBar" || result === "progressBar") {
val progressBar: ProgressBar = findViewById<ProgressBar>(R.id.progressBar)
val pre = data!!.toInt()
if (pre >= 100) { //關(guān)閉
progressBar.visibility = View.GONE
} else {
progressBar.visibility = View.VISIBLE
progressBar.progress = data.toInt()
}
}
// Log.e(MainActivity.TAG, result)
}
}
}
4、結(jié)尾
? ? ? ?一定要趕在新年第一天內(nèi)完成本篇發(fā)布,更加詳細(xì)代碼本文暫不作詳細(xì)講解。后續(xù)將持續(xù)發(fā)文講解,并將代碼放到這里。本人安卓水平優(yōu)先,文章適用于眾多新手,老手可直接繞過?。?!
????????所有代碼免費分享給大家隨便使用,無需考慮版權(quán)和收費問題,完整代碼放在下面的連接中了,請拿走。
joapp: 一個用于原生APP與內(nèi)嵌WEB間進(jìn)行交互的代碼集合,方便實現(xiàn)H5中對原生APP各種能力的調(diào)用,簡單易用。 (gitee.com)
????????附代碼結(jié)構(gòu)截圖:文章來源:http://www.zghlxwxcb.cn/news/detail-777695.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-777695.html
到了這里,關(guān)于【JoAPP】Android WebView與H5交互實現(xiàn)(JAVA+KOTLIN)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!