如何优雅的使用WebView

WebView 是Android开发中经常会用到的功能,是一个基于webkit引擎,用于显示来自本地、服务器web页面的控件,可以很好的提升应用扩展性。有以下优点:

  • 可以直接显示和渲染web页面
  • webview可以直接用html文件(网络上或本地assets中)作布局
  • 和JavaScript交互调用

基本使用方法在 安卓开发文档 中已经做了相关的详细介绍,这里主要根据日常开发中需求,整理了以下3个方面。

一、处理web页面中的图片点击事件

在应用中使用 WebView 加载网页进行显示的时候,多数情况下都要进行内容适配,这会导致图片进行一定比例的缩小,在手机上看起来模糊,影响用户体验。我们可以提取出网页中的关键图片添加点击事件,然后对图片进行自定义处理,比如:放大、旋转等。

1.定义JavaScript接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JavaScriptInterface {

private Context context;

JavaScriptInterface(Context context) {
this.context = context;
}

//点击图片的回调方法
//必须添加JavascriptInterface注解,否则无法响应
@JavascriptInterface
public void openImage(String img) {
Intent intent = new Intent();
//这里获取到的是图片地址
intent.putExtra("image", img);
//跳转到图片处理页面
intent.setClass(context, ImageActivity.class);
context.startActivity(intent);
}
}

2.在WebView中添加JavascriptInterface

1
2
JavaScriptInterface javascriptInterface = new JavaScriptInterface(context);
webview.addJavascriptInterface(javascriptInterface, "imagelistener");

3.新建图片遍历和处理的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void addImageClickListener(WebView view) {
// 遍历所有的img节点,添加onclick函数,函数的功能是在图片点击的时候调用本地java接口并传递url过去
view.loadUrl("javascript:(function(){" +
"var objs = document.getElementsByTagName(\"img\"); " +
"for(var i=0;i<objs.length;i++) " +
"{"
+ " objs[i].onclick=function() " +
" { "
+ " window.imagelistner.openImage(this.src); " +
" } " +
"}" +
"})()");
}

//循环遍历接收到的数据,提取出带有”img”标签的内容,设置它的宽度占屏幕的100%,图片自适应
private static void imgReset(WebView view) {
view.loadUrl("javascript:(function(){" +
"var objs = document.getElementsByTagName('img'); " +
"for(var i=0;i<objs.length;i++) " +
"{"
+ "var img = objs[i]; "
+ " img.style.maxWidth = '100%'; "
+ " img.style.height = 'auto'; "
+ "}" +
"})()");
}

4.设置WebViewClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
webview.setWebViewClient(new WebViewClient() {
//复写shouldOverrideUrlLoading()方法,设置打开网页时不调用系统浏览器, 直接在WebView中显示
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// html加载完成之后,添加监听图片的点击js函数
addImageClickListner(view);
imgReset(view);
}
});
webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);

至此, WebView 加载的网页中,图片点击事件处理已经完成。需要注意的是,使用此方法来允许JavaScript控制主机应用程序。这是一个强大的功能,但也为面向JELLY_BEAN或更早版本的应用程序提供安全风险。如果应用程序在运行Android 4.2的设备上运行,那么定位比JELLY_BEAN更晚的版本的应用程序仍然存在漏洞。使用此方法的最安全的方法是将JELLY_BEAN_MR1作为目标,并确保只有在Android 4.2或更高版本上运行时才会调用该方法。

使用这些较旧的版本,JavaScript可以使用反射来访问注入对象的公共字段。在包含非信任内容的 WebView 中使用此方法可能允许攻击者以非预期的方式操纵主机应用程序,并使用主机应用程序的权限执行Java代码。在可能包含不可信内容的 WebView 中使用此方法时要格外小心。

二、实现web文件下载功能

WebView 控制网页在应用中进行显示的时候,如果碰到下载链接,点击将不会有任何效果,这是因为默认状态下 WebView 没有开启文件下载的功能,如果要实现文件下载的功能,需要设置WebView的DownloadListener,自己实现DownloadListener的接口来达到文件的下载。

1
2
3
4
5
6
public interface DownloadListener {

public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype, long contentLength);

}
参数类型释义
urlString要下载的内容的完整网址
userAgentString用于下载的用户代理
contentDispositionString配置http头部信息(如果有的话)
mimetypeString从服务器获知的内容的MIME类型
contentLengthlong从服务器获知的文件大小

1.实现DownloadListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyDownLoadListener implements DownloadListener {

private Context mContext;

WebViewDownLoadListener(Context mContext) {
this.mContext = mContext;
}

@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
//这里进行文件下载的处理
//1、先判断手机是否有SD卡用于存储文件
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//如果没有则提醒用户
return;
}
//2、通过url进行文件下载,这里使用的是AsyncTask
DownloaderTask task = new DownloaderTask();
task.execute(url);
}

