Android 的 WebView 组件默认是不启用 type 为 file 的 input 的,需要在代码中做一些类似 hack 的编码(因为解决问题的目标对象的方法都是加了@hide注解的)才能召唤神龙。
目标对象:WebChromeClient
实例化一个目标对象,并重写它的几个隐藏方法(针对不同的Android系统版本,方法名和入参都不一样,所以方法有多个),然后将目标对象作为参数传递给 WebView 对象的 setWebChromeClient 方法。
目标对象隐藏方法的重写
// For Android 3.0+
public void openFileChooser( ValueCallback uploadMsg, String acceptType )...
// For Android 3.0+
public void openFileChooser(ValueCallback uploadMsg)...
//For Android 4.1
public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture)...
// For Lollipop 5.0+ Devices
public boolean onShowFileChooser(WebView mWebView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams)...
代码如下:
private WebChromeClient mWebChromeClient = new WebChromeClient(){
// For Android 3.0+
@SuppressWarnings({ "rawtypes" })
public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
vCbFileChooser = uploadMsg;
/*Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
MainActivity.this.startActivityForResult(
Intent.createChooser(i,"文件选择"),
FILECHOOSER_RESULTCODE
);*/
selPic();
}
// For Android 3.0+
@SuppressWarnings({ "unused", "rawtypes" })
public void openFileChooser(ValueCallback uploadMsg) {
openFileChooser(uploadMsg, "");
}
//For Android 4.1
@SuppressWarnings({ "unused", "rawtypes" })
public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture){
openFileChooser(uploadMsg, acceptType);
}
// For Lollipop 5.0+ Devices
@SuppressWarnings("unchecked")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(
WebView mWebView, ValueCallback<Uri[]> filePathCallback,
WebChromeClient.FileChooserParams fileChooserParams
) {
if (vCbFileChooser != null) {
vCbFileChooser.onReceiveValue(null);
vCbFileChooser = null;
}
vCbFileChooser = filePathCallback;
selPic();
return true;
}
};
在上面的代码中:
1、所有被重写的方法最后都会调用 selPic 方法,这个方法会显示一个对话框,让用户选择是拍照选取照片还是直接从已保存的文件中选取图片。
2、vCbFileChooser 变量维持着向页面回传值的 ValueCallback 对象,直到 onActivityResult。
selPic 方法实现
/**
* 弹出对话框,提示拍照或者选择照片文件
*/
@SuppressWarnings("unused")
protected final void selPic() {
if (!checkSDcard()){return;}
String[] selectPicTypeStr = { "拍照","选择照片" };
AlertDialog alertDialog = new AlertDialog.Builder(this)
.setItems(
selectPicTypeStr,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0://拍照
chkPrivBeforeTakePhoto();
break;
case 1://选择图片文件
choosePicFile();
break;
default:
break;
}
}
}
).setOnCancelListener(
new DialogInterface.OnCancelListener() {
@SuppressWarnings("unchecked")
@Override
public void onCancel(DialogInterface dialog) {
if (null != vCbFileChooser) {
vCbFileChooser.onReceiveValue(null);
vCbFileChooser = null;
}
}
}
).show();
}
上述代码:
1、chkPrivBeforeTakePhoto 方法执行拍照选取流程(之所以这样取名,是因为在拍照之前,还要考虑到Android 6.0以上版本权限系统机制的变化);
2、choosePicFile 方法执行直接从已保存文件中选取图片的流程;
3、如果两中流程都没有,而是执行了取消操作(按下返回键或者点击了界面空白处),那么 vCbFileChooser 变量也必须调用 onReceivValue 方法回传空值,保证type=file的input能反复使用。
4、checkSDcard 方法的作用是在拍照以前判断有没有存储。
/**
* 检查SD卡是否存在
*/
public final boolean checkSDcard() {
boolean flag = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
if(!flag){Toast.makeText(this, "请插入手机存储卡再使用本功能", Toast.LENGTH_SHORT).show();}
return flag;
}
chkPrivBeforeTakePhoto 方法
private static final int PERMISSIONS_REQUEST_CODE_TAKE_PHOTO = 1;
@SuppressWarnings("unchecked")
private void chkPrivBeforeTakePhoto(){
if(
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
) {
if (null != vCbFileChooser) {
vCbFileChooser.onReceiveValue(null);
vCbFileChooser = null;
}
new AlertDialog
.Builder(this)
.setTitle("提示信息")
.setMessage("该功能需要您接受应用对一些关键权限(拍照)的申请,如之前拒绝过,可到手机系统的应用管理授权设置界面再次设置。")
.setPositiveButton("确认", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, PERMISSIONS_REQUEST_CODE_TAKE_PHOTO);
}
})
.show();
} else {
chooseTakePhoto();
}
}
private void chooseTakePhoto(){
pathTakePhoto = Environment.getExternalStorageDirectory().getPath()
+ "/mbossclient/camera/temp/"
+ (System.currentTimeMillis() + ".jpg");
File vFile = new File(pathTakePhoto);
if (!vFile.exists()) {//必须确保文件夹路径存在,否则拍照后无法完成回调
File vDirPath = vFile.getParentFile();
vDirPath.mkdirs();
} else {
if (vFile.exists()) {
vFile.delete();
}
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
uriTakePhoto = Uri.fromFile(vFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uriTakePhoto);
startActivityForResult(intent, TAKEPHOTO_RESULTCODE);
}
上述代码:
1、Android 6.0 及以上版本都需要就权限进行询问操作;
2、chooseTakePhoto 方法执行实际的拍照流程;
3、TAKEPHOTO_RESULTCODE 用于在 onActivityResult 方法中识别出是执行了拍照选取的流程。
choosePicFile 方法
/**
* 选择文件
*/
private void choosePicFile(){
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
MainActivity.this.startActivityForResult(
Intent.createChooser(i,"文件选择"),
FILECHOOSER_RESULTCODE
);
}
FILECHOOSER_RESULTCODE 用于在onActivityResult方法中识别出是执行了从已保存文件中选取图片文件的流程。
onActivityResult 方法
@SuppressLint("NewApi")
@SuppressWarnings("unchecked")
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == FILECHOOSER_RESULTCODE) {//从文件选择器选择照片
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(null == vCbFileChooser) {return;}
vCbFileChooser.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
vCbFileChooser = null;
} else {
if(null == vCbFileChooser) {return;}
Uri result = (intent == null || resultCode != RESULT_OK)? null:intent.getData();
vCbFileChooser.onReceiveValue(result);
vCbFileChooser = null;
}
} else if(requestCode == TAKEPHOTO_RESULTCODE){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(null == vCbFileChooser) {return;}
if(null == uriTakePhoto) {
vCbFileChooser.onReceiveValue(null);
vCbFileChooser = null;
return;
}
addImageGallery(pathTakePhoto);
Uri[] uris = new Uri[1];
uris[0] = uriTakePhoto;
vCbFileChooser.onReceiveValue(uris);
vCbFileChooser = null;
uriTakePhoto = null;
pathTakePhoto = null;
} else {
if(null == vCbFileChooser) {return;}
if(null == uriTakePhoto) {
vCbFileChooser.onReceiveValue(null);
vCbFileChooser = null;
return;
}
addImageGallery(pathTakePhoto);
vCbFileChooser.onReceiveValue(uriTakePhoto);
vCbFileChooser = null;
uriTakePhoto = null;
pathTakePhoto = null;
}
}
super.onActivityResult(requestCode, resultCode, intent);
}
上述代码:
1、以Android Lollipop版本为届,低于该版本的系统与等于或高于该版本的系统处理方式不一样,表面上看主要是使用API获取uri数据的方法不同;
2、无论取没取到 uri 数据,只要 vCbFileChooser 变量不为空,都必须调用一次 onReceiveValue 方法,而且这之后要将它以及相关变量置为null,以保证type=file的input能反复使用。
3、addImageGallery 方法的作用是将拍照生成的图片(不是缩略图)添加到相册,保证后续还能从系统中索取到。
/**
* 解决拍照后在相册中找不到的问题
*/
private void addImageGallery(String path) {
if (null == path || "".equals(path)) {
return;
}
File file = new File(pathTakePhoto);
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}