html5与android之间相互调用

html5与android之间相互调用

H5 与 Android 原生之间的相互调用是混合开发的核心场景,常用方式包括 H5 调用 Android 方法(通过 JavaScriptInterface)和 Android 调用 H5 方法(通过 evaluateJavascript)。

一、H5 调用 Android 原生方法(H5 → Android)

通过 Android 给 WebView 注册 JavaScriptInterface 接口,H5 可直接调用该接口中的方法,传递参数或触发原生逻辑。

1. 基础用法:传递简单参数(字符串、数字)

Android 侧:定义接口并注册到 WebView

kotlin

// 1. 定义交互接口类
class AndroidInterface(private val context: Context) {
    // 注解 @JavascriptInterface 必须添加(API 17+)
    @JavascriptInterface
    fun showToast(message: String) {
        // 在主线程显示 Toast(WebView 回调在子线程,需切换)
        Handler(Looper.getMainLooper()).post {
            Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
        }
    }

    @JavascriptInterface
    fun openNativePage(pageName: String) {
        // 打开原生页面(如 ***pose 页面或 Activity)
        Handler(Looper.getMainLooper()).post {
            when (pageName) {
                "setting" -> context.startActivity(Intent(context, SettingActivity::class.java))
                "user" -> context.startActivity(Intent(context, UserActivity::class.java))
            }
        }
    }
}

// 2. 在 WebView 中注册接口
WebView(context).apply {
    settings.javaScriptEnabled = true // 必须开启 JS
    // 注册接口:H5 中通过 window.AndroidInterface 调用
    addJavascriptInterface(AndroidInterface(context), "AndroidInterface")
}

H5 侧:调用原生方法

javascript

运行

// 调用原生 Toast
window.AndroidInterface.showToast("这是 H5 触发的原生 Toast");

// 调用原生打开页面
document.getElementById("openSetting").onclick = function() {
    window.AndroidInterface.openNativePage("setting");
};
2. 传递复杂参数(JSON 对象)

H5 传递 JSON 字符串,原生解析为对象;或原生返回 JSON 给 H5。

Android 侧:接收 JSON 并解析

kotlin

@JavascriptInterface
fun submitForm(formJson: String) {
    // 用 Gson 解析 JSON 字符串
    val gson = Gson()
    val formData = gson.fromJson(formJson, FormData::class.java) // FormData 是数据类
    // 处理表单数据(如提交到服务器)
    Handler(Looper.getMainLooper()).post {
        Log.d("H5交互", "收到表单:${formData.username}, ${formData.phone}")
    }
}

// 数据类
data class FormData(val username: String, val phone: String, val address: String)

H5 侧:传递 JSON 字符串

javascript

运行

// 构造表单数据
const formData = {
    username: "张三",
    phone: "13800138000",
    address: "北京市"
};
// 转成 JSON 字符串传递(避免参数格式错误)
window.AndroidInterface.submitForm(JSON.stringify(formData));
3. 原生回调 H5(带返回值)

H5 调用原生方法时,传递一个回调函数名,原生处理完成后调用该 H5 函数返回结果。

Android 侧:调用 H5 回调函数

kotlin

@JavascriptInterface
fun getNativeConfig(callbackName: String) {
    // 原生获取配置信息
    val config = mapOf(
        "appVersion" to "1.0.0",
        "isLogin" to true,
        "theme" to "dark"
    )
    val configJson = Gson().toJson(config) // 转 JSON 字符串
    // 调用 H5 的回调函数(通过 evaluateJavascript)
    Handler(Looper.getMainLooper()).post {
        webView.evaluateJavascript(
            "window.$callbackName($configJson);", // 执行 H5 回调
            null
        )
    }
}

H5 侧:定义回调并接收结果

javascript

运行

// 定义回调函数(挂载到 window 上)
window.onConfigReceived = function(config) {
    console.log("原生配置:", config);
    // 处理配置(如更新页面主题)
    if (config.theme === "dark") {
        document.body.classList.add("dark-mode");
    }
};

// 调用原生方法,传递回调函数名
window.AndroidInterface.getNativeConfig("onConfigReceived");

二、Android 调用 H5 方法(Android → H5)

Android 通过 WebView.evaluateJavascript() 执行 H5 中的全局方法,传递参数或获取返回值。

1. 调用 H5 无参方法

H5 侧:定义全局方法

javascript

运行

// 全局方法:刷新页面数据
window.refreshPage = function() {
    console.log("原生触发页面刷新");
    // 执行刷新逻辑(如重新请求接口)
    fetchData();
};

Android 侧:调用该方法

kotlin

// 在需要时(如原生数据更新后)调用
webView.evaluateJavascript(
    "window.refreshPage();", // 执行 H5 方法
    null // 无返回值时可忽略回调
)
2. 传递参数给 H5 方法

H5 侧:定义带参方法

javascript

运行