2.新建DownloaderTask 继承AsyncTask并重写其方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
private class DownloaderTask extends AsyncTask<String, Void, String> {
//文件存储路径(自定义)
String dpath = AppConfig.attach_path;

DownloaderTask() {

}

@Override
protected String doInBackground(String... params) {
String url = params[0];
String[] tmpname = url.split("/");
String fileName = tmpname[4] + "_" + url.substring(url.lastIndexOf("/") + 1);
try {
fileName = URLDecoder.decode(fileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File file = new File(dpath, fileName);
if (file.exists()) {
Log.i("tag", "The file has already exists.");
return fileName;
}
try {
URL realUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection)realUrl.openConnection();
if (conn.getResponseCode() == 200) {
//获取文件流,并写入SD卡
InputStream input = conn.getInputStream();
writeToSDCard(fileName, input);
input.close();
return fileName;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

@Override
protected void onCancelled() {
super.onCancelled();
}

@Override
protected void onPostExecute(String result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
if (result == null) {
Toast t = Toast.makeText(mContext, "连接错误!请稍后再试!", Toast.LENGTH_LONG);
t.setGravity(Gravity.CENTER, 0, 0);
t.show();
return;
}
Toast t = Toast.makeText(mContext, "已保存到SD卡。", Toast.LENGTH_LONG);
t.setGravity(Gravity.CENTER, 0, 0);
t.show();

File file = new File(dpath, result);
Log.i("tag", "Path=" + file.getAbsolutePath());
//获取intent跳转,打开本地文件
Intent intent = getFileIntent(file);

//捕获异常并处理掉,防止APP崩溃
try {
mContext.startActivity(intent);
} catch (Exception e) {
Toast.makeText(mContext, "没有找到可匹配的程序", Toast.LENGTH_SHORT).show();
}
}

@Override
protected void onPreExecute() {
// TODO Auto-generated method stub
super.onPreExecute();
}

@Override
protected void onProgressUpdate(Void... values) {
// TODO Auto-generated method stub
super.onProgressUpdate(values);
}
}

3.写入文件到本地和打开文件的方法

3.1 写入文件到本地
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void writeToSDCard(String fileName, InputStream input) {

if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File file = new File(AppConfig.attach_path, fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
byte[] b = new byte[2048];
int j = 0;
while ((j = input.read(b)) != -1) {
fos.write(b, 0, j);
}
fos.flush();
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
Log.i("tag", "NO SDCard.");
}
}
3.2 打开文件

请注意Android7.0以后权限机制进行了调整,为了提高文件访问的安全性,限制了应用通过file://方式直接对外提供文件路径的功能,向应用外部提供这种uri会直接导致应用崩溃。替代方案是使用FileProvider通过content://方式提供这些uri,并进行临时访问授权,对应的目录要在xml中注册才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 private Intent getFileIntent(File file) {
Uri uri = Uri.fromFile(file);
String type = getMIMEType(file);
Log.i("tag", "type=" + type);
//判断是否是Android N(7.0)以及更高版本
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory("android.intent.category.DEFAULT");
if (Build.VERSION.SDK_INT >= 24) {
//关于FileProvider的相关配置,这里就不做介绍
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri contentUri = FileProvider.getUriForFile(mContext, "XXX.YYYY.ZZ.fileProvider", file);
intent.setDataAndType(contentUri, type);
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, type);
}
return intent;
}

//获取文件类型
private String getMIMEType(File f) {
String type = "";
String fName = f.getName();
/* 取得扩展名 */
String end = fName.substring(fName.lastIndexOf(".") + 1, fName.length()).toLowerCase();
/* 依扩展名的类型决定MimeType */
switch (end) {
case "pdf":
type = "application/pdf";
break;
case "m4a":
case "mp3":
case "mid":
case "xmf":
case "ogg":
case "wav":
type = "audio/*";
break;
case "3gp":
case "mp4":
type = "video/*";
break;
case "jpg":
case "gif":
case "png":
case "jpeg":
case "bmp":
type = "image/*";
break;
case "apk":
/* android.permission.INSTALL_PACKAGES */
type = "application/vnd.android.package-archive";
break;
case "pptx":
case "ppt":
type = "application/vnd.ms-powerpoint";
break;
case "docx":
case "doc":
type = "application/msword";
break;
case "xlsx":
case "xls":
type = "application/vnd.ms-excel";
break;
default:
/*如果无法直接打开,就跳出软件列表给用户选择 */
type = "*/*";
break;
}
return type;
}

三、处理web页面的弹框

每个web页面都会有自己的逻辑,其中涉及到弹框的处理,默认情况下,Android WebView是不支持 js 的Alert(),Confirm(),Prompt()函数的弹出提示框,需要我们设置WebChromeClient 对象来完成。这里我们可以分为以下两种情况来讨论。

1.不对弹框进行任何处理

这种很简单,直接设置就行

1
webView.setWebChromeClient(new WebChromeClient());

2.处理弹框样式

重写WebChromeClient的下列方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//警告框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
// 这里处理交互逻辑
// 如果客户端返回true,WebView将假定客户端将处理对话框。如果客户端返回false,它将继续执行
return false;
}
//确认对话框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
//提示对话框
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
//告诉客户端显示一个对话框来确认离开当前页面的导航。这是onbeforeunload javascript事件的结果。
//如果客户端返回true,WebView将假定客户端将处理确认对话框并调用相应的JsResult方法。
//如果客户端返回false,则默认值true将返回到javascript以接受离开当前页面的导航。
//默认行为是返回false。将JsResult设置为true将离开当前页面,false将取消导航。
@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}