通常我们在自己开发的 APP 中打开网页无非两种方法: 一是跳转到系统自带的浏览器,二是使用 WebView 控件加载页面。使用 WebView 控件的好处就是可以通过各种 api 接口来定制各种行为,常用的几个设置地方为 WebSettings、JavaScriptInterface、WebViewClient 和 WebChromeClient。平时出现的问题都可以通过修改这些设置来解决。
使用了 WebView 还是跳转到了系统自带的浏览器?
很简单的解决方法,为你的 webview 设置一个新的 WebViewClient。1
2
3
4
5
6
7webView.setWebViewClient(new WebViewClient(){
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
1 | // 或者直接添加,效果是一样的 |
获取网页的标题和图标
通过 WebChromeClient 可以获取到这些信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14webView.setWebChromeClient(new WebChromeClient() {
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
setTitle(title);
}
public void onReceivedIcon(WebView view, Bitmap icon) {
super.onReceivedIcon(view, icon);
setIcon(icon);
}
});
但是,这里有个问题,当通过 webView.goBack()
方式返回上一级Web页面的时候不会触发这个方法,因此会导致标题无法跟随历史记录返回上一级页面。所以需要在 onPageFinished()
中对界面标题重新设置。1
2
3
4
5
6
7webView.setWebViewClient(new WebViewClient(){
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
setTitle(String.valueOf(view.getTitle()));
}
});
返回键实现网页的后退键
在 WebView 中可以通过 goBack() 方法后退到历史记录的上一项。1
2
3
4
5
6
7
8// 在 Actvity 中监听返回键按钮
public void onBackPressed() {
if (webView.canGoBack())
webView.goBack();
else
super.onBackPressed();
}
设置 WebView 的 header
在 WebView 的 loadUrl()
方法中传入 Header 参数即可。1
2
3
4
5
6
7public void loadURLWithHTTPHeaders() {
final String url = "http://cpacm.net";
WebView webView = new WebView(getActivity());
Map<String,String> extraHeaders = new HashMap<String, String>();
extraHeaders.put("Referer", "http://www.google.com");
webView.loadUrl(url, extraHeaders);
}
设置 WebView 的 User-Agent
不要试图在 Header 里面去修改,而是在 WebSettings 修改1
webView.getSettings().setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
如何设置 WebView 的缓存
当需要本地缓存网页的时候就需要打开 WebViewSettings 的缓存开关,这样子当下次进到该页面无网络的情况下也能打开页面。
1 | WebSettings settings = webView.getSettings(); |
无法下载文件?
在自己写的 WebView 下是无法直接下载文件,需要自己监听下载事件并对下载的动作进行处理。1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 当下载文件时打开系统自带的浏览器进行下载,当然也可以对捕获到的 url 进行处理在应用内下载。
**/
webView.setDownloadListener(new FileDownLoadListener());
private class FileDownLoadListener implements DownloadListener {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
}
}
无法打开文件选择器?
通过重写 WebChromeClient
来实现点击 <input type='file'>
来打开系统文件选择器。
一个完整的Activity示例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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144public class MainActivity extends AppCompatActivity {
/** Android 5.0以下版本的文件选择回调 */
protected ValueCallback<Uri> mFileUploadCallbackFirst;
/** Android 5.0及以上版本的文件选择回调 */
protected ValueCallback<Uri[]> mFileUploadCallbackSecond;
protected static final int REQUEST_CODE_FILE_PICKER = 51426;
protected String mUploadableFileTypes = "image/*";
private WebView mWebView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initWebView();
}
private void initWebView() {
mWebView = (WebView) findViewById(R.id.my_webview);
mWebView.loadUrl("file:///android_asset/index.html");
mWebView.setWebChromeClient(new OpenFileChromeClient());
}
private class OpenFileChromeClient extends WebChromeClient {
// Android 2.2 (API level 8)到Android 2.3 (API level 10)版本选择文件时会触发该隐藏方法
"unused") (
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, null);
}
// Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本选择文件时会触发,该方法为隐藏方法
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
openFileChooser(uploadMsg, acceptType, null);
}
// Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本选择文件时会触发,该方法为隐藏方法
"unused") (
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
openFileInput(uploadMsg, null, false);
}
// Android 5.0 (API level 21)以上版本会触发该方法,该方法为公开方法
"all") (
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
if (Build.VERSION.SDK_INT >= 21) {
final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;//是否支持多选
openFileInput(null, filePathCallback, allowMultiple);
return true;
}
else {
return false;
}
}
}
"NewApi") (
protected void openFileInput(final ValueCallback<Uri> fileUploadCallbackFirst, final ValueCallback<Uri[]> fileUploadCallbackSecond, final boolean allowMultiple) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
}
mFileUploadCallbackFirst = fileUploadCallbackFirst;
//Android 5.0及以上版本
if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
}
mFileUploadCallbackSecond = fileUploadCallbackSecond;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
if (allowMultiple) {
if (Build.VERSION.SDK_INT >= 18) {
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
}
i.setType(mUploadableFileTypes);
startActivityForResult(Intent.createChooser(i, "选择文件"), REQUEST_CODE_FILE_PICKER);
}
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
if (requestCode == REQUEST_CODE_FILE_PICKER) {
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
//Android 5.0以下版本
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(intent.getData());
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {//Android 5.0及以上版本
Uri[] dataUris = null;
try {
if (intent.getDataString() != null) {
dataUris = new Uri[] { Uri.parse(intent.getDataString()) };
}
else {
if (Build.VERSION.SDK_INT >= 16) {
if (intent.getClipData() != null) {
final int numSelectedFiles = intent.getClipData().getItemCount();
dataUris = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
dataUris[i] = intent.getClipData().getItemAt(i).getUri();
}
}
}
}
}
catch (Exception ignored) { }
mFileUploadCallbackSecond.onReceiveValue(dataUris);
mFileUploadCallbackSecond = null;
}
}
}
else {
//这里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系统版本下分别持有了
//WebView对象,在用户取消文件选择器的情况下,需给onReceiveValue传null返回值
//否则WebView在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
}
}
怎么为 WebView 的加载添加进度条
这里的 onPageFinished()
有个问题,不能在这里监听页面是否加载完毕(我自己测试的时候,好像在重定向和加载完 iframes 时都会调用这个方法)。
把页面加载完毕的判断放在 onProgressChanged()
里可能会更为准确。
1 | webView.setWebChromeClient(new WebChromeClient() { |
怎样对页面进行 Js 注入?
首先你要在 WebView 开启 JavaScript,然后搭建桥梁1
2
3
4
5
6
7
8
9
10
11WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new WebAppBridge(new WebAppBridge.OauthLoginImpl() {
public void getResult(String s) {
//TODO
}
}),
"oauth");
webView.loadUrl("javascript:" + getAssetsJs("autologin.js"));
webView.loadUrl("javascript:adduplistener()");
WebAppBridge的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class WebAppBridge {
private OauthLoginImpl oauthLogin;
public WebAppBridge(OauthLoginImpl oauthLogin) {
this.oauthLogin = oauthLogin;
}
public void getResult(String str) {
if (oauthLogin != null)
oauthLogin.getResult(str);
}
public interface OauthLoginImpl {
void getResult(String s);
}
}
简单的说就是向网页注入一段 js, 在这段 js 里面设置回调到java中的方法 getResult()
,由 WebAppBridge.getResult 来回收。
其中js的核心代码为1
oauth.getResult(str);
其中 oauth 这个名称要与 webView.addJavascriptInterface()
方法的第二个参数一样。
具体的代码可以参考这个项目中写的 js 注入逻辑 OauthDialog
如何手动添加 Cookie
需要获得 CookieManager 的对象并将 cookie 设置进去。
从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同,请自行获取。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* 将cookie设置到 WebView
* @param url 要加载的 url
* @param cookie 要同步的 cookie
*/
public static void syncCookie(String url,String cookie) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(context);
}
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
/**
* cookie 设置形式
* cookieManager.setCookie(url, "key=value;" + "domain=[your domain];path=/;")
**/
cookieManager.setCookie(url, cookie);
}
删除 Cookie 的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 这个两个在 API level 21 被抛弃
* CookieManager.getInstance().removeSessionCookie();
* CookieManager.getInstance().removeAllCookie();
*
* 推荐使用这两个, level 21 新加的
* CookieManager.getInstance().removeSessionCookies();
* CookieManager.getInstance().removeAllCookies();
**/
public static void removeCookies() {
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.flush();
} else {
CookieSyncManager.createInstance(Application.getInstance());
CookieSyncManager.getInstance().sync();
}
}
如何使 HTML5 video 在 WebView 全屏显示
当网页全屏播放视频时会调用 WebChromeClient.onShowCustomView()
方法,所以可以通过将 video 播放的视图全屏达到目的。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
public void onShowCustomView(View view, CustomViewCallback callback) {
if (view instanceof FrameLayout && fullScreenView != null) {
// A video wants to be shown
this.videoViewContainer = (FrameLayout) view;
this.videoViewCallback = callback;
fullScreenView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
fullScreenView.setVisibility(View.VISIBLE);
isVideoFullscreen = true;
}
}
public void onHideCustomView() {
if (isVideoFullscreen && fullScreenView != null) {
// Hide the video view, remove it, and show the non-video view
fullScreenView.setVisibility(View.INVISIBLE);
fullScreenView.removeView(videoViewContainer);
// Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) {
videoViewCallback.onCustomViewHidden();
}
isVideoFullscreen = false;
videoViewContainer = null;
videoViewCallback = null;
}
}
但是很多的手机版本在网页视频播放时是不会调用这个方法的,所以这个方法局限性很大。
Android5.0上 WebView中Http和Https混合问题
1 | /** |
如何避免 WebView 的内存泄露问题
- 可以将 Webview 的 Activity 新起一个进程,结束的时候直接System.exit(0);退出当前进程;
- 不在xml中定义 WebView,而是在代码中创建,使用 getApplicationgContext() 作为传递的 Conetext;
- 在 Activity 销毁的时候,将 WebView 置空
1
2
3
4
5
6
7
8
9
10
11
protected void onDestroy() {
if (webView != null) {
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
webView.clearHistory();
((ViewGroup) webView.getParent()).removeView(webView);
webView.destroy();
webView = null;
}
super.onDestroy();
}
总结
如果你踩到了 WebView 上的坑,请先默哀一分钟,然后努力找找解决方法吧,总会有人体验过你的悲剧,也会有人重蹈你的覆辙。
当然 WebView 里肯定不止我上面列出来的这些问题,如果你有更多的 WebView 问题解决方案欢迎评论交流。