上一篇文章介绍了WebView与JS之间的数据交互,其实就是把字符串传来传去,这对文本格式的信息传输来说倒还凑合,倘若要传输图片信息就不管用了。所以,要想让h5网页支持从手机上传图片,还得另外想办法,当然各版本的Android系统也都提供了相应的解决办法。在Android 4.*系统上面,开发者可以重写WebChromeClient的openFileChooser函数;在Android 5.0以上的系统,开发者可以重写WebChromeClient的onShowFileChooser函数。话虽如此,可实际编码的时候,会发现并不容易,因为不但要兼容各种版本的安卓系统,而且要考虑不同操作方式下面的处理步骤。 首先是Android不同系统的适配问题,对于4.*版本要重写openFileChooser方法,对于5.0以上版本要重写onShowFileChooser方法。另外注意二者的回调方式也不一样,4.*的回调参数类型是ValueCallback<Uri>,而5.0以上的回调参数类型是ValueCallback<Uri[]>,因此要声明两个回调参数变量,分别用来保存二者各自的回调信息。相关代码如下所示:
private static ValueCallback<Uri> mUploadMessage;
private static ValueCallback<Uri[]> mUploadMessageLollipop;
private class MyWebChromeClient extends WebChromeClient {
// Android 4.*(包括4.1、4.2、4.3、4.4)
public void openFileChooser(ValueCallback<Uri> uploadMsg,
String acceptType, String capture) {
Log.d(TAG, "openFileChooser 4.*");
mUploadMessage = uploadMsg;
openSelectDialog();
}
// Android 5.0+(包括5.*、6.0、7.*、8.*)
@Override
public boolean onShowFileChooser(WebView webView,
ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.d(TAG, "openFileChooser 5.0+");
mUploadMessageLollipop = filePathCallback;
openSelectDialog();
return true;
}
}
然后就上传图片这个功能而言,既要支持从手机相册中挑选已有的图片,也要支持现场拍照并即时上传拍摄好的照片。如此一来,就不能仅仅从相册选择文件,而要弹出一个列表对话框,好让用户决定是从相册上传图片,还是当场拍照当场上传。所以接下来得同时实现这两种上传方式,示例代码如下:
private String mCameraPhotoPath = null;
private void openSelectDialog() {
// 声明相机的拍照行为
Intent photoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (photoIntent.resolveActivity(getPackageManager()) != null) {
mCameraPhotoPath = "file:" + getExternalFilesDir(Environment.DIRECTORY_PICTURES)
.toString() + "/" + DateUtil.getNowDateTime("") + ".jpg";
Log.d(TAG, "photoFile=" + mCameraPhotoPath);
photoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse(mCameraPhotoPath));
}
Intent[] intentArray = new Intent[] { photoIntent };
// 声明相册的打开行为
Intent selectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
selectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
selectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
selectionIntent.setType("image/*");
// 弹出含相机和相册在内的列表对话框
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, selectionIntent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, "请拍照或选择图片");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
startActivityForResult(Intent.createChooser(chooserIntent, "选择图片"), 1);
}
选择好图片确定后(含拍照和从相册选取),App代码进入到onActivityResult方法内部,开发者在此校验结果代码,根据图片选取形式分别获得具体的图片数据,然后区分4.*系统和5.+系统将图片传给h5页面。下面是onActivityResult方法的处理代码:
private static final int FILE_SELECT_CODE = 1;
private int mResultCode = Activity.RESULT_CANCELED;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult requestCode=" + requestCode + ", resultCode=" + resultCode);
if (requestCode != FILE_SELECT_CODE
|| (mUploadMessage == null && mUploadMessageLollipop == null)) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
mResultCode = resultCode;
Log.d(TAG, "mCameraPhotoPath=" + mCameraPhotoPath);
if (resultCode == Activity.RESULT_OK) {
uploadPhoto(resultCode, data);
}
}
private void uploadPhoto(int resultCode, Intent data) {
long fileSize = 0;
try {
String file_path = mCameraPhotoPath.replace("file:", "");
File file = new File(file_path);
fileSize = file.length();
} catch (Exception e) {
e.printStackTrace();
}
if (data != null || mCameraPhotoPath != null) {
Integer count = 1;
ClipData images = null;
try {
images = data.getClipData();
} catch (Exception e) {
e.printStackTrace();
}
if (images == null && data != null && data.getDataString() != null) {
count = data.getDataString().length();
} else if (images != null) {
count = images.getItemCount();
}
Uri[] results = new Uri[count];
// Check that the response is a good one
if (resultCode == Activity.RESULT_OK) {
Log.d(TAG, "fileSize=" + fileSize);
if (fileSize != 0) {
// If there is not data, then we may have taken a photo
if (mCameraPhotoPath != null) {
results = new Uri[] { Uri.parse(mCameraPhotoPath) };
}
} else if (data.getClipData() == null) {
results = new Uri[] { Uri.parse(data.getDataString()) };
} else {
for (int i = 0; i < images.getItemCount(); i++) {
results[i] = images.getItemAt(i).getUri();
}
}
}
// 区分不同系统分别返回上传结果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mUploadMessageLollipop.onReceiveValue(results);
mUploadMessageLollipop = null;
} else {
mUploadMessage.onReceiveValue(results[0]);
mUploadMessage = null;
}
}
}
其后还要注意,用户打开相册或者打开相机的时候,也有可能什么都不做就返回到原页面,由于这个取消选择的操作没有走完全流程,导致h5网页的回调资源没有回收,用户再去上传图片之时会发现页面不会响应了,因此开发者要在代码中手工替h5页面回收回调资源,这样下次用户才能继续上传图片。手工回收资源的办法是重写Activity的onResume函数,具体实现代码见下:
@Override
protected void onResume() {
super.onResume();
// 取消选择时需要回调onReceiveValue,否则网页会挂住,不会再响应点击事件
if (mResultCode == Activity.RESULT_CANCELED) {
try {
if (mUploadMessageLollipop != null) {
mUploadMessageLollipop.onReceiveValue(null);
}
if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
接着即可打开实际的h5页面进行图片上传测试啦,这里的h5测试网址用的是http://m.54php.cn/demo/h5_upload,测试的调用代码很简单,设置好WebView的访问地址以及浏览器对象就好了,例子代码如下所示:
WebView webView = (WebView) findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setBuiltInZoomControls(true);
webView.loadUrl("http://m.54php.cn/demo/h5_upload");
webView.setWebViewClient(new MyWebViewClient(this));
webView.setWebChromeClient(new MyWebChromeClient());
最后观察一下WebView配合上述测试网址的运行界面,先看看Android4.4手机的测试画面,下面的左图为打开测试网址的初始界面,右图为点击上传按钮后在屏幕中央弹出选择对话框:
先在对话框中选择从相册上传,成功上传图片后的h5页面如下面的左图所示;重新点击上传按钮,这次选择使用相机拍照,并把照片成功上传后的h5页面如下面的右图所示:
再来看看Android6.0手机的测试画面,下面的左图为打开测试网址的初始界面,右图为点击上传按钮后在屏幕下方弹出选择对话框:
先在对话框中选择从相册上传,成功上传图片后的h5页面如下面的左图所示;重新点击上传按钮,这次选择使用相机拍照,并把照片成功上传后的h5页面如下面的右图所示: