一、简介
WebView 在 Android 平台上是一个特殊的 View, 基于 webkit 引擎、展现 web 页面的控件,这个类可以被用来在你的 app 中仅仅显示一张在线的网页,还可以用来开发浏览器。WebView 内部实现是采用渲染引擎来展示 view的内容,提供网页前进后退,网页放大,缩小,搜索。
二、常用方法1. 加载url
//方式1. 加载一个网页:
webView.loadUrl("http://www.google.com/");
//方式2:加载apk包中的html页面
webView.loadUrl("file:///android_asset/test.html");
//方式3:加载手机本地的html页面
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
// 方式4: 加载 HTML 页面的一小段内容
WebView.loadData(String data, String mimeType, String encoding)
// 参数说明:
// 参数1:需要截取展示的内容
// 内容里不能出现 ’#’, ‘%’, ‘\’ , ‘?’ 这四个字符,若出现了需用 %23, %25, %27, %3f 对应来替代,否则会出现异常
// 参数2:展示内容的类型
// 参数3:字节码
2. WebView 的状态
//激活WebView为活跃状态,能正常执行网页的响应
webView.onResume() ;
//当页面被失去焦点被切换到后台不可见状态,需要执行onPause
//通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。
webView.onPause();
//当应用程序(存在webview)被切换到后台时,这个方法不仅仅针对当前的webview而是全局的全应用程序的webview
//它会暂停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
webView.pauseTimers()
//恢复pauseTimers状态
webView.resumeTimers();
//销毁Webview
//在关闭了Activity时,如果Webview的音乐或视频,还在播放。就必须销毁Webview
//但是注意:webview调用destory时,webview仍绑定在Activity上
//这是由于自定义webview构建时传入了该Activity的context对象
//因此需要先从父容器中移除webview,然后再销毁webview:
rootLayout.removeView(webView);
webView.destroy();
3. 关于前进 / 后退网页
//是否可以后退
Webview.canGoBack()
//后退网页
Webview.goBack()
//是否可以前进
Webview.canGoForward()
//前进网页
Webview.goForward()
//以当前的index为起始点前进或者后退到历史记录中指定的steps
//如果steps为负数则为后退,正数则为前进
Webview.goBackOrForward(intsteps)
问题:在不做任何处理前提下 ,浏览网页时点击系统的“Back”键,整个 Browser 会调用 finish()而结束自身? 解决方案:
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
4. 清除缓存数据
//清除网页访问留下的缓存
//由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
Webview.clearCache(true);
//清除当前webview访问的历史记录
//只会webview访问历史记录里的所有记录除了当前访问记录
Webview.clearHistory();
//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
Webview.clearFormData();
三、实例
需求:
- 实现显示加载进度条;
- 网页中的电话号码可以拨打;
- 网页中添加图片、视频等文件。
1. 在 AndroidManifest.xml 中添加访问网络权限
2. 布局文件
3. 逻辑代码
public class ScanOrderActivity extends BaseActivity {
private ValueCallback mUploadCallbackAboveL;
private ValueCallback mUploadCallbackBelow;
private Uri imageUri;
private final int REQUEST_CODE_IMG = 10011;
private final int REQUEST_CODE_VIDEO = 10022;
@BindView(R.id.title_bar)
TitleBar mTitleBar;
@BindView(R.id.webView)
WebView mWebView;
private WebSettings mWebSettings;
private ProgressDialog dialog = null;
@Override
protected int getLayoutId() {
return R.layout.activity_add_order;
}
@Override
protected void initView() {
mTitleBar.setOnTitleBarListener(new OnTitleBarListener() {
@Override
public void onLeftClick(View view) {
finish();
}
@Override
public void onTitleClick(View view) {
}
@Override
public void onRightClick(View view) {
}
});
Bundle bundle = getIntent().getExtras();
String url = bundle.getString("newOrder");
mWebView.loadUrl(url);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("tel:")) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
startActivity(intent);
}else if (url.startsWith("http:") || url.startsWith("https:")) {
view.loadUrl(url);
}
return true;
}
});
mWebSettings = mWebView.getSettings();
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setAllowFileAccess(true);
mWebSettings.setAllowFileAccessFromFileURLs(true);
mWebSettings.setAllowUniversalAccessFromFileURLs(true);
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
mWebSettings.setBuiltInZoomControls(true);
mWebSettings.setSupportZoom(false);
mWebSettings.setUseWideViewPort(true);
mWebSettings.setLoadWithOverviewMode(true);
mWebSettings.setGeolocationEnabled(true);
mWebSettings.setDomStorageEnabled(true);
mWebView.requestFocus();
mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
//优先使用缓存
mWebSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(PermissionRequest request) {
request.grant(request.getResources());
request.getOrigin();
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (newProgress == 100) {
// 加载完毕
closeDialog(newProgress);
} else {
openDialog(newProgress);
}
super.onProgressChanged(view, newProgress);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) {
mUploadCallbackAboveL = filePathCallback;
String[] acceptTypes = fileChooserParams.getAcceptTypes();
if (acceptTypes.length > 0) {
openCamera(acceptTypes[0]);
}
return true;
}
}
);
}
private void openDialog(int newProgress) {
if (dialog == null) {
dialog = new ProgressDialog(ScanOrderActivity.this);
dialog.setTitle("正在加载");
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setProgress(newProgress);
dialog.show();
} else {
dialog.setProgress(newProgress);
}
}
private void closeDialog(int newProgress) {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
dialog = null;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_VIDEO) {
if (null != mUploadCallbackBelow) {
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
mUploadCallbackBelow.onReceiveValue(result);
mUploadCallbackBelow = null;
} else if (null != mUploadCallbackAboveL) {
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
if (result != null) {
mUploadCallbackAboveL.onReceiveValue(new Uri[]{result});
} else {
mUploadCallbackAboveL.onReceiveValue(null);
}
mUploadCallbackAboveL = null;
}
} else if (requestCode == REQUEST_CODE_IMG) {
// 经过上边(1)、(2)两个赋值操作,此处即可根据其值是否为空来决定采用哪种处理方法
if (mUploadCallbackBelow != null) {
chooseBelow(resultCode, data);
} else if (mUploadCallbackAboveL != null) {
chooseAbove(resultCode, data);
} else {
ToastUtils.showToast(ScanOrderActivity.this,"发生错误");
}
}
}
/**
* 调用相机/相册选择器弹窗
*/
private void openCamera(String accept) {
if (accept.contains("video")) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);//设置视频录制的质量,0为低质量,1为高质量。
//intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 6);//限制时长 单位秒
//intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 1024L * 1024 * 10);//指定视频最大允许的尺寸,单位为byte
//开启摄像机
startActivityForResult(intent, REQUEST_CODE_VIDEO);
} else {
// 指定拍照存储位置的方式调起相机
String filePath = Environment.getExternalStorageDirectory() + File.separator
+ Environment.DIRECTORY_PICTURES + File.separator;
String fileName = "IMG_" + DateFormat.format("yyyyMMdd_hhmmss", Calendar.getInstance(Locale.CHINA)) + ".jpg";
imageUri = Uri.fromFile(new File(filePath + fileName));
// Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// startActivityForResult(intent, REQUEST_CODE_IMG);
// 选择图片(不包括相机拍照),则不用成功后发刷新图库的广播
// Intent i = new Intent(Intent.ACTION_GET_CONTENT);
// i.addCategory(Intent.CATEGORY_OPENABLE);
// i.setType("image/*");
// startActivityForResult(Intent.createChooser(i, "Image Chooser"), REQUEST_CODE_IMG);
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
Intent photo = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
Intent chooserIntent = Intent.createChooser(photo, "Image Chooser");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[]{captureIntent});
startActivityForResult(chooserIntent, REQUEST_CODE_IMG);
}
}
/**
* Android API < 21(Android 5.0)版本的回调处理
*
* @param resultCode 选取文件或拍照的返回码
* @param data 选取文件或拍照的返回结果
*/
private void chooseBelow(int resultCode, Intent data) {
if (RESULT_OK == resultCode) {
updatePhotos();
if (data != null) {
// 这里是针对文件路径处理
Uri uri = data.getData();
if (uri != null) {
mUploadCallbackBelow.onReceiveValue(uri);
} else {
mUploadCallbackBelow.onReceiveValue(null);
}
} else {
// 以指定图像存储路径的方式调起相机,成功后返回data为空
mUploadCallbackBelow.onReceiveValue(imageUri);
}
} else {
mUploadCallbackBelow.onReceiveValue(null);
}
mUploadCallbackBelow = null;
}
/**
* Android API >= 21(Android 5.0) 版本的回调处理
*
* @param resultCode 选取文件或拍照的返回码
* @param data 选取文件或拍照的返回结果
*/
private void chooseAbove(int resultCode, Intent data) {
if (RESULT_OK == resultCode) {
updatePhotos();
if (data != null) {
// 这里是针对从文件中选图片的处理
Uri[] results;
Uri uriData = data.getData();
if (uriData != null) {
results = new Uri[]{uriData};
for (Uri uri : results) {
Log.d("TAG", "chooseAbove: "+uri.toString());
}
mUploadCallbackAboveL.onReceiveValue(results);
} else {
mUploadCallbackAboveL.onReceiveValue(null);
}
} else {
mUploadCallbackAboveL.onReceiveValue(new Uri[]{imageUri});
}
} else {
mUploadCallbackAboveL.onReceiveValue(null);
}
mUploadCallbackAboveL = null;
}
private void updatePhotos() {
// 该广播即使多发(即选取照片成功时也发送)也没有关系,只是唤醒系统刷新媒体文件
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(imageUri);
sendBroadcast(intent);
}
//销毁Webview
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
}
四、面试题
- 如何避免 WebView 内存泄露?
- 不在 xml 中定义 Webview ,而是在需要的时候在 Activity 中创建,并且 Context 使用 getApplicationgContext()。
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);
- 在 Activity 销毁( WebView )的时候,先让 WebView 加载null内容,然后移除 WebView,再销毁 WebView,最后置空。
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}