1. 程式人生 > >Android WebView使用和處理開啟相機拍照回收

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