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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章