// 全局方法:更新用户信息显示
window.updateUserInfo = function(userInfo) {
    document.getElementById("username").innerText = userInfo.name;
    document.getElementById("avatar").src = userInfo.avatar;
};

Android 侧:传递参数(JSON 格式)

kotlin

// 构造用户信息
val userInfo = mapOf(
    "name" to "李四",
    "avatar" to "https://example.***/avatar.png"
)
val userJson = Gson().toJson(userInfo) // 转 JSON 字符串

// 调用 H5 方法并传递参数
webView.evaluateJavascript(
    "window.updateUserInfo($userJson);", // 注意参数无需引号(JSON 本身带引号)
    null
)
3. 获取 H5 方法的返回值

H5 侧:定义有返回值的方法

javascript

运行

// 全局方法:获取当前页面标题
window.getPageTitle = function() {
    return document.title; // 返回页面标题
};

Android 侧:接收返回值(通过回调)

kotlin

webView.evaluateJavascript("window.getPageTitle();") { result ->
    // result 是 H5 返回的字符串(带双引号,需处理)
    val title = result.replace("\"", "") // 去除引号
    Log.d("H5返回", "当前页面标题:$title")
}

三、通用注意事项

  1. 线程问题

    • JavaScriptInterface 的方法运行在 WebView 子线程,若需更新 UI(如显示 Toast、跳转页面),需切换到主线程(用 Handler 或 runOnUiThread)。
    • evaluateJavascript 的回调也运行在子线程,更新 UI 需同样处理。
  2. 安全问题

    • addJavascriptInterface 在 API < 17 时有安全漏洞(可能被恶意 H5 利用),需确保只加载可信 H5,或升级最小支持版本(API 17+)。
    • 传递敏感信息(如 Token)时,避免明文传输,可加密后传递。
  3. 参数格式

    • 字符串参数需用双引号包裹(如 window.showToast("消息"))。
    • 复杂对象必须序列化为 JSON 字符串,避免语法错误。
  4. 调试技巧

    • H5 侧:用 console.log 输出日志,通过 Chrome 开发者工具(chrome://inspect)查看。
    • Android 侧:用 WebChromeClient 监听 H5 日志:

      kotlin

      webChromeClient = object : WebChromeClient() {
          override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
              Log.d("H5日志", "${consoleMessage.message()}")
              return super.onConsoleMessage(consoleMessage)
          }
      }
      

四、典型应用场景

