一、前言
所谓通信,说白了就是WebView和原生互相调用对方的函数、互相传值。WebView和原生通信的方式也很简单,不过最好是自己动手实现一下,才能够记得牢固。不管是面试还是笔试,都会经常考到。我也看过其他大佬的文章,不过有些地方没有给出必要的说明,让人看后还是不知道怎么实现,所以本文的最后会给出整个项目的源码,若有不清楚的地方,下载下来跑一遍就能够理解了。Demo的截图如下:
二、原生调用js
方法1:通过WebView对象的loadUrl()方法
loadUrl()方法不仅可以加载一个html网页,还可以调用js的方法,是不是很神奇呢!这里给出的例子是原生调用js的cat()方法,loadUrl()方式的缺点是不方便获取js方法的返回值,如果你给这个cat()方法返回一个值,你会发现html页面的body直接就展示这个返回值了,这显然不是你想要的结果吧。
原生java代码
mWebView.loadUrl("javascript:cat()");
html
let flag1 = true
function cat() {
document.getElementById('div1').innerHTML = flag1 ? "小肥咪" : "大肥咪"
flag1 = !flag1
}
方法2:通过WebView对象的evaluateJavascript()方法
上面说了loadUrl()方式的缺点是不方便获取js方法的返回值,那么想要方便地获取js方法的返回值,可以使用evaluateJavascript()这个方法。
原生java代码
mWebView.evaluateJavascript("javascript:sayHello()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {//可以在这个回调方法里面拿到js方法的返回值
Log.d(TAG, "onReceiveValue: "+value);
}
});
html
let flag2 = true
function sayHello() {
document.getElementById("div2").innerHTML = flag2 ? "喵喵喵喵叫" : "嗷嗷叫"
flag2 = !flag2
return "我是js方法的返回值"
}
三、js调用原生
js调用原生的话,这里介绍3种方式。
方法1:通过WebView对象的addJavascriptInterface()方法
这种方法其实就是用了对象映射,使用起来也不复杂。我们需要写一个类,在里面写上自己的方法,再把@JavascriptInterface注解加到你的方法上面。addJavascriptInterface()这个方法接收两个参数,第一个参数直接new出来一个刚才你自定义的类的对象;addJavascriptInterface()方法的第二个参数是一个字符串,是可以自定义的,这里我直接自定义为"android",其实也就是js里面用到的,作为一个对象名。
原生java代码
//自定义一个类
class MyNative{
@JavascriptInterface
public void getNative1(String who){
Log.d(TAG, "getNative1: "+who);
Toast.makeText(MainActivity.this, "js通过对象映射addJavascriptInterface的方式调用了原生的方法", Toast.LENGTH_SHORT).show();
}
}
MyNative myNative = new MyNative();//不要使用匿名内部类放入addJavascriptInterface中
//让js调用原生的方法
mWebView.addJavascriptInterface(myNative,"android");//这里的android也可以写成别的字符串,待会js里面需要用到这个名字
html
下面的android对应的就是原生java代码里面你取的对象名
<button class="btn" onclick="window.android.getNative1('张三')">addJavascriptInterafce</button>
方法2:通过重写WebViewClient对象的shouldOverrideUrlLoading()方法
shouldOverrideUrlLoading()这个方法返回true,可以将url拦截(不打开新的网页)。在js里面调用的话,我们其实是需要写上一个url的。在url上可以拼接一些参数,传给原生使用。
原生java代码
mWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {//js调用原生的方式二
Log.d(TAG, "shouldOverrideUrlLoading: ");
Uri url = request.getUrl();
if (url!=null){
String schemenName = url.getScheme();//约定好的协议名称,这里我约定成了myscheme,经测试,协议名需要完全为小写字母
if ("myscheme".equals(schemenName)){
String authority = url.getAuthority();//约定好的地址名
if ("myAddress".equals(authority)){
String userName = url.getQueryParameter("userName");//获取用户名参数
String password = url.getQueryParameter("password");//获取密码参数
Log.d(TAG, "shouldOverrideUrlLoading: 获取到js传过来的用户名userName为:"+userName+",密码为:"+password);
Toast.makeText(MainActivity.this, "通过重写shouldOverrideUrlLoading的方式让js调用原生的方法", Toast.LENGTH_SHORT).show();
}
return true;
}
}
return super.shouldOverrideUrlLoading(view,request);
}
});
html
<button class="btn" onclick="document.location = 'myscheme://myAddress?userName=张三&password=123456'">shouldOverrideUrlLoading</button>
方法3: 通过重写WebChromeClient对象的onJsAlert()、onJsConfirm()或者onJsPrompt()方法
这种方式其实就是拦截js中的三种弹框,我觉得有点投机取巧,假设网页不是放在安卓中的WebView里面展示,直接从浏览器打开,那么弹框其实还是可以看到的。
原生java代码
mWebView.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Log.d(TAG, "onJsPrompt: ");
Uri uri = Uri.parse(message);
if (uri!=null){
String schemeName = uri.getScheme();//自定义的协议名称
if("myscheme".equals(schemeName)){
String authority = uri.getAuthority();
if ("myAddress".equals(authority)){//自定义的地址名
String userName = uri.getQueryParameter("userName");//获取js传过来的参数userName
String password = uri.getQueryParameter("password"); //获取js传过来的password
Toast.makeText(MainActivity.this, "通过拦截prompt弹框的方式让js调用原生的方法,原生获取到的userName为:"+userName+",password为:"+password, Toast.LENGTH_SHORT).show();
}
result.cancel();//这行要加上,不然onJsPrompt只会被调用一次,而且后续的js方法也会失效
return true;//返回true表示拦截弹框,只要符合约定的协议,就将js中的prompt弹框拦截,不让其弹出来
}
}
return super.onJsPrompt(view,url,message,defaultValue,result);
}
});
html
<button class="btn" onclick="showPromptDialog()">onJsPrompt</button>
function showPromptDialog() {
prompt("myscheme://myAddress?userName=张三&password=123456")
}
四、代码汇总
看了上面零零散散的代码,你是不是还不明白具体怎么调用?没关系,我相信一次性粘贴完所有的代码,你可以很轻松地理解。
4.1 MainActivity.java代码
package com.example.webviewtest;
import androidx.appcompat.app.AppCompatActivity;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
private Button loadUrlBtn;
private Button evaluateBtn;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initEvents();
}
private void initViews() {
mWebView=findViewById(R.id.webView);
mWebView.loadUrl("file:///android_asset/index.html");
mWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {//js调用原生的方式二
Log.d(TAG, "shouldOverrideUrlLoading: ");
Uri url = request.getUrl();
if (url!=null){
String schemenName = url.getScheme();//约定好的协议名称,这里我约定成了myscheme,经测试,协议名需要完全为小写字母
if ("myscheme".equals(schemenName)){
String authority = url.getAuthority();//约定好的地址名
if ("myAddress".equals(authority)){
String userName = url.getQueryParameter("userName");//获取用户名参数
String password = url.getQueryParameter("password");//获取密码参数
Log.d(TAG, "shouldOverrideUrlLoading: 获取到js传过来的用户名userName为:"+userName+",密码为:"+password);
Toast.makeText(MainActivity.this, "通过重写shouldOverrideUrlLoading的方式让js调用原生的方法", Toast.LENGTH_SHORT).show();
}
return true;
}
}
return super.shouldOverrideUrlLoading(view,request);
}
});
mWebView.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Log.d(TAG, "onJsPrompt: ");
Uri uri = Uri.parse(message);
if (uri!=null){
String schemeName = uri.getScheme();//自定义的协议名称
if("myscheme".equals(schemeName)){
String authority = uri.getAuthority();
if ("myAddress".equals(authority)){//自定义的地址名
String userName = uri.getQueryParameter("userName");//获取js传过来的参数userName
String password = uri.getQueryParameter("password"); //获取js传过来的password
Toast.makeText(MainActivity.this, "通过拦截prompt弹框的方式让js调用原生的方法,原生获取到的userName为:"+userName+",password为:"+password, Toast.LENGTH_SHORT).show();
}
result.cancel();//这行要加上,不然onJsPrompt只会被调用一次,而且后续的js方法也会失效
return true;//返回true表示拦截弹框,只要符合约定的协议,就将js中的prompt弹框拦截,不让其弹出来
}
}
return super.onJsPrompt(view,url,message,defaultValue,result);
}
});
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);//不使用缓存
loadUrlBtn=findViewById(R.id.loadUrlBtn);
evaluateBtn=findViewById(R.id.evaluateBtn);
}
private void initEvents(){
//原生调用js的第一种方式:调用WebView的loadUrl()方法,会使得页面重新刷新一次,不方便获取js函数的返回值
loadUrlBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mWebView.loadUrl("javascript:cat()");
}
});
//原生调用js的第二种方式,调用WebView的evaluateJavascript()方法
evaluateBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mWebView.evaluateJavascript("javascript:sayHello()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {//可以在这个回调方法里面拿到js方法的返回值
Log.d(TAG, "onReceiveValue: "+value);
}
});
}
});
MyNative myNative = new MyNative();//不要使用匿名内部类放入addJavascriptInterface中
//让js调用原生的方法
mWebView.addJavascriptInterface(myNative,"android");
}
class MyNative{
@JavascriptInterface
public void getNative1(String who){
Log.d(TAG, "getNative1: "+who);
Toast.makeText(MainActivity.this, "js通过对象映射addJavascriptInterface的方式调用了原生的方法", Toast.LENGTH_SHORT).show();
}
}
}
4.2 Html代码
<!DOCTYPE html>
<head>
<title>首页</title>
<script>
let flag1 = true
let flag2 = true
//让原生调用的方法1
function cat() {
document.getElementById('div1').innerHTML = flag1 ? "小肥咪" : "大肥咪"
flag1 = !flag1
}
//让原生调用的方法2
function sayHello() {
document.getElementById("div2").innerHTML = flag2 ? "喵喵喵喵叫" : "嗷嗷叫"
flag2 = !flag2
return "我是js方法的返回值"
}
function showPromptDialog() {
prompt("myscheme://myAddress?userName=张三&password=123456")
}
</script>
<style>
.body {
background-color: #c7edcc;
margin: 0;
}
.container {
height: 100vh;
display: flex;
flex-direction: column;
margin-left: 6.0185vw;
margin-right: 6.0185vw;
}
h1 {
text-align: center;
}
.btnDiv {
margin-top: 50px;
display: flex;
flex-direction: column;
}
.btn {
height: 40px;
margin-bottom: 10px;
}
</style>
</head>
<body class="body">
<div class="container">
<h1>我是WebView</h1>
<div id="div1">从前有只猫</div>
<div id="div2">奶牛猫好乖啊啊</div>
<div class="btnDiv">
<button class="btn" onclick="window.android.getNative1('张三')">addJavascriptInterafce</button>
<button class="btn" onclick="document.location = 'myscheme://myAddress?userName=张三&password=123456'">shouldOverrideUrlLoading</button>
<button class="btn" onclick="showPromptDialog()">onJsPrompt</button>
</div>
</div>
</body>
</html>
五、 总结
本篇文章介绍的通信方式共有5种,其中原生调用js有2种,js调用原生有3种。
整个项目的源码已经放到码云上了,点我跳转到码云。
你可以将源码下载下来,在AS上跑一遍。