WebView中打开相机,文件选择器的问题和解决方法
近几年前端开发真是越来越火,H5页面开发的移动端页面甚至有夺我原生开发半壁江山的意思,忧伤忧伤。不过从实际情况考虑,H5一套代码到处跑的特性,我们的Android, IOS ...也就只能呵呵了。然而我还是比较喜欢原生应用,对网络质量要求低,经过H5页面加载不出来一片空白就不受得抓狂!吐槽归吐槽,正事不能落下。
上一篇意图调相机的2方式种以及那些你知道的不状语从句:知道的坑中完成了对意向调起系统相机,结果处理以及一些问题的应对。其实上篇文章还是因为今天的主题web视图中调用相机系统-而起,因为涉及到调用相机本身的一些问题之前不是很明确,所以专门搞了一下,记录下来,所以如果调用相机操作本身有什么疑问或问题,请点击跳转到上一篇电子邮件寻找答案,本篇不再重复。接下来们看看在WebView中调用相机的一些问题。
问题说明
最近有个需求是要上传身份证正反照,说来简单,可偏偏这部分业务是H5页面处理的,所以只能通过H5页面去拍照或选取本地图片了,然而问题来了 - 这段H5代码在用浏览器打开可以实现功能,但是放在WebView中却没有动作。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>相机调用</title> <script type="text/javascript">
function previewPhoto(sourceId, targetId) {
var url;
if (navigator.userAgent.indexOf("MSIE") >= 1) { // IE
url = document.getElementById(sourceId).value;
} else if(navigator.userAgent.indexOf("Firefox") > 0) { // Firefox
url = window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));
} else if(navigator.userAgent.indexOf("Chrome") > 0) { // Chrome
url = window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));
} else if(navigator.userAgent.indexOf("Opera") > 0
|| navigator.userAgent.indexOf("Oupeng") > 0) { // Oupeng
url = window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));
} else {
url = "flower_err.jpg";
} <!--window.alert("address:" + url);-->
window.alert("address:" + navigator.userAgent); var imgPre = document.getElementById(targetId);
imgPre.src = url;
}
</script>
</head>
<body>
<a href="http://www.baidu.com">去百度</a> <br><br>
<img id="img" width="200px" height="300px" alt="图片预览区">
<br>
<input type="file" id="pic" name="camera" accept="image/*" onchange="previewPhoto(this.id, 'img');"/> <br><br>
<input type="file" accept="image/*" multiple>
</body>
</html>
在浏览器中正常运行:
根据前人描述,是因为Android源码中将这部分屏蔽了,需要在webView.setWebChromeClient(new WebChromeClient())中重写WebChromeClient的openFileChooser()等方法,接下来我们就打开源码看看。
源码分析
遇到问题看源码是最直接也是最有效的办法,虽然通常情况下阅读源码比看网上一些帖子难度要大点,但却是问题的根本所在。可能有时候遇到很多问题不知道专门从源码下手,这时候就只能用问题去百度,谷歌去了,看看前辈们是怎么解决这个问题的,遇到涉及源码时再回头追本溯源,这样便会对问题本身理解深刻;久而久之,可见成效。说到这里,一个推荐用英语查看各版本源码的地址,毕竟你不会下载了所有版本的源码。闲话少叙,据说不同版本还不一样,那就一个一个看(WebChromeClient.java在\机器人\ WebKit的包下):
(Android 2.2)8 <= API <= 10(Android 2.3)
以Version 2.3.7_r1(API 10)为例(API <8时就没有这个方法):
可以看到,openFileChooser()方法用来告诉客户端打开一个文件选择器,只有一个入参ValueCallback对象uploadMsg,uploadMsg是一个回调值,用来设置待上传文件的Uri,用onReceiveValue()方法来唤醒等待线程(英语不好,莫见怪);并且该方法被隐藏了。
(Android 3.0)11 <= API <= 15(Android 4.0.3)
以Version 2.3.7_r1(API 15)为例:
可以看到,该方法也是被隐藏了;不过openFileChooser()方法比上一版多了一个字符串入参acceptType,H5页面中输入标签声明的文件选择器设置的接受属性值,就是上边H5代码中这一行:
<input type="file" id="pic" name="camera" accept="image/*" onchange="previewPhoto(this.id, 'img');"/>
(Android 4.1.2)16 <= API <= 20(Android 4.4W.2)
以版本4.4W(API 20)为例:
/**
* Tell the client to open a file chooser.
* @param uploadFile A ValueCallback to set the URI of the file to upload.
* onReceiveValue must be called to wake up the thread.a
* @param acceptType The value of the 'accept' attribute of the input tag
* associated with this file picker.
* @param capture The value of the 'capture' attribute of the input tag
* associated with this file picker.
* @hide
*/
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {
uploadFile.onReceiveValue(null);
}
同样有@hide标签;又比上一版多了一个String入参捕获,同样是输入标签的同名属性值(用来指定设备比如capture =“camera”,不过好像用的很少了)。
API> = 21(Android 5.0.1)
以版本5.0(API 21)为例:
/**
* Tell the client to open a file chooser.
* @param uploadFile A ValueCallback to set the URI of the file to upload.
* onReceiveValue must be called to wake up the thread.a
* @param acceptType The value of the 'accept' attribute of the input tag
* associated with this file picker.
* @param capture The value of the 'capture' attribute of the input tag
* associated with this file picker.
*
* @deprecated Use {@link #showFileChooser} instead.
* @hide This method was not published in any SDK version.
*/
@Deprecated
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {
uploadFile.onReceiveValue(null);
}
之前的@hide干嘛用的,之前不知道,但是这里就有说明了 - 这个方法没有在任何SDK版本发布,也就是说这个方法没有公开,所以不会像别的普通方法那样Override,那要怎么搞?后边说。
还有,这个方法被@deprecated标记了,用新方法showFileChooser()替换了,那我再找找showFileChooser:
/**
* Tell the client to show a file chooser.
*
* This is called to handle HTML forms with 'file' input type, in response to the
* user pressing the "Select File" button.
* To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and
* return true.
*
* @param webView The WebView instance that is initiating the request.
* @param filePathCallback Invoke this callback to supply the list of paths to files to upload,
* or NULL to cancel. Must only be called if the
* <code>showFileChooser</code> implementations returns true.
* @param fileChooserParams Describes the mode of file chooser to be opened, and options to be
* used with it.
* @return true if filePathCallback will be invoked, false to use default handling.
*
* @see FileChooserParams
*/
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
return false;
}
看,这个注释就很用心了.onShowFileChooser()方法和openFileChooser()同样的作用,但是有更详细的解释 -
- 这个方法用来处理HTML表单中声明type =“file”的输入标签,响应的时机用户按下“选择文件”按钮
- 如果要取消该操作(选择文件操作),需要调用filePathCallback.onReceiveValue(空); 返回true;
- 返回值的含义:返回true表示认可再该方法中重写的对filePathCallback的操作,返回false表示使用默认处理(即空方法,不做任何处理)
参数filePathCallback泛型由原来的一个Uri变为Uri [],说明可以支持一次选取多个文件(当然,调用系统相机直接拍照的话还是只能一张一拍,此时Uri []中之只有1个人素,若从相册或文件系统选,应该可以多选(本人没有现现,不敢说肯定可以));
参数FileChooserParams fileChooserParams应该和原来的是一个道理,就是输入标签的属性集合,可以看一下源码:
/**
* Parameters used in the {@link #onShowFileChooser} method.
*/
public static abstract class FileChooserParams {
/** Open single file. Requires that the file exists before allowing the user to pick it. */
public static final int MODE_OPEN = 0;
/** Like Open but allows multiple files to be selected. */
public static final int MODE_OPEN_MULTIPLE = 1;
/** Like Open but allows a folder to be selected. The implementation should enumerate
all files selected by this operation.
This feature is not supported at the moment.
@hide */
public static final int MODE_OPEN_FOLDER = 2;
/** Allows picking a nonexistent file and saving it. */
public static final int MODE_SAVE = 3; /**
* Parse the result returned by the file picker activity. This method should be used with
* {@link #createIntent}. Refer to {@link #createIntent} for how to use it.
*
* @param resultCode the integer result code returned by the file picker activity.
* @param data the intent returned by the file picker activity.
* @return the Uris of selected file(s) or null if the resultCode indicates
* activity canceled or any other error.
*/
public static Uri[] parseResult(int resultCode, Intent data) {
return WebViewFactory.getProvider().getStatics().parseFileChooserResult(resultCode, data);
} /**
* Returns file chooser mode.
*/
public abstract int getMode(); /**
* Returns an array of acceptable MIME types. The returned MIME type
* could be partial such as audio/*. The array will be empty if no
* acceptable types are specified.
*/
public abstract String[] getAcceptTypes(); /**
* Returns preference for a live media captured value (e.g. Camera, Microphone).
* True indicates capture is enabled, false disabled.
*
* Use <code>getAcceptTypes</code> to determine suitable capture devices.
*/
public abstract boolean isCaptureEnabled(); /**
* Returns the title to use for this file selector, or null. If null a default
* title should be used.
*/
public abstract CharSequence getTitle(); /**
* The file name of a default selection if specified, or null.
*/
public abstract String getFilenameHint(); /**
* Creates an intent that would start a file picker for file selection.
* The Intent supports choosing files from simple file sources available
* on the device. Some advanced sources (for example, live media capture)
* may not be supported and applications wishing to support these sources
* or more advanced file operations should build their own Intent.
*
* <pre>
* How to use:
* 1. Build an intent using {@link #createIntent}
* 2. Fire the intent using {@link android.app.Activity#startActivityForResult}.
* 3. Check for ActivityNotFoundException and take a user friendly action if thrown.
* 4. Listen the result using {@link android.app.Activity#onActivityResult}
* 5. Parse the result using {@link #parseResult} only if media capture was not requested.
* 6. Send the result using filePathCallback of {@link WebChromeClient#onShowFileChooser}
* </pre>
*
* @return an Intent that supports basic file chooser sources.
*/
public abstract Intent createIntent();
}
都有注释,不解释。
解决办法
看完源码一切都明了了,怎么做,重写上边这些方法就好。但是@hide方法不能Override怎么办 - 简单粗暴,直接写(没有代码提示是不是有点心虚?等运行完了就不心有)。为了兼容所有版本,最好把3个参数不同的openFileChooser()方法都写上,onShowFileChooser()正常Override就好:
webView.setWebChromeClient(new WebChromeClient() { /**
* 8(Android 2.2) <= API <= 10(Android 2.3)回调此方法
*/
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
Log.e("WangJ", "运行方法 openFileChooser-1");
// (2)该方法回调时说明版本API < 21,此时将结果赋值给 mUploadCallbackBelow,使之 != null
mUploadCallbackBelow = uploadMsg;
takePhoto();
} /**
* 11(Android 3.0) <= API <= 15(Android 4.0.3)回调此方法
*/
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
Log.e("WangJ", "运行方法 openFileChooser-2 (acceptType: " + acceptType + ")");
openFileChooser(uploadMsg);
} /**
* 16(Android 4.1.2) <= API <= 20(Android 4.4W.2)回调此方法
*/
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.e("WangJ", "运行方法 openFileChooser-3 (acceptType: " + acceptType + "; capture: " + capture + ")");
openFileChooser(uploadMsg);
} /**
* API >= 21(Android 5.0.1)回调此方法
*/
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.e("WangJ", "运行方法 onShowFileChooser");
// (1)该方法回调时说明版本API >= 21,此时将结果赋值给 mUploadCallbackAboveL,使之 != null
mUploadCallbackAboveL = filePathCallback;
takePhoto();
return true;
}
}); /* 省略其他内容 */
/**
* 调用相机
*/
private void takePhoto() {
// 指定拍照存储位置的方式调起相机
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); // 选择图片(不包括相机拍照),则不用成功后发刷新图库的广播
// Intent i = new Intent(Intent.ACTION_GET_CONTENT);
// i.addCategory(Intent.CATEGORY_OPENABLE);
// i.setType("image/*");
// startActivityForResult(Intent.createChooser(i, "Image Chooser"), REQUEST_CODE);
} @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE) {
// 经过上边(1)、(2)两个赋值操作,此处即可根据其值是否为空来决定采用哪种处理方法
if (mUploadCallbackBelow != null) {
chooseBelow(resultCode, data);
} else if (mUploadCallbackAboveL != null) {
chooseAbove(resultCode, data);
} else {
Toast.makeText(this, "发生错误", Toast.LENGTH_SHORT).show();
}
}
} /**
* Android API < 21(Android 5.0)版本的回调处理
* @param resultCode 选取文件或拍照的返回码
* @param data 选取文件或拍照的返回结果
*/
private void chooseBelow(int resultCode, Intent data) {
Log.e("WangJ", "返回调用方法--chooseBelow"); if (RESULT_OK == resultCode) {
updatePhotos(); if (data != null) {
// 这里是针对文件路径处理
Uri uri = data.getData();
if (uri != null) {
Log.e("WangJ", "系统返回URI:" + uri.toString());
mUploadCallbackBelow.onReceiveValue(uri);
} else {
mUploadCallbackBelow.onReceiveValue(null);
}
} else {
// 以指定图像存储路径的方式调起相机,成功后返回data为空
Log.e("WangJ", "自定义结果:" + imageUri.toString());
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) {
Log.e("WangJ", "返回调用方法--chooseAbove"); 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.e("WangJ", "系统返回URI:" + uri.toString());
}
mUploadCallbackAboveL.onReceiveValue(results);
} else {
mUploadCallbackAboveL.onReceiveValue(null);
}
} else {
Log.e("WangJ", "自定义结果:" + imageUri.toString());
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);
}
为什么要分开chooseBelow(),chooseAbove()处理?
因为openFileChooser(),onShowFileChooser()方法参数中那个回调参数的泛型不同(一个开放的,一个开放的[]),分开处理明了一些。
看结果:
怎么样?看完这个结果,粗暴写那几个@hide的方法不心虚了吧?
为什么同样的HTML文件在浏览器中打开和我们做的不一样,浏览器节能拍照又能选文件呢?
那是因为我们写死了要么是使用拍照,要么是用文件选取,如果你愿意,可以根据openFileChooser(),onShowFileChooser()方法中的参数指定更个性化的响应,也可以做到像浏览器一样。
可能的问题
权限问题
再次提示,了别忘权限问题,别再这里被坑。
打包完成后不能工作
本来在demo中跑的好好的,但当我们打好释放包测试的时候却又发现没拍拍,没法选择图片了!!!真是坑了个爹啊!!!想想不奇怪,因为openFileChooser ()方法被系统隐藏,又不能Override,而我们的release包是开启了混淆的,所以在打包的时候混淆了openFileChooser(),这就导致无法回调openFileChooser()了。-
keepclassmembers class * extends android。 webkit.WebChromeClient {
public void openFileChooser(...);
}
当然作为良好的面向对象开发者,你可以用一个借口把这个过程写的更优美一点,我只求能把问题说明白,这里就不实现这一步了。
好像没什么了吧,想起了再加。水平有限,如有错,欢迎指正
照旧,Demo源码GitHub传送门,如有收获,欢迎Star
https://gitee.com/anan9303/CallCameraDemo
http://blog.csdn.net/a_running_wolf/article/details/77983739
WebView中打开相机,文件选择器的问题和解决方法的更多相关文章
- QtCreator中打开.ui文件时卡死崩溃的解决方法
问题 QtCreator中打开一个项目,在编辑器中打开普通的.cpp或.h等文件正常,构建正常,运行正常,但是打开ui文件(QtCreator自动调用designer)时会卡死然后崩溃退出. 解决方法 ...
- 在Android Studio中打开Android Device Monitor时报错的解决方法
在Android Studio中打开Android Device Monitor时报以下错误时(Android-SDK\tools\lib\monitor-x86_64\configuration\1 ...
- ubuntu14.04中解压缩window中的zip文件,文件名乱码的解决方法
在windows上压缩的文件,是以系统默认编码中文来压缩文件.由于zip文件中没有声明其编码,所以linux上的unzip一般以默认编码解压,中文文件名会出现乱码. 通过unzip行命令解压,指定字符 ...
- 如何在单独的窗口中打开 Excel 文件
如何在单独的窗口中打开 Excel 文件 文章编号:087583 2012/11/1 18:45:29 故障现象: 如何在单独的窗口中打开 Excel 文件? 解决方案: 比较安全的方法就是直 ...
- 在Eclipse 中打开当前文件夹
最近试过好多次,安装插件来 在Eclipse 中打开当前文件所在文件夹,结果总是不甚如意. 烦躁了,决定还是不要使用插件了!!! 1.打开Eclipse,点击菜单栏上的Run--External To ...
- [Eclipse] eclipse中打开xml文件,使用ctrl+鼠标左键无法跳转至Java源文件【待解决】
eclipse中打开xml文件,使用ctrl+鼠标左键无法跳转至Java源文件: 1. 设置eclipse ctrl + 左键打开源文件代码,如下图,设置都正常 2. 在网上找了很多种办法,均失败,在 ...
- [置顶] 如何在浏览器中打开PDF文件并实现预览的思路与代码
编写项目遇到一个需要在浏览器中打开PDF文件的问题.最终实现效果如下: 其实也就是简单的在浏览器中实现一个打开pdf文件,并有类似预览功能的边框. 其实在网上经常见到类似的页面,在浏览器中打开pdf文 ...
- XamarinSQLite教程Xamarin.iOS项目中打开数据库文件
XamarinSQLite教程Xamarin.iOS项目中打开数据库文件 以下是打开MyDocuments.db数据库的具体操作步骤: (1)将Mac电脑上的MyDocuments.db数据库移动到W ...
- Visual C++ 2010项目在Visual Studio 2013中打开.rc文件提示"undefined keyword or key name: SS_REALSIZECONTROL"解决方法
1.以方式打开.rc文件. 2.删除其中包含SS_REALSIZECONTROL定义的内容. 3.在资源编辑器中打开.rc文件,重新设置Real Size Control的属性(不能在代码编辑器里重新 ...
随机推荐
- JAVA常见算法题(四)
package com.xiaowu.demo; /** * 将一个正整数分解质因数.例如:输入90,打印出90=2*3*3*5. * * * @author WQ * */ public class ...
- 【Hadoop】Hadoop mr wordcount基础
1.基本概念 2.Mapper package com.ares.hadoop.mr.wordcount; import java.io.IOException; import java.util.S ...
- 为集群配置Impala和Mapreduce
FROM: http://www.importnew.com/5881.html -- 扫描加关注,微信号: importnew -- 原文链接: Cloudera 翻译: ImportNew.com ...
- 【Java编码准则】の #13使用散列函数保存password
明文保存password的程序在非常多方面easy造成password的泄漏.尽管用户输入的password一般时明文形式.可是应用程序必须保证password不是以明文形式存储的. 限制passwo ...
- TestNG+ReportNG+IDEA+Git+Jenkins+surefire持续集成数据驱动dubbo接口测试
一.pom.xml增加testng相关配置 <!--添加插件 关联testNg.xml--><plugin> <groupId>org.apache.maven.p ...
- Android学习(十六) 通过GestureDetector进行手势识别
一.手势交互过程: 1)触屏时,触发MotionEvent事件. 2)被OnTouchListener监听,在onTouch()中获得MotionEvent对象. 3)GestureDetector转 ...
- hdu5303(2015多校2)--Delicious Apples(贪心+枚举)
Delicious Apples Time Limit: 5000/3000 MS (Java/Others) Memory Limit: 524288/524288 K (Java/Other ...
- LeetCode LinkList 23. Merge k Sorted Lists
这两天一直也没有顾上记录一下自己做过的题目,回头看看,感觉忘的好快,今天做了一个hard,刚开始觉得挺难得,想了两种方法,一种是每次都从k个list中选取最小的一个,为空的直接跳过,再就是每次合并其中 ...
- Linux下画原理图和PCB
Linux下画原理图和PCB Windows下大名鼎鼎的Allegro和经典的Protel 99SE都是不支持Linux操作系统的.做Linux驱动开发免不了要看一下原理图和PCB. 一般的做法有三种 ...
- 【最后的冲刺】android中excel表的导入和数据处理
[最后的冲刺]android中excel表的导入和数据处理 ——学校课程的查询和修改 1.编写 The Class类把课程表courses.db当做一个实体类,hashcode和equals这两个类是 ...