本篇文章介紹了常用的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之後換了內核,也不卡頓了,開心。
對於自定義相機,下一篇文章就寫個自定義相機,可能要寫好久,因爲不太懂。先給自己定個目標,做人嘛要有目標,不然和鹹魚有什麼區別呢。