近几年前端开发真是越来越火,H5 页面开发的移动端页面甚有夺我原生开发半壁江山的意思,忧伤忧伤。不过从实际情况考虑,H5 一套代码到处跑的特性,我们的 Android、IOS…也就只能呵呵了。然而我还是比较喜欢原生应用,对网络质量要求低,经常碰到 H5 页面加载不出来一片空白就不由得抓狂!吐槽归吐槽,正事不能落下。

上一篇Intent调相机的2种方式以及那些你知道的和不知道的坑中完成了对 Intent 调起系统相机、结果处理以及一些问题的应对。其实上篇文章还是因为今天的主题 WebView中调用系统相机 而起,因为涉及到调用相机本身的一些问题之前不是很明确,所以专门搞了一下,记录下来,所以如果调用相机操作本身有什么疑问或问题,请点击跳转到上一篇寻找答案,本篇不再重复。接下来们看看在 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) { // IEurl = document.getElementById(sourceId).value;} else if(navigator.userAgent.indexOf("Firefox") > 0) { // Firefoxurl = window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));} else if(navigator.userAgent.indexOf("Chrome") > 0) { // Chromeurl = window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));}  else if(navigator.userAgent.indexOf("Opera") > 0|| navigator.userAgent.indexOf("Oupeng") > 0) { // Oupengurl = 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() 等方法,接下来我们就打开源码看看。

源码分析

遇到问题看源码是最直接也是最有效的办法,虽然通常情况下阅读源码比看网上一些帖子难度要大点,但却是问题的根本所在。可能有时候遇到很多问题不知道专门从源码下手,这时候就只能用问题去百度、去Google了,看看前辈们是怎么解决这个问题的,遇到涉及源码时再回头追本溯源,这样便会对问题本身理解深刻;久而久之,可见成效。说到这里,推荐一个在线查看各版本源码的地址,毕竟你不会下载了所有版本的源码。闲话少叙,据说不同版本还不一样,那就一个一个看(WebChromeClient.java在 \android\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() 方法来唤醒等待线程(英语不好,莫见怪);并且该方法被 Hide 了。

(Android 3.0) 11 <= API <= 15 (Android 4.0.3)

以 Version 2.3.7_r1(API 15) 为例:

可以看到,该方法也是被 Hide 了;不过 openFileChooser() 方法比上一版多了一个字符串入参acceptType,H5页面中input标签声明的文件选择器设置的 accept 属性值,就是上边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)

以 Version 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,同样是 input 标签的同名属性值(用来指定设备比如capture=”camera”,不过好像用的很少了)。

API >= 21 (Android 5.0.1)

以 Version 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 干嘛用的,之前不知道,但是这里就有说明了——This method was not published in any SDK version,也就是说这个方法没有公开,所以不会像别的普通方法那样 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” 的 input 标签,响应的时机时用户按下“选择文件”按钮
  • 如果要取消该操作(选择文件操作),需要调用 filePathCallback.onReceiveValue(null); return true;
  • 返回值的含义:返回true表示认可再该方法中重写的对 filePathCallback 的操作,返回false表示使用默认处理(即空方法,不做任何处理)

参数 filePathCallback 泛型由原来的一个Uri变为 Uri[],说明可以支持一次选取多个文件(当然,调用系统相机直接拍照的话还是只能一张一张拍,此时Uri[]中之只有1个元素,若从相册或文件系统选,应该可以多选(本人没有实现,不敢说肯定可以));

参数 FileChooserParams fileChooserParams应该和原来的是一个道理,就是input标签的属性集合,可以看一下源码:

/*** 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 enumerateall 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,使之 != nullmUploadCallbackBelow = 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)回调此方法*/@Overridepublic boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {Log.e("WangJ", "运行方法 onShowFileChooser");// (1)该方法回调时说明版本API >= 21,此时将结果赋值给 mUploadCallbackAboveL,使之 != nullmUploadCallbackAboveL = 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()方法参数中那个回调参数的泛型不同(一个Uri、一个Uri[]),分开处理明了一些。
看结果:

怎么样?看完这个结果,粗暴写那几个 @hide 的方法不心虚了吧?

为什么同样的HTML文件在浏览器中打开和我们做的不一样,浏览器节能拍照又能选文件呢?
那是因为我们写死了要么是使用拍照,要么是用文件选取,如果你愿意,可以根据 openFileChooser()、onShowFileChooser()方法中的参数指定更个性化的响应,也可以做到像浏览器一样。

可能的问题

权限问题

再次提示,别忘了权限问题,别再这里被坑。

打包完成后不能工作

本来在demo中跑的好好的,但当我们打好release包测试的时候却又发现没法拍照、没法选择图片了!!!真是坑了个爹啊!!!想想不奇怪,因为 openFileChooser() 方法被系统隐藏,又不能 Override,而我们的release包是开启了混淆的,所以在打包的时候混淆了openFileChooser(),这就导致无法回调openFileChooser()了。
-keepclassmembers class * extends android.webkit.WebChromeClient{
public void openFileChooser(…);
}

当然作为良好的面向对象开发者,你可以用一个借口把这个过程写的更优美一点,我只求能把问题说明白,这里就不实现这一步了。

好像没什么了吧,想起了再加。水平有限,如有谬误,欢迎指正
照旧,Demo源码GitHub传送门,如有收获,欢迎Star

【相机】(2)——WebView中打开相机、文件选择器的问题和解决方法相关推荐

  1. qt程序卡死 linux,QtCreator中打开.ui文件时卡死崩溃的解决方法

    用Java实现单链表的基本操作 笔试题中经常遇到单链表的考题,下面用java总结一下单链表的基本操作,包括添加删除节点,以及链表转置. package mars; //单链表添加,删除节点 publi ...

  2. 在Android Studio中打开Android Device Monitor时报错的解决方法

    在Android Studio中打开Android Device Monitor时报错的解决方法 参考文章: (1)在Android Studio中打开Android Device Monitor时报 ...

  3. WPF引用外部类库中的资源文件提示不能找到的解决方法

    WPF引用外部类库中的资源文件提示不能找到的解决方法 参考文章: (1)WPF引用外部类库中的资源文件提示不能找到的解决方法 (2)https://www.cnblogs.com/scy251147/ ...

  4. Android WebView中打开相机拍照和选择相册

    一般在项目中与js交互,可能会遇到上传文件图片等操作,避免不了一些坑,下面简单说一下,Android 在不同版本中webView调用相机,选择相册的方法是不一样的,3.0以下的调用 public vo ...

  5. MyEclipse中导入java文件出现乱码问题的解决方法

    一.将整个project设置编码UTF-8 windows->Preferences->general->Workspace->Text file encoding->O ...

  6. Mac系统打开dmg文件出现“资源忙”的解决方法

    有时候我们打开dmg安装包的时候会出现如下情况,这种可能是一些进程卡了,或者是自己打开的时候点击了跳过,偶尔会出现这样. 这个时候需要打开"活动监视器"退出几个"disk ...

  7. 解决evince打开pdf文件遇到的错误及解决方法

    综合简化了网上的一些资料,现将我的方法介绍下 1.首先应该保证你已经成功安装了evince,这个一般都是默认安装的,如果没有,可以运行sudo apt-get install evince-commo ...

  8. H5+在浏览器中打开相机扫描二维码

    刚开始提出要在手机浏览器中的HTML页面做扫描是有点懵的,一顿百度后搜到两种方法: 方案一,浏览器中打开相机navigator.mediaDevices.getUserMedia 自带浏览器不支持该属 ...

  9. android项目中在webview中打开pdf

    android项目中在webview中打开pdf. 关于android打开pdf文件方法有多种,下面的文章有详细说明: Android 实现 PDF 文件阅读功能调研 Android 使用PDF.js ...

最新文章

  1. 维基解密揭秘CIA五种恶意软件用法
  2. Andriod监听支付宝收款实现个人支付宝支付接口!附安卓App
  3. weblogic占用java_weblogic内存占用过大调优
  4. CodeForces-1058B B. Vasya and Cornfield
  5. 胡巴小年调皮搞事情?嗨翻五城华为体验店
  6. 秒速五厘米男主php,秒速五厘米最后男主从事的是什么工作?
  7. java中各种数据类型的使用
  8. 华为云welink成像是反的_华为云Welink下载_华为云Welink 中文版 v6.6.6.0_Win10镜像官网...
  9. yum安装nginx,并配置静态资源服务器
  10. hadoop的shuffle过程
  11. keras ConvLSTM2D 的简单应用
  12. 【对讲机的那点事】带你玩转摩托罗拉GP328对讲机读写频
  13. Android之开发者模式配置全面(解决打开了开发者模式,程序还是不能烧进手机问题)
  14. friendly发音_friendly是什么意思
  15. Android 文件存储-图片存储
  16. Testin云层天咨众测学院开课了!
  17. Adaptive Personalized Federated Learning 论文解读+代码解析
  18. 阿里云ECS最新的实例规格族有哪些
  19. AppInventor简单使用教程
  20. 2022计算机软件毕业设计大全

热门文章

  1. Java大数据学习路线图
  2. 大商创虚拟服务器安装教程,【大商创X】一键部署安装文档教程解析
  3. Matlab Shannon编码
  4. npm安装electron时卡死 ,而利用cnpm安装electron时失败(等一系列幺蛾子)
  5. 【题解】「JSOI2012」玄武密码(AC自动机)
  6. 一方包、二方包和三方包的概念
  7. Privacy Definitions - (alpha, beta)-privacy
  8. 使用Tornado+Redis维护ADSL拨号服务器代理池
  9. 阅读替换净化规则_安卓小说阅读器「阅读」增加净化规则,精选104个书源+各分类书源整理归类 | 樱花庄...
  10. android 判断是否为系统应用