目前HTML5发展快速,很多native app都会适当地嵌入网页,以此来适应多变的需求变化。但是android的WebView默认支持的功能很弱,很多都需要自定义,才能实现我们想要的效果。并且WebView在不同的版本下,均有不同程度的bug。总结通常使用WebView需要注意的地方如下:
1.重写WebViewClient
protected ProgressBar mProgressBar;
private RelativeLayout mLayoutNetError; //显示网络错误
private Button mBtnRetry; //重新刷新
private boolean mInited = false; //是否网页初始化完成
private boolean mNetworkError = false; //是否网络错误的标记
protected String mEntryUrl; //刚进入时打开的url
private ValueCallback<Uri> mUploadMessage; //上传文件时用
/**
/**
-keep public class android.net.http.SslError
-keep public class android.webkit.WebViewClient
-dontwarn android.webkit.WebView
-dontwarn android.net.http.SslError
public *;
* Web View Client
*
* @author houjinyun
* @date 2013-11-18 下午12:44:28
*/
public class CustomWebViewClient extends WebViewClient {
private static final String TAG = "CustomWebViewClient";
//网页开始加载,在这里可以进行加载动画等等
@Override
public void
onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mProgressBar.setVisibility(View.VISIBLE);
}
@Override
public void
onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mProgressBar.setVisibility(View.GONE);
if(!mInited && !mNetworkError) {
mInited = true;
}
}
/**
* SSL接收过程发生错误
*/
@Override
public void
onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error) {
if(!mInited) {
mWebView.loadData("", "text/html", "utf_8");
mLayoutNetError.setVisibility(View.VISIBLE); //显示网络错误
mBtnRetry.setTag(mEntryUrl); //记录URL
mNetworkError = true;
} else {
//这样才能让WebView处理https请求,否则WebView将无法处理
handler.proceed();
}
}
/**
* 接收过程发生错误
*/
@Override
public void
onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
Log.d(TAG, description + "-- error code " + errorCode + " of " + failingUrl);
//网页加载出现错误,只处理如下几种情况。默认的WebView会直接显示出错的网络地址url,有可能暴露数据。在这种情况下,需要将网页内容置空,提示用户网络错误
if(errorCode == WebViewClient.ERROR_CONNECT || errorCode == WebViewClient.ERROR_TIMEOUT || errorCode == WebViewClient.ERROR_HOST_LOOKUP) {
mWebView.loadData("", "text/html", "utf_8");
mLayoutNetError.setVisibility(View.VISIBLE); //显示网络错误
mBtnRetry.setTag(failingUrl); //记录URL
mNetworkError = true;
}
}
//return false表示WebView自己处理,return true表示我们自己来处理该url。像“tel:10086”等等类似的url,WebView是不会处理的,需要我们自己来处理。
//通常情况下,以http、https、file(表示本地网页)开头的url,都给WebView自己来加载,其他类型的url都通过系统应用程序来打开
@Override
public boolean
shouldOverrideUrlLoading(WebView view, String url) {
if(Uri.parse(url).getScheme().startsWith("file")) {
//如果是本地加载的话,直接用当前浏览器加载
return false;
}
if(Uri.parse(url).getScheme().startsWith("http")) {
return false;
}
if ("about:blank".equals(url)){
return false; //不需要处理空白页
}
//不能识别的,启动系统程序进行加载
Intent intent = new Intent(Intent.ACTION_VIEW);
try {
intent.setData(Uri.parse(url));
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
2.WebChromeClient
* Web Chrome Client
*
* @author houjinyun
*
*/
public class CustomWebChromeClient extends WebChromeClient{
private static final String TAG = "WebChromeClient";
/**
* 控制台消息输出
*/
@Override
public void
onConsoleMessage(java.lang.String message, int lineNumber, java.lang.String sourceId){
Log.d(TAG, String.format("%s -- From line %s of %s", message, lineNumber, sourceId));
}
/**
* 控制台消息输出
*/
@Override
public boolean
onConsoleMessage(ConsoleMessage cm) {
if (cm.messageLevel() == ConsoleMessage.MessageLevel.DEBUG){
Log.d(TAG, String.format("%s -- From line %s of %s", cm.message(), cm.lineNumber(), cm.sourceId()));
}else if (cm.messageLevel() == ConsoleMessage.MessageLevel.LOG
|| cm.messageLevel() == ConsoleMessage.MessageLevel.TIP){
Log.i(TAG, String.format("%s -- From line %s of %s", cm.message(), cm.lineNumber(), cm.sourceId()));
}else if (cm.messageLevel() == ConsoleMessage.MessageLevel.WARNING){
Log.w(TAG, String.format("%s -- From line %s of %s", cm.message(), cm.lineNumber(), cm.sourceId()));
}else if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR){
Log.e(TAG, String.format("%s -- From line %s of %s", cm.message(), cm.lineNumber(), cm.sourceId()));
}
return true;
}
@Override
public boolean
onJsAlert(WebView view, String url, String message, final JsResult result){
AlertDialog.Builder builder = new AlertDialog.Builder(BaseWebActivity.this);
builder.setMessage(message);
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
builder.setCancelable(false).create().show();
return true;
}
/**
* 页面加载进度
*/
@Override
public void
onProgressChanged(android.webkit.WebView view, int newProgress){
Log.d(TAG, String.format("正在加载...(%s)", newProgress));
}
// For Android 3.0+
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
this.openFileChooser(uploadMsg, acceptType, null);
}
// For Android < 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
this.openFileChooser(uploadMsg, null, null);
}
//针对网页里的<input type="file" accept="image/*" capture="camera" >, WebView默认是不会弹出选择文件对话框的,需要重写该方法,自己来弹出选择文件对话框。
//注意SDK不同的版本会有不同的方法,需要统一处理
// For Android > 4.1.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.d("BankkaActivity", "acceptType=" + acceptType + ",capture="+capture);
mUploadMessage = uploadMsg;
if ("camera".equalsIgnoreCase(capture) && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){//有存贮卡的话,才走拍照
//拍照
takePicture4InputFile();
}else{
if (capture == null && (acceptType == null || acceptType.startsWith("image/")) && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
showMenu4InputFile(acceptType);
}else{
//文件选择
chooseFile4InputFile(acceptType);
}
}
}
}
3.openFileChooser()
针对网页里<input type="file" />来进行文件上传时,必须重写该方法。见第3点里的重写方法。
/**
* 选择文件为客户端的Input File功能
*/
private void chooseFile4InputFile(String acceptType){
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//能够返回一个Uri结果
if (CommonMethod.isEmpty(acceptType)){//接收类型
acceptType = "*/*";
//选择的文件类型,例如:image/*表示图片
}
intent.setType(acceptType);
startActivityForResult(Intent.createChooser(intent, "File Chooser"), FILECHOOSER_CHOOSERFILE);
}
/**
* 拍照为客户端的Input File功能
*/
private void takePicture4InputFile(){
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(takePictureIntent, FILECHOOSER_TAKEPICTURE);
}
/**
* 显示选择菜单为Input File功能
* @param acceptType
*/
private void showMenu4InputFile(final String acceptType){
String[] menus = {"拍照", "相册"};
Dialog dialog = new AlertDialog.Builder(this).setItems(menus, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch(which){
case 0:
takePicture4InputFile();
break;
case 1:
default:
chooseFile4InputFile(acceptType);
break;
}
}
}).create();
//这里很重要,如果弹出对话框,用户选择一个图片或者进行拍照,但是进行到一半的时候,用户cancel了,这个时候再去点击“选择文件”按钮时,网页会失去响应。
//原因是:点击“选择文件”按钮时,网页会缓存一个ValueCallback
对象,必须触发了该对象的onReceiveValue()方法,WebView才会释放,进而才能再一次的选择文件。
//当弹层被取消时,返回未选择文件
dialog.setOnCancelListener(new OnCancelListener(){
@Override
public void onCancel(DialogInterface dialog) {
mUploadMessage.onReceiveValue(null);
mUploadMessage = null;
}
});
dialog.show();
}
在onActivityResult()方法里进行回调处理:
if (requestCode == FILECHOOSER_TAKEPICTURE && this.mUploadMessage != null){
//选择文件时的拍照功能
Uri uri = null;
if (resultCode == Activity.RESULT_OK && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
FileOutputStream fos = null;
try{
File file = Environment.getExternalStorageDirectory();
file = new File(file, "51zhangdan/temp");
if (!file.exists()){
file.mkdirs();
}
file = new File(file, "filechoosertmp.png");
Bitmap bitmap = (Bitmap)intent.getExtras().get("data");
fos = new FileOutputStream(file);
bitmap.compress(CompressFormat.PNG, 100, fos);
fos.close();
fos = null;
uri = Uri.fromFile(file);
}catch(Exception e){
Log.e(TAG, String.format("处理TakePicture图片数据出错.%s", e.getMessage()));
}finally{
if (fos != null){
try {
fos.close();
} catch (IOException e) {
Log.e(TAG, String.format("关闭TakePicture图片数据出错.%s", e.getMessage()));
}
}
}
}
//这里进行回调,WebView才能获取到你选择了哪个文件
this.mUploadMessage.onReceiveValue(uri);
this.mUploadMessage = null;
}
4.DownloadListener
默认情况下,WebView是不能处理下载链接的,一个download url也是以http、https之类开头的,在shouldOverrideUrlLoading()里是不能根据url来判断它是否是一个下载链接。
//该接口可以监听到WebView里打开的下载链接,重写该接口,我们可以得到下载地址的url,进而自己来处理下载。
public class CustomDownloadListener implements android.webkit.DownloadListener {
private static final String TAG = "CustomDownloadListener";
@Override
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype, long contentLength) {
Log.d(TAG, "download:");
Log.d(TAG, url+ "");
Log.d(TAG, userAgent + "");
Log.d(TAG, contentDisposition + "");
Log.d(TAG, mimetype + "");
Log.d(TAG, "contentLength: " + contentLength);
CommonMethod.launchBrowser(BaseWebActivity.this, url);
}
}
mWebView.setDownloadListener(new CustomDownloadListener());
5.proguard混淆问题
混淆时,该WebView里的自定义方法可能会被混淆掉,出现莫名其妙的问题,导致我们的设置都不起作用。例如:
-keep class com.example.app.activities.BaseWebActivity$*{*;}
-
keepclassmembers
class com.
example
.app.activities.BaseWebActivity$*{*;}
-keep public class android.webkit.WebViewClient
-dontwarn android.webkit.WebView
-dontwarn android.net.http.SslError
-
dontwarn
android.webkit.WebViewClient
#如果有与javascript进行交互的对象,则需要如下处理方式
-keepclassmembers class com.example.app.view.ComJSInterface {public *;
}
6.onDestroy()时问题
在界面退出时,由于WebView自身的bug,可能会出现一些异常,可以做如下处理:
@Override
protected void onDestroy() {
super.onDestroy();
if(mWebView != null) {
((ViewGroup)findViewById(R.id.RelativeLayout_Content)).removeView(mWebView);
mWebView.removeAllViews();
mWebView.destroy();
mWebView = null;
}
}