Android WebView使用和處理開啟相機拍照回收
本篇文章介紹了常用的WebView使用,和處理了回收問題,如有問題,請留言斧正。
概述
WebView是android開發中必不可少的元件,目前環境下混合開發日新月異,相對我們開發者來說,必須要掌握相關的WebView使用技巧和常見問題。
WebView簡介
WebView用來展示網頁內容,WebView在4.4之前基於webkit引擎,從Android 4.4(KitKat)開始,原本基於WebKit的WebView開始基於Chromium核心,這一改動大大提升了WebView元件的效能以及對HTML5,CSS3,JavaScript的支援。不過它的API卻沒有很大的改動,在相容低版本的同時只引進了少部分新的API,並不需要你做很大的改動。WebView的屬性有很多,不過已經封裝在幾個三個大類中,並且webview本身也有很多屬性可以提供使用,通常是getUrl、reload等。常用到的三個配置類是:WebSettings、WebChromeClient、WebViewClient。WebView的屬性我就不介紹了,大家可以看看原始碼。
WebSettings
WebSettings是用來管理WebView配置的類。當WebView第一次建立時,內部會包含一個預設配置的集合。若我們想更改這些配置,便可以通過WebSettings裡的方法來進行設定。
WebSettings物件可以通過WebView.getSettings()獲得,它的生命週期是與它的WebView本身息息相關的,如果WebView被銷燬了,那麼任何由WebSettings呼叫的方法也同樣不能使用。
WebSettings settings = web.getSettings();
// 儲存(storage)
// 啟用HTML5 DOM storage API,預設值 false
settings.setDomStorageEnabled(true);
// 啟用Web SQL Database API,這個設定會影響同一程序內的所有WebView,預設值 false
// 此API已不推薦使用
settings.setDatabaseEnabled(true);
// 啟用Application Caches API,必需設定有效的快取路徑才能生效,預設值 false
settings.setAppCacheEnabled(true);
settings.setAppCachePath(context.getCacheDir().getAbsolutePath());
// 定位(location)
settings.setGeolocationEnabled(true);
// 是否儲存表單資料
settings.setSaveFormData(true);
// 是否當webview呼叫requestFocus時為頁面的某個元素設定焦點,預設值 true
settings.setNeedInitialFocus(true);
// 是否支援viewport屬性,預設值 false
// 頁面通過`<meta name="viewport" ... />`自適應手機螢幕
settings.setUseWideViewPort(true);
// 是否使用overview mode載入頁面,預設值 false
// 當頁面寬度大於WebView寬度時,縮小使頁面寬度等於WebView寬度
settings.setLoadWithOverviewMode(true);
// 佈局演算法
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
// 是否支援Javascript,預設值false
settings.setJavaScriptEnabled(true);
// 是否支援多視窗,預設值false
settings.setSupportMultipleWindows(false);
// 是否可用Javascript(window.open)開啟視窗,預設值 false
settings.setJavaScriptCanOpenWindowsAutomatically(false);
// 資源訪問
settings.setAllowContentAccess(true); // 是否可訪問Content Provider的資源,預設值 true
settings.setAllowFileAccess(true); // 是否可訪問本地檔案,預設值 true
// 是否允許通過file url載入的Javascript讀取本地檔案,預設值 false
settings.setAllowFileAccessFromFileURLs(false);
// 是否允許通過file url載入的Javascript讀取全部資源(包括檔案,http,https),預設值 false
settings.setAllowUniversalAccessFromFileURLs(false);
// 資源載入
settings.setLoadsImagesAutomatically(true); // 是否自動載入圖片
settings.setBlockNetworkImage(false); // 禁止載入網路圖片
settings.setBlockNetworkLoads(false); // 禁止載入所有網路資源
// 縮放(zoom)
settings.setSupportZoom(true); // 是否支援縮放
settings.setBuiltInZoomControls(false); // 是否使用內建縮放機制
settings.setDisplayZoomControls(true); // 是否顯示內建縮放控制元件
// 預設文字編碼,預設值 "UTF-8"
settings.setDefaultTextEncodingName("UTF-8");
settings.setDefaultFontSize(16); // 預設文字尺寸,預設值16,取值範圍1-72
settings.setDefaultFixedFontSize(16); // 預設等寬字型尺寸,預設值16
settings.setMinimumFontSize(8); // 最小文字尺寸,預設值 8
settings.setMinimumLogicalFontSize(8); // 最小文字邏輯尺寸,預設值 8
settings.setTextZoom(100); // 文字縮放百分比,預設值 100
WebChromeClient
從名字上不難理解,這個類就像WebView的委託人一樣,是幫助WebView處理各種通知和請求事件
// 獲得所有訪問歷史專案的列表,用於連結著色。
public void getVisitedHistory(ValueCallback<String[]> callback) {
}
// <video /> 控制元件在未播放時,會展示為一張海報圖,HTML中可通過它的'poster'屬性來指定。
public Bitmap getDefaultVideoPoster() {
return null;
}
public View getVideoLoadingProgressView() {
return null;
}
// 接收當前頁面的載入進度
public void onProgressChanged(WebView view, int newProgress) {
}
// 接收文件標題
public void onReceivedTitle(WebView view, String title) {
}
// 接收圖示(favicon)
public void onReceivedIcon(WebView view, Bitmap icon) {
}
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
}
// 通知應用當前頁進入了全屏模式,此時應用必須顯示一個包含網頁內容的自定義View
public void onShowCustomView(View view, CustomViewCallback callback) {
}
// 通知應用當前頁退出了全屏模式,此時應用必須隱藏之前顯示的自定義View
public void onHideCustomView() {
}
// 顯示一個alert對話方塊
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return false;
}
// 顯示一個confirm對話方塊
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
// 顯示一個prompt對話方塊
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
// 顯示一個對話方塊讓使用者選擇是否離開當前頁面
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}
// 指定源的網頁內容在沒有設定許可權狀態下嘗試使用地理位置API。
// 從API24開始,此方法只為安全的源(https)呼叫,非安全的源會被自動拒絕
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
}
// 當前一個呼叫 onGeolocationPermissionsShowPrompt() 取消時,隱藏相關的UI。
public void onGeolocationPermissionsHidePrompt() {
}
// 通知應用開啟新視窗
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
return false;
}
// 通知應用關閉視窗
public void onCloseWindow(WebView window) {
}
// 請求獲取取焦點
public void onRequestFocus(WebView view) {
}
// 通知應用網頁內容申請訪問指定資源的許可權(該許可權未被授權或拒絕)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequest(PermissionRequest request) {
request.deny();
}
// 通知應用許可權的申請被取消,隱藏相關的UI。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequestCanceled(PermissionRequest request) {
}
// 為'<input type="file" />'顯示檔案選擇器,返回false使用預設處理
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
return false;
}
// 接收JavaScript控制檯訊息
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return false;
}
WebViewClient
// 攔截頁面載入,返回true表示宿主app攔截並處理了該url,否則返回false由當前WebView處理
// 此方法在API24被廢棄,不處理POST請求,這裡有的人會介紹說返回true,這種說法是錯誤的,看這個方法的註釋就知道,如果返
//true,是為了讓app自己離開webview來處理,比如我們可以在這裡面處理電話號(tel:),預設返回false
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
// 攔截頁面載入,返回true表示宿主app攔截並處理了該url,否則返回false由當前WebView處理
// 此方法添加於API24,不處理POST請求,可攔截處理子frame的非http請求
@TargetApi(Build.VERSION_CODES.N)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return shouldOverrideUrlLoading(view, request.getUrl().toString());
}
// 此方法廢棄於API21,調用於非UI執行緒
// 攔截資源請求並返回響應資料,返回null時WebView將繼續載入資源
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return null;
}
// 此方法添加於API21,調用於非UI執行緒
// 攔截資源請求並返回資料,返回null時WebView將繼續載入資源
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
// 頁面(url)開始載入
public void onPageStarted(WebView view, String url, Bitmap favicon) {
}
// 頁面(url)完成載入
public void onPageFinished(WebView view, String url) {
}
// 將要載入資源(url)
public void onLoadResource(WebView view, String url) {
}
// 這個回撥添加於API23,僅用於主框架的導航
// 通知應用導航到之前頁面時,其遺留的WebView內容將不再被繪製。
// 這個回撥可以用來決定哪些WebView可見內容能被安全地回收,以確保不顯示陳舊的內容
// 它最早被呼叫,以此保證WebView.onDraw不會繪製任何之前頁面的內容,隨後繪製背景色或需要載入的新內容。
// 當HTTP響應body已經開始載入並體現在DOM上將在隨後的繪製中可見時,這個方法會被呼叫。
// 這個回調發生在文件載入的早期,因此它的資源(css,和影象)可能不可用。
// 如果需要更細粒度的檢視更新,檢視 postVisualStateCallback(long, WebView.VisualStateCallback).
// 請注意這上邊的所有條件也支援 postVisualStateCallback(long ,WebView.VisualStateCallback)
public void onPageCommitVisible(WebView view, String url) {
}
// 此方法廢棄於API23
// 主框架載入資源時出錯
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
}
// 此方法添加於API23
// 載入資源時出錯,通常意味著連線不到伺服器
// 由於所有資源載入錯誤都會呼叫此方法,所以此方法應儘量邏輯簡單
@TargetApi(Build.VERSION_CODES.M)
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
if (request.isForMainFrame()) {
onReceivedError(view, error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
}
}
// 此方法添加於API23
// 在載入資源(iframe,image,js,css,ajax...)時收到了 HTTP 錯誤(狀態碼>=400)
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
}
// 是否重新提交表單,預設不重發
public void onFormResubmission(WebView view, Message dontResend, Message resend) {
dontResend.sendToTarget();
}
// 通知應用可以將當前的url儲存在資料庫中,意味著當前的訪問url已經生效並被記錄在核心當中。
// 此方法在網頁載入過程中只會被呼叫一次,網頁前進後退並不會回撥這個函式。
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
}
// 載入資源時發生了一個SSL錯誤,應用必需響應(繼續請求或取消請求)
// 處理決策可能被快取用於後續的請求,預設行為是取消請求
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.cancel();
}
// 此方法添加於API21,在UI執行緒被呼叫
// 處理SSL客戶端證書請求,必要的話可顯示一個UI來提供KEY。
// 有三種響應方式:proceed()/cancel()/ignore(),預設行為是取消請求
// 如果呼叫proceed()或cancel(),Webview 將在記憶體中儲存響應結果且對相同的"host:port"不會再次呼叫 onReceivedClientCertRequest
// 多數情況下,可通過KeyChain.choosePrivateKeyAlias啟動一個Activity供使用者選擇合適的私鑰
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
request.cancel();
}
// 處理HTTP認證請求,預設行為是取消請求
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
handler.cancel();
}
// 通知應用有個已授權賬號自動登陸了
public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
}
// 給應用一個機會處理按鍵事件
// 如果返回true,WebView不處理該事件,否則WebView會一直處理,預設返回false
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
return false;
}
// 處理未被WebView消費的按鍵事件
// WebView總是消費按鍵事件,除非是系統按鍵或shouldOverrideKeyEvent返回true
// 此方法在按鍵事件分派時被非同步呼叫
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
super.onUnhandledKeyEvent(view, event);
}
// 通知應用頁面縮放係數變化
public void onScaleChanged(WebView view, float oldScale, float newScale) {
}
這些方法的發生順序發生在webview載入過程中:
shouldOverrideUrlLoading
onProgressChanged[...]
shouldInterceptRequest
onProgressChanged[...]
onPageStarted
onProgressChanged[...]
onLoadResource
onProgressChanged[...]
onReceivedTitle/onPageCommitVisible
onProgressChanged[100]
onPageFinished
JavaScript和WebView互動
WebView呼叫網頁上的JavaScript程式碼
在WebView中呼叫JS基本格式為webView.loadUrl(“javascript:methodName(parameterValues)”);
這種是呼叫JS的無返回值的方法,WebView也可以呼叫JS有返回值的方法,當然前提是在4.4之上的版本才支援,通過evaluateJavaScript方法,傳入JS方法和方法返回型別的回撥。
舉例說明,下面是JS的方法
function readyToGo() {
alert("Hello")
}
function alertMessage(message) {
alert(message)
}
function getYourCar(){
return "Car";
}
WebView呼叫JavaScript無參無返回值函式
String call = "javascript:readyToGo()";
webView.loadUrl(call);
WebView呼叫JavScript有參無返回值函式
String call = "javascript:alertMessage(\"" + "content" + "\")";
webView.loadUrl(call);
WebView呼叫JavaScript有引數有返回值的函式
@TargetApi(Build.VERSION_CODES.KITKAT)
private void evaluateJavaScript(WebView webView){
webView.evaluateJavascript("getYourCar()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
Log.d("findCar",s);
}
});
}
JavaScript通過WebView呼叫Java程式碼
從API19開始,Android提供了@JavascriptInterface物件註解的方式來建立起Javascript物件和Android原生物件的繫結,提供給JavScript呼叫的函式必須帶有@JavascriptInterface。
1.先設定啟用JS支援
//是否支援Javascript,預設值false
settings.setJavaScriptEnabled(true);
2.注入物件到Javascript
public class JSObject {
@JavascriptInterface
public void say(String words) {
// todo
}
}
// 注入物件'jsobj',在網頁中通過`jsobj.say(...)`呼叫,網頁端直接可以拿到'jsobj'這個物件。
web.addJavascriptInterface(new JSObject(), "jsobj")
3.JS使用
window.jsobj.say(...)
這裡JS也可以呼叫android的有返回值的方法
定義一個帶返回值的Java方法,並使用@JavaInterface
@JavaInterface
public String getMessage(){
return "Hello,boy~";
}
JS方法可以直接通過物件呼叫
function showHello(){
var str=window.jsobj.getMessage();
console.log(str);
}
WebView載入優化
此處參考別人的,自己沒有嘗試,因為在開發中的專案暫時沒有用到。但是和我的想法不謀而合
當WebView的使用頻率變得頻繁的時候,對於其各方面的優化就變得逐漸重要了起來。可以知道的是,我們每載入一個 H5頁面,都會有很多的請求。除了HTML主URL自身的請求外,HTML外部引用的 JS、CSS、字型檔案、圖片都是一個個獨立的HTTP 請求,雖然請求是併發的,但當網頁整體數量達到一定程度的時候,再加上瀏覽器解析、渲染的時間,Web整體的載入時間變得很長。同時請求檔案越多,消耗的流量也會越多。那麼對於載入的優化就變得非常重要,這方面的經驗我也沒有什麼別的,大概三個方面:
一個,就是資源本地化的問題
首先可以明確的是,以目前的網路條件,通過網路去伺服器獲取資源的速度是遠遠比不上從本地讀取的。談論各種優化策略其實恰恰忽略了“需要載入”才是阻擋速度提升的最大絆腳石。所以我們的思路一,就是將一些較重的資源比如js、css、圖片甚至HTML本身進行本地化處理,在每次載入到這些資源的時候,從本地讀取進行載入,可以簡單記憶為“存·取·更”。
1.“存”——將上述重量級資源打包進apk檔案,每次載入相應檔案時時從本地取即可。也可不打包,在第一次載入時以及接下來的若干間隔時間裡動態下載儲存,將所有的資原始檔都存在Android的asset目錄下;
2.“取”——重寫WebViewClient的WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)方法,通過一定的判別方法(例如正則表示式)攔截相應的請求,從本地讀取相應資源並返回;
3.“更”——建立起Cache Control機制,定期或使用API通知的形式控制本地資源的更新,保證本地資源是最新和可用的。
第二個,就是快取的問題
倘若你不採用或不完全採用第一條資源本地化的思路,那麼你的WebView快取是必須要開啟的(雖然這一思路和第一條有重合的地方)。
WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDomStorageEnabled(true);//開啟DOM快取
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
在網路正常時,採用預設快取策略,在快取可獲取並且沒有過期的情況下載入快取,否則通過網路獲取資源以減少頁面的網路請求次數。
這裡值得提起的是,我們經常在app裡用WebView展示頁面時,並不想讓使用者覺得他是在訪問一個網頁。因為倘若我們的app裡網頁非常多,而我們給使用者的感覺又都像在訪問網頁的話,我們的app便失去了意義。(我的意思是為什麼使用者不直接使用瀏覽器呢?)
所以這時,離線快取的問題就值得我們注意。我們需要讓使用者在沒有網的時候,依然能夠操作我們的app,而不是面對一個和瀏覽器裡的網路錯誤一樣的頁面,哪怕他能進行的操作十分有限。
這裡我的思路是,在開啟快取的前提下,WebView在載入頁面時檢測網路變化,倘若在載入頁面時使用者的網路突然斷掉,我們應當更改WebView的快取策略。
ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if(networkInfo.isAvailable()) {
settings.setCacheMode(WebSettings.LOAD_DEFAULT);//網路正常時使用預設快取策略
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//網路不可用時只使用快取
}
既然有快取,就要有快取控制,與一相似的是我們也要建立快取控制機制,定期或接受伺服器通知來進行快取的清空或更新。
第三個,就是延遲載入和執行js
在WebView中,onPageFinished()的回撥意味著頁面載入的完成。但該方法會在JavScript指令碼執行完成後才會觸發,倘若我們要載入的頁面使用了JQuery,會在處理完DOM物件,執行完$(document).ready(function() {})後才會渲染並顯示頁面。這是不可接受的,所以我們需要對Js進行延遲載入,當然這部分是Web前端的工作。
處理開啟相機拍照回收
先放兩張效果圖
使用了WebView並且其中要開啟原生相機拍照和開啟相簿選擇圖片,在記憶體較低的手機上測試必定會出現回收,測試手機小米三,當記憶體不足的時候,從webview介面開啟拍照後,只要返回後就會回收當前的WebView介面。我自己的手機nexus6p倒是沒有出現回收,讓我很尷尬。後來拿了一部青橙的手機測試後,我感覺要死啦。它的現象是必定回收~~~
至於拍照回收頁面,大概是有的手機限定了記憶體使用,開啟拍照後就會收回。
其中我的專案分為開啟拍照和開啟相簿,對於開啟相簿,我採用自定義的相簿,沒有出現回收。
至於相機,我剛開始採用了google的cameraView相機唉,發現還是會回收。最後只有通過解決回收問題了。
我的思路:
呼叫拍照的時候可以傳入圖片路徑給相機,那麼我只要在回收的時候儲存這個路徑並且判斷是否拍照了照片,然後吧圖片轉成base64字串,然後在WebViewClient中的onPageStarted方法中判斷是否回收,然後壓縮圖片,再然後就是吧字串通過呼叫H5的方法有參構造方法傳給H5。後來發現這裡有個問題,我的壓縮是耗時操作,那麼H5那端需要啟用一個延遲獲取值(H5的timeout方法延遲500毫秒即可)
這是異常的應該執行的操作,那麼正常的就是,點選input標籤,並且input標籤設定accept型別,點選這種檔案操作會呼叫WebChromeClient的onShowFileChooser方法;
1.判斷是否有上次的圖片,進行刪除;
2.過濾input的型別,判斷是否開啟相機和相簿;
3.開啟相簿或者相機,並且返回true(返回true,表示app會處理)儲存filePathCallback物件(呼叫它的onReceiveValue(xx)方法,這樣的話H5那邊就可以接到到返回的uri了)
4.在onActivityResult()方法中獲取拍照後的資料,並且包裝成uri[]陣列。並且使用filePathCallback回撥。
這是一次正常的流程。
接下來就是重中之重了,擼程式碼
回收相關的欄位設定
/**
* 判斷此頁面是否被回收
*/
private boolean isRecycler;
/**
* 用來給H5呼叫的
*/
private boolean isH5Recycler;
/**
* 用來給H5呼叫:判斷當前的type是哪種(eg:家訪、核賬)
*/
private String H5ActionType;
/**
* 用來儲存得到的一維碼,防止介面回收;介面回收後要將其儲存
*/
private String mOneCode;
/**
* 用來給H5呼叫的,在頁面被回收的時候要儲存
*/
private String H5FileList;
/**
* 用來給H5呼叫的,在頁面被回收的時候要儲存,幫它儲存家訪action中填寫的內容
*/
private String H5VisitEditData;
/**
* 用來給H5呼叫的,在頁面被回收的時候要儲存,幫它儲存核賬action中填寫的內容
*/
private String H5CallAccountEditData;
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("photo_path", mCurrentPhotoPath);
outState.putParcelable("uri", photoURI);
outState.putString("h5_file_list", H5FileList);
outState.putString("H5VisitEditData",H5VisitEditData);
outState.putString("H5CallAccountEditData",H5CallAccountEditData);
outState.putString("mOneCode", mOneCode);
outState.putString("H5ActionType",H5ActionType);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
isRecycler = true;
isH5Recycler = true;
H5ActionType = savedInstanceState.getString("H5ActionType");
mCurrentPhotoPath = savedInstanceState.getString("photo_path");
photoURI = savedInstanceState.getParcelable("uri");
H5FileList = savedInstanceState.getString("h5_file_list");
H5VisitEditData = savedInstanceState.getString("H5VisitEditData");
H5CallAccountEditData = savedInstanceState.getString("H5CallAccountEditData");
mOneCode = savedInstanceState.getString("mOneCode");
}
}
WebView的設定,先在pagestart中判斷是否回收
mWebView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("tel:")) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
startActivity(intent);
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (isRecycler) {
openStorage();
}
}
});
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
//當正常的拍照流程沒有問題的話,那麼如果拍完照片的話並且給H5,那麼的話手機端不知道什麼時候要刪除圖片
//為了防止圖片過多,那麼在點選input標籤的時候就刪除上一個圖片地址
deleteImageFile();
String[] chooserParams = fileChooserParams.getAcceptTypes();
Logger.d(Arrays.toString(chooserParams));
List<String> list = Arrays.asList(chooserParams);
if (list.contains(imageExtension)) {
openCamera();
} else if (list.contains(galleryExtension)) {
//採用相簿並不使用系統自帶的
PhotoPicker.builder()
.setPhotoCount(1)
.setShowCamera(false)
.setShowGif(false)
.setPreviewEnabled(false)
.start(WebDetailActivity.this, PhotoPicker.REQUEST_CODE);
} else {
return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
}
mValueCallback = filePathCallback;
return true;
}
});
接下來是打相機拍完照片,後返回就應該到onActivityResult()方法了。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//儲存一些值用來回收判斷
mRequestCode = requestCode;
mResultCode = resultCode;
mGalleryIntent = data;
if (resultCode == Activity.RESULT_CANCELED && requestCode == REQUEST_GALLERY && mValueCallback != null) {
//如果相簿沒有選擇或者直接返回需要給callback設定,不設定的話onShowFileChooser方法不會呼叫
mValueCallback.onReceiveValue(null);
mValueCallback = null;
}
if (requestCode == REQUEST_CAMERA) {
//開啟拍照後並沒有選擇或者直接返回的話,需要把當前傳入給相機應用的圖片檔案刪除
if (resultCode == Activity.RESULT_CANCELED) {
deleteImageFile();
if (!CommUtil.checkIsNull(mValueCallback)) {
//如果相機沒有選擇或者直接返回需要給callback設定,不設定的話onShowFileChooser方法不會呼叫
mValueCallback.onReceiveValue(null);
mValueCallback = null;
}
} else if (resultCode == Activity.RESULT_OK) {
//TODO 下一步應該壓縮圖片
if (mValueCallback != null) {
Uri[] results = null;
results = new Uri[]{photoURI};
mValueCallback.onReceiveValue(results);
mValueCallback = null;
}
}
}
//裡是正常的開啟相簿返回後
if (requestCode == PhotoPicker.REQUEST_CODE) {
if (resultCode == RESULT_OK) {
if (data != null) {
ArrayList<String> photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS);
Logger.d(photos.get(0));
File file = new File(photos.get(0));
Observable.just(file)
.map(new Func1<File, Uri>() {
@Override
public Uri call(File file) {
return getImageContentUri(WebDetailActivity.this, file);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Uri>() {
@Override
public void call(Uri uri) {
if (!CommUtil.checkIsNull(uri)) {
Uri[] results = new Uri[]{uri};
mValueCallback.onReceiveValue(results);
mValueCallback = null;
} else {
mValueCallback.onReceiveValue(null);
mValueCallback = null;
}
}
});
} else {
ToastUtils.showShort(R.string.data_unusual);
mValueCallback.onReceiveValue(null);
mValueCallback = null;
}
} else {
mValueCallback.onReceiveValue(null);
mValueCallback = null;
}
}
}
/**
* 絕對路徑轉uri
*
* @param context
* @param imageFile
* @return content Uri
*/
public static Uri getImageContentUri(Context context, java.io.File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "=? ",
new String[]{filePath}, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
那麼這些方法都準備好了,下一步是開啟圖片,程式碼上面有一個方法是開啟相機的方法,下面也寫出來
/**
* 先判斷是否有相機模組
*/
private void openCamera() {
boolean hasSystemFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
if (hasSystemFeature) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}
if (photoFile != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
photoURI = FileProvider.getUriForFile(this, "com.cango.adpickcar.fileprovider", photoFile);
} else {
photoURI = Uri.fromFile(photoFile);
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_CAMERA);
}
}
}
}
/**
* 建立一個圖片檔案
*
* @return
* @throws IOException
*/
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
mCurrentPhotoPath = image.getAbsolutePath();
return image;
}
/**
* 刪除當前的圖片檔案
*
* @return
*/
private boolean deleteImageFile() {
if (mCurrentPhotoPath != null) {
File emptyFile = new File(mCurrentPhotoPath);
if (emptyFile.exists())
return emptyFile.delete();
}
return false;
}
接下來如果正常的走通流程就沒有問題,那麼如果不正常呢,就是被回收呢,因為之前已經將回收要儲存的屬性已經在回收中儲存了,那麼我就在onpagestart中判斷是否回收,然後將回收後產生的圖片轉成base64通過js方法給H5就可以了。
@Override
public void onPageFinished(WebView view, String url) {
loadDia.dismiss();
if (isRecycler) {
openStorage();
}
super.onPageFinished(view, url);
}
@AfterPermissionGranted(REQUEST_STORAGE_GROUP)
private void openStorage() {
String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (EasyPermissions.hasPermissions(this, perms)) {
if (isRecycler) {
isRecycler = false;
recycler();
}
} else {
EasyPermissions.requestPermissions(this, getString(R.string.location_group_and_storage),
REQUEST_STORAGE_GROUP, perms);
}
}
上面的程式碼就是判斷是否回收會走recycler()
/**
* 假如被回收要做的事情
*/
private void recycler() {
isRecycler = false;
switch (mRequestCode) {
case REQUEST_CAMERA:
if (mResultCode == Activity.RESULT_OK) {
if (mWebView != null) {
Logger.d(mCurrentPhotoPath);
Observable
.just(mCurrentPhotoPath)
.map(new Func1<String, String>() {
@Override
public String call(String s) {
//把bitmap壓縮了
return bitmapToString(s);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
//呼叫js方法把base64的字串給H5
String call = "javascript:recyclerPhoto(\"" + s + "\")";
mWebView.loadUrl(call);
}
});
}
} else if (mResultCode == Activity.RESULT_CANCELED) {
deleteImageFile();
} else {
}
break;
}
}
//把bitmap轉換成String
public static String bitmapToString(String filePath) {
Bitmap bm = getSmallBitmap(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//1.5M的壓縮後在100Kb以內,測試得值,壓縮後的大小=94486位元組,壓縮後的大小=74473位元組
//這裡的JPEG 如果換成PNG,那麼壓縮的就有600kB這樣
bm.compress(Bitmap.CompressFormat.JPEG, 40, baos);
byte[] b = baos.toByteArray();
return Base64.encodeToString(b, Base64.DEFAULT);
}
接下來就是JS那邊在初始的時候要判斷是否回收,判斷當前型別(用來跳轉具體的頁面)、判斷是否有圖片64位字串(將圖片加入圖片組中)。
其實H5那邊處理的就需要加一個延遲操作來判斷是否有64位字串,延遲500毫秒就可以。
總結
這種方式處理webview開啟拍照回收很好,只是程式碼麻煩,並且還需要H5來配合。現在就在想如何優化這邊,也不能讓使用者換手機呀(^_^),想了方法是自定義一個相機,不會產生回收就夠用了。這篇文章寫的不好,因為沒有什麼自己的東西,也只自己遇到的回收問題,其實webView就用原生的就好,自從4.4之後換了核心,也不卡頓了,開心。
對於自定義相機,下一篇文章就寫個自定義相機,可能要寫好久,因為不太懂。先給自己定個目標,做人嘛要有目標,不然和鹹魚有什麼區別呢。