前言:
我们在开发Android应用的时候,很多的时候需要跟网页打交道,比如我们现在在做一个新闻App,在app首页会有一个轮询的广告位,我们点击其中一项会跳转至一个Web网页,在这个网页里可能会有几个条目的新闻信息,在用户点击某条新闻后,我们希望跳出Web页面展示新闻的详情,这个时候就需要用到Js调用Java代码来实现了。如果我们需要在Web页中进行分享可能也需要用JavaScrip调用Java代码来执行分享的操作。
类似这样的场景很多,如果我们需要实现这种功能,就要了解java和js之间的交互的方法。本篇我们要学习的就是通过:HTML -> JS -> Java 来完成Web端与Android客户端的友好沟通,并在最后给出 Android 4.4 以后 WebView 需要注意的一些事项。
本节例程下载地址:WillFlow_WebViewJS
一、WebView 和 JavaScrip 交互基础
如果我们想要执行Js,那么必须满足两个条件,第一个是在设置中开启JavaScript支持,即需要调用 setJavascriptEnabled(true),第二个是需要设置WebChromeClient,两者缺一不可。
(一)核心步骤
- 暴露数据
首先定义一个类,定义将要暴露出来的方法,JavaScrip 通过调用该类中的方法来实现和 Android 客户端的交互。 - 参数设置
接着在 WebView 所在页面 Activity 使用下述代码设置暴露的接口:
webview.getSettings().setJavaScriptEnabled(true);
webview.addJavascriptInterface(object,"name");
- JavaScrip 调用 Android
最后在 JavaScrip 或者 html 中通过 “name.xxx” 调用对象里暴露的方法:
(二)举三个栗子
(1)HTML 通过 JavaScrip 显示 Toast 与 普通列表的对话框
主要实现过程:通过加载本地的 HTML 文件(里面有JavaScrip脚本),实现 Android 本地方法和 JavaScrip 中的交互。所以先准备好我们的 HTML 文件,创建好后放到 Assets 目录下:
- demo1.html
Js调用Android
然后我们自定义一个 Object 对象,JavaScript 通过该类暴露的方法来调用 Android 中的方法:
- MyObject.java
public class MyObject {
private Context context;
public MyObject(Context context) {
this.context = context;
}
mWebView = (WebView) findViewById(R.id.webView);
// 将显示Toast和对话框的方法暴露给JavaScript脚本调用
public void showToast(String name) {
Toast.makeText(context, name, Toast.LENGTH_SHORT).show();
}
public void showDialog() {
new AlertDialog.Builder(context)
.setTitle("联系人列表").setIcon(R.mipmap.ic_lion_icon)
.setItems(new String[]{"小A", "小B", "小C", "小D"}, null)
.setPositiveButton("确定", null).create().show();
}
}
最后是在 MainActivity.java 中启用 JavaScript 支持:
- MainActivity.java
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webView);
WebSettings webSettings = mWebView.getSettings();
// 设置WebView允许调用Js
webSettings.setJavaScriptEnabled(true);
webSettings.setDefaultTextEncodingName("UTF-8");
// 通过调用addjavascriptInterface将object对象暴露给Js
mWebView.addJavascriptInterface(new MyObject(MainActivity.this), "myObj");
mWebView.loadUrl("file:///android_asset/demo1.html");
}
}
- 显示效果:
(2)HTML通过JS调用三种不同的对话框
主要实现过程同上,先往assets目录下放一个我们写好的html文件:
- demo2.html
测试Js的三种不同对话框
三种对话框的使用
Alert对话框
Confirm对话框
Prompt对话框
- MainActivity.java
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webView);
// 获得WebSettings对象,支持Js脚本、可访问文件、支持缩放、UTF-8编码方式
WebSettings webSettings = mWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccess(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDefaultTextEncodingName("UTF-8");
// 设置WebChromeClient,处理网页中的各种js事件
mWebView.setWebChromeClient(new MyWebChromeClient());
mWebView.loadUrl("file:///android_asset/demo2.html");
}
// 这里需要自定义一个类实现WebChromeClient类,并重写三种不同对话框的处理方法
// 分别重写onJsAlert,onJsConfirm,onJsPrompt方法,分别对应于Js中的alert()、confirm()、prompt()方法的回调
class MyWebChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
// 创建一个Builder来显示网页中的对话框
new Builder(MainActivity.this).setTitle("Alert对话框").setMessage(message)
.setPositiveButton("确定", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
}).setCancelable(false).show();
return true;
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
new Builder(MainActivity.this).setTitle("Confirm对话框").setMessage(message)
.setPositiveButton("确定", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
}).setCancelable(false).show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, final JsPromptResult result) {
// 获得一个LayoutInflater对象factory,加载指定布局成相应对象
final LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
final View myView = inflater.inflate(R.layout.prompt_view, null);
// 设置TextView对应网页中的提示信息,edit设置来自于网页的默认文字
((TextView) myView.findViewById(R.id.text)).setText(message);
((EditText) myView.findViewById(R.id.edit)).setText(defaultValue);
// 定义对话框上的确定按钮
new Builder(MainActivity.this).setTitle("Prompt对话框").setView(myView)
.setPositiveButton("确定", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 单击确定后取得输入的值,传给网页处理
String value = ((EditText) myView.findViewById(R.id.edit)).getText().toString();
result.confirm(value);
}
})
.setNegativeButton("取消", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
}).show();
return true;
}
}
}
- 运行效果:
(3)HTML通过JS读取Android联系人并显示
实现功能:该代码实现的是通过js读取Android手机中联系列表,然后显示到HTML中,当我们点击某个电话号码时,会直接跳转到拨号页面。
实现关键:利用onLoad()在网页加载的时候加载相应的js脚本,而js脚本中定义的一个函数是取出传递过来的对象,获取里面的数据,通过for循环遍历取出数据并展示!
首先往assets文件夹下编写要给demo3.html文件,内容如下:
- demo3.html
显示获取的联系人列表
用户id
姓名
号码
- 数据类Contact.java
public class Contact {
private String id;
private String name;
private String phone;
public Contact(){}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Contact{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
- MainActivity.java
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@SuppressLint("JavascriptInterface")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 设置WebView的相关设置,依次是:
// 支持js,不保存表单,不保存密码,不支持缩放
// 同时绑定Java对象
mWebView = (WebView) findViewById(R.id.mWebView);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setSaveFormData(false);
mWebView.getSettings().setSavePassword(false);
mWebView.getSettings().setSupportZoom(false);
mWebView.getSettings().setDefaultTextEncodingName("UTF-8");
mWebView.addJavascriptInterface(new SharpJS(), "sharp");
mWebView.loadUrl("file:///android_asset/demo3.html");
}
// 自定义一个Js的业务类,传递给JS的对象就是这个,调用时直接javascript:sharp.contactlist()
public class SharpJS {
public void contactlist() {
try {
Log.i(TAG, "contactlist()方法执行了!");
String json = buildJson(getContacts());
mWebView.loadUrl("javascript:show('" + json + "')");
} catch (Exception e) {
Log.e(TAG, "设置数据失败 : " + e);
}
}
public void call(String phone) {
Log.i(TAG, "call()方法执行了!");
Intent it = new Intent(Intent.ACTION_CALL, Uri.parse("tel : " + phone));
startActivity(it);
}
}
// 将获取到的联系人集合写入到JsonObject对象中,再添加到JsonArray数组中
public String buildJson(List contacts)throws Exception
{
JSONArray array = new JSONArray();
for(Contact contact:contacts)
{
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", contact.getId());
jsonObject.put("name", contact.getName());
jsonObject.put("phone", contact.getPhone());
array.put(jsonObject);
}
return array.toString();
}
// 定义一个获取联系人的方法,返回的是List的数据
public List getContacts()
{
List Contacts = new ArrayList();
// 查询raw_contacts表获得联系人的id
ContentResolver resolver = getContentResolver();
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
// 查询联系人数据
Cursor cursor = resolver.query(uri, null, null, null, null);
while(cursor.moveToNext())
{
Contact contact = new Contact();
// 获取联系人姓名,手机号码
contact.setId(cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID)));
contact.setName(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)));
contact.setPhone(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
Contacts.add(contact);
}
cursor.close();
return Contacts;
}
}
- 运行效果:
好的,就是那么简单,但是上述代码在有些手机上是执行不了的,这取决于你手机安卓系统的版本,因为在Android4.4以后addJavascriptInterface()就不可以用了,至于为什么,请往下看。
二、Android 4.4 后 WebView 的一些注意事项
Android 4.4 开始,Android中的WebView不再是基于WebKit的,而是开始基于Chromium,这个改变使得WebView的性能大幅提升,并且对HTML5,CSS,JavaScript有了更好的支持!
虽然chromium完全取代了以前的WebKit,但WebView的API接口并没有变并且与老的版本完全兼容。这样带来的好处是以前基于WebView构建的APP,现在无需做任何修改就能享受chromium内核的高效与强大。不过对于Android 4.4后的WebView,我们还是需要注意下面的这些问题:
(1)多线程
如果你在子线程而不是在UI线程中调用了 WebView 的相关方法,那么可能会带来无法预料的错误。 所以,当你的程序中需要用到多线程时候,也请使用 runOnUiThread() 方法来保证你关于 WebView 的操作是在UI线程中进行的:
runOnUiThread(newRunnable(){
@Override
publicvoid run(){
// Code for WebView goes here
}
});
(2)线程阻塞
永远不要阻塞UI线程,这是开发Android程序的一个真理。虽然是真理,我们却往往不自觉的 犯一些错误违背它,一个开发中常犯的错误就是:在UI线程中去等待JavaScript 的回调。 例如:
// This code is BAD and will block the UI thread
webView.loadUrl("javascript:fn()");
while(result == null) {
Thread.sleep(100);
}
千万不要这样做,Android 4.4中,提供了新的Api来做这件事情,evaluateJavascript() 就是专门来异步执行JavaScript代码的。
- evaluateJavascript() 方法
专门用于异步调用JavaScript方法,并且能够得到一个回调结果。
mWebView.evaluateJavascript(script, new ValueCallback() {
@Override
public void onReceiveValue(String value) {
// TO DO ...
}
});
(3)处理WebView中url的跳转
新版 WebView 对于自定义scheme的url跳转,新增了更为严格的限制条件,当我们实现了 shouldOverrideUrlLoading() 或 shouldInterceptRequest() 回调的时候,WebView 也只会在跳转url是合法Url的时候才会跳转。例如,如果你使用这样一个url :Show Profile
,shouldOverrideUrlLoading() 将不会被调用。
正确的使用方式是:
Show Profile
对应的检测Url跳转的方式:
// The URL scheme should be non-hierarchical (no trailing slashes)
private static final String APP_SCHEME = "example-app:";
@Override
publicboolean shouldOverrideUrlLoading(WebView view,String url){
if(url.startsWith(APP_SCHEME)){
urlData = URLDecoder.decode(url.substring(APP_SCHEME.length()), "UTF-8");
respondToData(urlData);
return true;
}
return false;
}
也可以这样使用:
webView.loadDataWithBaseURL("example-app://example.co.uk/", HTML_DATA,null,"UTF-8",null);
(4)UserAgent 变化
UserAgent 是什么?
User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。它是一个特殊字符串头,是一种向访问网站提供你所使用的** 浏览器类型及版本、操作系统及版本、浏览器内核 **等信息的标识。UserAgent 有什么用?
通过这个标识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计。例如:用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的UA来判断的。UserAgent 如何获取?
如果你的App对应的服务端程序,会根据客户端传来的UserAgent来做不同的事情,那么你需要注意 的是,新版本的WebView中,UserAgent有了些微妙的改变:
Mozilla/5.0 (Linux; Android 4.4; Nexus 4 Build/KRT16H)
AppleWebKit/537.36(KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0
Mobile Safari/537.36
使用getDefaultUserAgent()方法可以获取默认的UserAgent,也可以通过下面的方法来设置和获取自定义的 UserAgent:
mWebView.getSettings().setUserAgentString(ua);
mWebView.getSettings().getUserAgentString();
(5)使用addJavascriptInterface()的注意事项
从Android4.2开始,只有添加 @JavascriptInterface 声明的Java方法才可以被JavaScript调用, 例如:
class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
(6)读取联系人问题的解决
看完上面的,我们知道在Android4.2后,只有添加 @JavascriptInterface 声明的Java方法才可以被JavaScript调用,于是乎我们为之前的两个方法加上@JavascriptInterface。
但是,加完以后,并没有和我们的预想一样,出现我们想要的联系人列表,这是为什么呢? 我们通过查看Log发现下面这样一段信息:
I/MainActivity: contactlist()方法执行了!
W/WebView: java.lang.Throwable: A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {67a5150} called on Looper (JavaBridge, tid 2161) {2e1cd98}, FYI main Looper is Looper (main, tid 1) {67a5150})
at android.webkit.WebView.checkThread(WebView.java:2340)
at android.webkit.WebView.loadUrl(WebView.java:933)
at com.wgh.willflow_webviewjs.MainActivity$SharpJS.contactlist(MainActivity.java:157)
at org.chromium.base.SystemMessageHandler.nativeDoRunLoopOnce(Native Method)
at org.chromium.base.SystemMessageHandler.handleMessage(SystemMessageHandler.java:41)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.os.HandlerThread.run(HandlerThread.java:61)
大概的意思就是:所有的WebView方法都应该在同一个线程程中调用,而这里的contactlist方法却在 JavaBridge 线程中被调用了!所以我们要要把contactlist里的东东写到同一个线程中,比如一种解决 方法,就是下面这种:
@JavascriptInterface
public void contactlist() {
mWebView.post(new Runnable() {
@Override
public void run() {
try {
Log.i(TAG, "contactlist()方法执行了!");
String json = buildJson(getContacts());
mWebView.loadUrl("javascript:show('" + json + "')");
} catch (Exception e) {
Log.e(TAG, "设置数据失败 : " + e);
}
}
});
}
接下来运行下程序,我们的手机联系人就可以读取到了。
结语:
到此为止,我们学会了使用 WebView 和 JavaScrip 的交互基础:显示 Toast 与 普通列表的对话框、HTML通过JS调用三种不同的对话框、HTML通过JS读取Android联系人并显示;然后我们知道了Android 4.4 后 WebView 的一些注意事项:多线程、线程阻塞、处理WebView中url的跳转、UserAgent 变化、使用addJavascriptInterface()的注意事项等。
我们将会在下一篇我们来讲 WebView 更加高级的功能,比如:文件下载、缓存、处理错误码等。有志共同进步的同学请头像右侧点击“(+)”保持对我的关注,后续好文第一时间推送给你。