【相机】(2)——WebView中打开相机、文件选择器的问题和解决方法
近幾年前端開發真是越來越火,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:
看,這個注釋就很用心了。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中打开相机、文件选择器的问题和解决方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蜘蛛流
- 下一篇: 超级详细树讲解三 —— B树、B+树图解