场景 调用方向 示例方法
显示原生弹窗(Toast/Dialog) H5 → Android showToast(message)showDialog(title, content)
打开原生页面 / 功能 H5 → Android openCamera()openMap(location)openNativePage(pageName)
同步登录状态 双向 H5 传 Token 给原生(setToken(token));原生传用户信息给 H5(updateUserInfo(user)
原生触发 H5 刷新 Android → H5 refreshPage()reloadData()
传递设备信息 Android → H5 getDeviceInfo()(返回设备型号、系统版本等)

通过上述方法,可实现 H5 与 Android 原生的灵活交互,满足混合开发的大部分需求。

三、截图展示及代码

效果图:

相关代码:



import android.Manifest
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.***.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import androidx.activity.***ponentActivity
import androidx.activity.***pose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.***pose.foundation.layout.Column
import androidx.***pose.foundation.layout.fillMaxSize
import androidx.***pose.foundation.layout.fillMaxWidth
import androidx.***pose.foundation.layout.padding
import androidx.***pose.material3.Button
import androidx.***pose.material3.MaterialTheme
import androidx.***pose.material3.Surface
import androidx.***pose.material3.Text
import androidx.***pose.runtime.***posable
import androidx.***pose.ui.Modifier
import androidx.***pose.ui.platform.LocalContext
import androidx.***pose.ui.unit.dp
import androidx.***pose.ui.viewinterop.AndroidView
import androidx.core.app.Activity***pat
import androidx.core.content.Context***pat
import androidx.core.content.FileProvider
import ***.example.webviewh5conn.ui.theme.Webviewh5connTheme
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivityold : ***ponentActivity() {
    // 拍照相关变量
    private var currentPhotoPath: String? = null
    private var h5PhotoCallback: String? = null

    private val takePictureLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
            currentPhotoPath?.let { path ->
                val imageUri = getImageUri(File(path))
                Toast.makeText(this, "拍照成功: ${imageUri.toString()}", Toast.LENGTH_LONG).show()
                callH5Callback(imageUri.toString())
            }
        } else {
            callH5Callback("error")
        }
    }

    private val pickPictureLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK && result.data != null) {
            // 获取相册选中的图片Uri
            val selectedImageUri = result.data?.data
            selectedImageUri?.let { uri ->
                Toast.makeText(this, "选中图片: ${uri.toString()}", Toast.LENGTH_LONG).show()
                callH5Callback(uri.toString()) // 直接返回Uri给H5
            } ?: run {
                callH5Callback("error")
            }
        } else {
            callH5Callback("error")
        }
    }


    private fun callH5Callback(imagePath: String) {
        h5PhotoCallback?.let { callback ->
            webView?.evaluateJavascript("javascript:$callback('$imagePath')") {}
        }
        h5PhotoCallback = null
    }

    // 生成图片Uri(适配7.0+)
    private fun getImageUri(file: File): Uri {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
        } else {
            Uri.fromFile(file)
        }
    }

    private var webView: WebView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Webviewh5connTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    WebViewContainer()
                }
            }
        }
    }

    @***posable
    fun WebViewContainer() {
        val context = LocalContext.current

        Column(
            modifier = Modifier.fillMaxSize()
        ) {
            Button(
                onClick = {
                    val message = "Hello from ***pose Android!"
                    webView?.evaluateJavascript("javascript:showMessage('$message')") { result ->
                        Toast.makeText(context, "H5返回: $result", Toast.LENGTH_SHORT).show()
                    }
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 50.dp, start = 20.dp, end = 20.dp)
            ) {
                Text("Android调用H5方法")
            }

            AndroidView(
                factory = { ctx ->
                    WebView(ctx).apply {
                        webView = this
                        initWebViewSettings(this, context)
                        loadUrl("file:///android_asset/dist/index.html"); 
                    }
                },
                modifier = Modifier.fillMaxSize()
            )
        }
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun initWebViewSettings(webView: WebView, context: Context) {
        val webSettings = webView.settings
        webSettings.javaScriptEnabled = true
        webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        webSettings.allowFileA***ess = true
        webSettings.allowContentA***ess = true
        // 允许访问ContentProvider资源(相册图片可能是content://格式)
        webSettings.allowFileA***essFromFileURLs = true
        webSettings.allowUniversalA***essFromFileURLs = true

        webView.addJavascriptInterface(AndroidInterface(this), "AndroidInterface")
        webView.webChromeClient = WebChromeClient()
    }

    inner class AndroidInterface(private val activity: MainActivityold) {
        @JavascriptInterface
        fun showToast(message: String) {
            activity.runOnUiThread {
                Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
            }
        }

        @JavascriptInterface
        fun getAndroidInfo(): String {
            return "Android设备信息:型号=${Build.MODEL}"
        }


        @JavascriptInterface
        fun takePhoto(callback: String) {
            activity.runOnUiThread {
                activity.h5PhotoCallback = callback
                if (Context***pat.checkSelfPermission(activity, Manifest.permission.CAMERA)
                    != PackageManager.PERMISSION_GRANTED
                ) {
                    Activity***pat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1001)
                } else {
                    startCamera()
                }
            }
        }


        @JavascriptInterface
        fun pickPhoto(callback: String) {
            activity.runOnUiThread {
                activity.h5PhotoCallback = callback
                // 检查相册权限(Android 13+需要READ_MEDIA_IMAGES权限)
                val requiredPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    Manifest.permission.READ_MEDIA_IMAGES
                } else {
                    Manifest.permission.READ_EXTERNAL_STORAGE
                }

                if (Context***pat.checkSelfPermission(activity, requiredPermission)
                    != PackageManager.PERMISSION_GRANTED
                ) {
                    // 请求相册权限
                    Activity***pat.requestPermissions(activity, arrayOf(requiredPermission), 1002)
                } else {
                    // 已有权限,打开相册
                    openGallery()
                }
            }
        }
    }

    // 启动相机(原有)
    private fun startCamera() {
        val photoFile = createImageFile() ?: run {
            Toast.makeText(this, "无法创建图片文件", Toast.LENGTH_SHORT).show()
            return
        }
        val photoUri = getImageUri(photoFile)
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        takePictureLauncher.launch(intent)
    }

    // 打开相册
        private fun openGallery() {
        try {
            val intent = Intent(Intent.ACTION_PICK).apply {
                setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_MIME_TYPE)
            }
            pickPictureLauncher.launch(intent)
        } catch (e: ActivityNotFoundException) {
            // 处理没有找到合适应用的情况
            Log.e("openGallery", "No activity found to handle image picking", e)
        }
    }

    ***panion object {
        private const val IMAGE_MIME_TYPE = "image/*"
    }


    // 创建图片文件
    private fun createImageFile(): File? {
        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date())
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return try {
            File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir).apply {
                currentPhotoPath = absolutePath
            }
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    @Deprecated("Deprecated in Java")
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1001 -> { // 相机权限
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    startCamera()
                } else {
                    callH5Callback("error")
                    Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show()
                }
            }
            1002 -> { // 相册权限
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    openGallery()
                } else {
                    callH5Callback("error")
                    Toast.makeText(this, "需要相册权限才能选择照片", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        webView?.destroy()
    }
}

转载请说明出处内容投诉
CSS教程网 » html5与android之间相互调用

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买