Android Hybrid开发中,常会遇到涉及选择图片上传的需求,比如实名认证功能需要上传用户身份证照片。这次我们就一起来探究一下Hybrid开发中,通过WebView实现选择图片上传功能的技术方案。
通常前端的代码是类似下面这样的:
<input type="file" accept="image/*">
前端发起input请求之后,Android端又是怎么样接收该请求的呢?如果不对WebView作任何处理,触发前端的input事件后,页面不会有任何响应。通过对WebView的了解,发现需要开发者设置WebChromeClient并实现相应的回调接口,然后在接口中处理文件上传请求。
WebChromeClient接口
从Android 2.0发展到Android 5.0以后,Google开发者对该接口进行了多次改版,最终演变成下面这些接口方法:
webview.setWebChromeClient(new WebChromeClient() {
// For Android < 3.0
public void openFileChooser(ValueCallback<Uri> valueCallback) {
***
}
// For Android >= 3.0
public void openFileChooser(ValueCallback valueCallback, String acceptType) {
***
}
// For Android >= 4.1
public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
***
}
// For Android >= 5.0
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
***
return true;
}
});
Android 3.0以后可以从接口中获得前端传递的文件类型参数acceptType,Android 5.0以后该参数被封装在FileChooserParams类里面。所有的接口方法都会提供ValueCallback参数,顾名思义,它就是用来给我们选择完图片文件之后回调给前端的回调接口,Android 5.0以后该回调接口支持Uri数组,因此可以实现同时选择多个文件的功能。
选择图片
文件请求过来了,那么我们就可以在接口中调起选择图片的页面了。Android提供原生API调起选择图片页面:
Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivityForResult(intent, START_ACTIVITY_UPLOAD_FILE_REQUEST);
}
结合多个Android版本API,我们可以提供一个统一的处理方法:
protected void openFileInput(final ValueCallback<Uri> fileUploadCallbackFirst,
final ValueCallback<Uri[]> fileUploadCallbackSecond,
final boolean allowMultiple,
final boolean capture,
String acceptType) {
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
}
mFileUploadCallbackFirst = fileUploadCallbackFirst;
if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
}
mFileUploadCallbackSecond = fileUploadCallbackSecond;
if (!TextUtils.isEmpty(acceptType) && acceptType.startsWith("image")) {
Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple && Build.VERSION.SDK_INT >= 18);
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivityForResultSafe(intent, START_ACTIVITY_UPLOAD_FILE_REQUEST);
}
} else {
Log.w(TAG, "Please contact developers to support other files.");
// 无法处理也要回调给H5,否则H5页面无法继续响应用户
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
} else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
返回图片
从上面的代码可发现,调起选择图片页面的方式是startActivityForResult,因此图片Uri返回的处理应该在onActivityResult方法。那么针对Android 5.0以下版本,需要通过mFileUploadCallbackFirst来回调给前端,Android 5.0以上则需要通过mFileUploadCallbackSecond回调给前端,并且要注意Android 5.0以上有可能有多张图片的情况。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == START_ACTIVITY_UPLOAD_FILE_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
if (mFileUploadCallbackFirst != null) {
Uri photoUri = null;
// TODO 获取所选图片的Uri
// TODO 是否需要对图片作压缩处理?
mFileUploadCallbackFirst.onReceiveValue(photoUri);
mFileUploadCallbackFirst = null;
} else if (mFileUploadCallbackSecond != null) {
Uri[] photoUris = null;
// TODO 获取所选图片的Uri数组
// TODO 是否需要对所有图片作压缩处理?
mFileUploadCallbackSecond.onReceiveValue(photoUris);
mFileUploadCallbackSecond = null;
}
}
} else {
// 用户取消选择,也要回调给前端
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
} else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
值得关注的是,很多时候用户选择图片是不会考虑图片大小的,因此带来的性能消耗也是不确定的,我们需要设计一套图片压缩算法,为图片上传功能带来更好的体验。图片的压缩可以参考之前的文章,我们讨论过Bitmap的加载优化,可以关注一下:Android – Bitmap加载性能优化