Android基於JsBridge封裝的高效帶加載進度的WebView

Tamic
http://blog.csdn.net/sk719887916/article/details/52402470

概述

從去年4月項目就一直用起了JsBridge,前面也針對jsBridge使用姿勢介紹過一篇入門篇,《Android JsBridge實戰 打造專屬你的Hybrid APP》,本篇接着繼續深入,通過再次優化封裝,大大優化了部分代碼,簡化上層調用流程,快速部署你的Hybridge APP。

再進行具體編碼前 ,我先進行了一般商業APP對WebView的需求

  • 可加載本地和雲端H5
  • 擁有cookie持久能力
  • 添加公共參數
  • 回退前進功能
  • Js與本地navtive交互
  • 擁有加載默認錯誤頁面能力
  • 加載網頁可展現進度
  • 支持https

好爲了滿足以上常用功能,大致對webview相關知識進行下普及。

效果圖:

這裏寫圖片描述

WebView

谷歌提供的系統組件,用來加載和展現html網頁,其採用webkit內核驅動,來實現網頁瀏覽功能。

擁有load() URL和本地html文件

    // 雲端
    webView.loadUrl("https://www.baidu.com"); 
    // 本地
    webView.loadUrl("file:///android_asset/demo.html"); 

WebViewClient

WebViewClient主要輔助WebView執行處理各種響應請求事件的,比如:

  • onLoadResource
  • onPageStart
  • onPageFinish
  • onReceiveError
  • onReceivedHttpAuthRequest
  • shouldOverrideUrlLoading

本次加載失敗頁面,和攔截加入header頭必須用到它,由於android無法攔截h5本身ajax的請求,所以對header同步不是很好,建議大家對於ajax請求採用cookie形式,以防止url參數服務端無法獲取的問題。

加入header 一般直接使用webView.load(url, header)

view.loadUrl(url, header);

爲了方便上層開發者調用,可以將此code加入到WebViewClient 的shouldOverrideUrlLoading中執行
姿勢那就是這樣:

 public boolean shouldOverrideUrlLoading(WebView view, String url) {
   if(this.onPageHeaders(url) != null) {
     view.loadUrl(url, this.onPageHeaders(url));
 }
   return super.shouldOverrideUrlLoading(view, url);
}

錯誤頁面也是複寫WebViewClient的onReceivedError() 來加入自定義的抽象onPageError(),姿勢如下:

public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    view.loadUrl(this.onPageError(failingUrl));
}

onPageHeaders()便是上層抽象出來的接口,方便我們直接加入header,而onPageError()是方便指定加載錯誤頁面,那麼在activity中就是這樣了,

 mProgressBarWebView.setWebViewClient(new    CustomWebViewClient(mProgressBarWebView.getWebView()) {

        @Override
        public String onPageError(String url) {
            return "file:///android_asset/error.html";
        }

        @Override
        public Map<String, String> onPageHeaders(String url) {
            return CookieManger.getHeader(getContext());
        }
    });

WebChromeClient

主要輔助WebView處理Javascript的對話框、網站Logo、網站title、load進度等處理。

  • onCloseWindow(關閉WebView)
  • onCreateWindow()
  • onJsAlert ()
  • onJsPrompt
  • onJsConfirm
  • onProgressChanged
  • onReceivedIcon
  • onReceivedTitle
  • onShowCustomView

WebView只是用來處理一些html的頁面內容,只用WebViewClient就行了,如果需要更豐富的處理效果,比如JS、進度條等,就要用到WebChromeClient。因爲這次功能要用加載進度,不得不說它。

爲了加入頂部的加載進度條,複寫WebChromeClient中onProgressChanged,在這裏更改我們加入的ProgressBar的進度,你也可以設置網頁標題,甚至可以全屏!

 public class CustomWebChromeClient extends WebChromeClient {
private NumberProgressBar mProgressBar;

public CustomWebChromeClient(NumberProgressBar progressBar) {
    this.mProgressBar = progressBar;
}

public void onProgressChanged(WebView view, int newProgress) {
    if(newProgress >= 95) {
        this.mProgressBar.setVisibility(8);
    } else {
        if(this.mProgressBar.getVisibility() == 8) {
            this.mProgressBar.setVisibility(0);
        }

        this.mProgressBar.setProgress(newProgress);
    }

    super.onProgressChanged(view, newProgress);
}

   //獲取tittle
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
    }

    //全屏
    @Override
    public void onShowCustomView(View view, CustomViewCallback callback) {
        super.onShowCustomView(view, callback);
    }
}

好了準備好了同步Header和進度條之後,就的考慮cookie同步問題

CookieSync

CookieManager
CookieManager是用來管理Cookie的,主要來管理cookie相關,提供如下API

  • setAcceptCookie()
  • setCookie()
  • getCookie(String url);
  • removeSessionCookies();
  • hasCookies()
  • removeAllCookie()

CookieSyncManager

CookieSyncManagerl繼承WebSyncManager,來管理同步cookie相關,主要有以下API

  • resetSync()
  • stopSync()
  • sync()
  • syncFromRamToFlash()
  • checkInstanceIsAllowed()

你想問這些api什麼意思,請保留點你的童真,不要問這麼簡單的問題好嗎?

接着我們就可以這樣操作來實現cookie同步了,

    CookieManager cookieManager = CookieManager.getInstance();
   // 接受服務器cookie
    cookieManager.setAcceptCookie(true);
    //移除之前的cookie
    cookieManager.removeSessionCookie();
    // 注入cookies
    List<String> cookies = getCookies(customCookies);
    for (String cookie : cookies) {
        cookieManager.setCookie(uri.getHost(), cookie);
    }
    // 同步cookie
    CookieSyncManager.getInstance().sync();

這裏需要注意棒棒糖以上的會出現無法同步問題那麼請這樣做

         if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.flush();
    } else {
        CookieSyncManager.getInstance().sync();
    }

經測試,完美!

你可能想問?我想自定義像header一樣加入一些自定義cookie,行,沒問題,繼續看!

 public static List<String> createCustomCookies() {
       List<String> cookies = new ArrayList<>();
        cookies.add(“author ” + "= " + "tamic");
         cookies.add(“data” + "= " + "2016.8.15");
         cookies.add(“key” + "= " + 4fdfsfd34dfdfswer");
         cookies.add(“chanel” + "= " + "簡書");
    return cookies;
}

很可能會遇到處理緩存問題,設置緩存webView緩存模式!這裏在普及下相關姿勢!

緩存模式

webview緩存模式有5種,具體方式:
- LOAD_CACHE_ONLY: 不使用網絡,只讀取本地緩存數據
- LOAD_DEFAULT: 根據cache-control決定是否從網絡上取數據。
- LOAD_CACHE_NORMAL: API level 17中已經廢棄, 從API level 11開始作用同LOAD_DEFAULT模式
- LOAD_NO_CACHE: 不使用緩存,只從網絡獲取數據.
- LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用緩存中的數據。

www.baidu.com的cache-control爲no-cache,在模式LOAD_DEFAULT下,無論如何都會從網絡上取數據,如果沒有網絡,就會出現錯誤頁面;在LOAD_CACHE_ELSE_NETWORK模式下,無論是否有網,只要本地有緩存,都會加載緩存。本地沒有緩存時才從網絡上獲取,
這個和Http緩存一致,我不在過多介紹,如果你想自定義緩存策略和時間,可以嘗試下,

清除緩存

CacheManager來處理webview緩存相關:

 clearCache(boolean)

  CacheManager.clear

在4.4以上的此api已經無法使用,也就是說緩存清空涉及安全,需要你自己去實現,就類似picasso, okhttp緩存,一樣要開發者自我去實現。
當然也可以這樣:

         WebView.clearCache(true);

清空歷史記錄

   mWebview.clearHistory();

需要在onPageFinished()的方法之後調用

webview支持https

webView.setWebViewClient(new SSLTolerentWebViewClient());
webView.loadUrl(myhttps url);

複寫WebViewClient的nReceivedSslError函數,執行handler.cancel();給用戶感知的話,來個對話框授權下就行了額

private class SSLTolerentWebViewClient extendsWebViewClient {
   public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {

  AlertDialog.Builder builder = new   AlertDialog.Builder(Tab1Activity.this);
  AlertDialog alertDialog = builder.create();
  String message = "SSL Certificate error.";
  switch (error.getPrimaryError()) {
    case SslError.SSL_UNTRUSTED:
     message = "The certificate authority is not trusted.";
      break;
    case SslError.SSL_EXPIRED:
    message = "The certificate has expired.";
     break;
    case SslError.SSL_IDMISMATCH:
    message = "The certificate Hostname mismatch.";
     break;
    case SslError.SSL_NOTYETVALID:
    message = "The certificate is not yet valid.";
    break;
}

message += " Do you want to continue anyway?";
alertDialog.setTitle("SSL Certificate Error");
 alertDialog.setMessage(message);
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
    // Ignore SSL certificate errors
    handler.proceed();
}
});

alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener() {
   @Override
    public void onClick(DialogInterface dialog, int which) {

     handler.cancel();
    }
    });
     alertDialog.show();
  }
}

ProgressBarWebView

學習了上面基礎知識,我這裏就開始進行自定義的進度條ProgressBarWebView的封裝了,這裏我直接對BridgeWebView進行擴展。下面是主要部分。

 public class ProgressBarWebView extends LinearLayout {
   static final String TAG = ProgressBarWebView.class.getSimpleName();
   private NumberProgressBar mProgressBar;
    private BridgeWebView mWebView;

   public ProgressBarWebView(Context context) {
       super(context);
       this.init(context, (AttributeSet)null);
   }

   public ProgressBarWebView(Context context, AttributeSet attrs) {
      super(context, attrs);
      this.init(context, attrs);
  }

@TargetApi(11)
public ProgressBarWebView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.init(context, attrs);
}

@TargetApi(21)
public ProgressBarWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    this.init(context, attrs);
}

爲了使webview能有後退功能!我屏蔽了長按事件,並且對返回鍵建進行了攔截。

 mWebView.setOnLongClickListener(new OnLongClickListener() {
        public boolean onLongClick(View v) {
            return true;
        }
    });
    this.mWebView.setOnKeyListener(new OnKeyListener() {
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if(event.getAction() == 0 && keyCode == 4 &&      ProgressBarWebView.this.mWebView.canGoBack()) {
                ProgressBarWebView.this.mWebView.goBack();
                return true;
            } else {
                return false;
            }
        }
    });

如果防止webview代碼產生內存泄漏,請及時在activity銷燬時,清空webview

 @Override
  public void onDestroy() {
    super.onDestroyView();
    if (mProgressBarWebView.getWebView() != null) {
        mProgressBarWebView.getWebView().destroy();
    }
}

看了構造方法你已明白,裏面包含一個BridgeWebView和一個NumberProgressBar 成員屬性,
接着就是對JsBridge進行封裝了

Java調用js代碼

public void registerHandler(final String handlerName, final JsHandler handler) {
     this.mWebView.registerHandler(handlerName, new BridgeHandler() {
         public void handler(String data, CallBackFunction function) {
             if(handler != null) {
                handler.OnHandler(handlerName, data, function);
             }
        }
    });
}

js調用Native

public void callHandler(final String handlerName, String javaData, final JavaCallHandler handler) {
    this.mWebView.callHandler(handlerName, javaData, new CallBackFunction() {
        public void onCallBack(String data) {
            if(handler != null) {
                handler.OnHandler(handlerName, data);
            }

        }
    });
}

看可jsBridge的可能問這個JsHandler誰神馬。本來在jsBridge源碼中沒這個東東的, 是爲了方便上層調用我自己封裝的接口,

 public interface JsHandler {
void OnHandler(String var1, String var2, CallBackFunction var3);

好了 關鍵的東西已經介紹完,如果對jsBridge可以看看去年我寫的一篇對他的介紹:Android JsBridge實戰 打造專屬你的Hybrid APP

接着使用我們封裝好的ProgressBarWebView

案列使用

配置

Gradle:

root:

  repositories {
maven { url "https://jitpack.io" }
jcenter()
  }

Module:

   dependencies {
   .....
   compile 'com.tamic:browse:1.0.0'

   }

初始化

    ProgressBarWebView  mProgressBarWebView = (ProgressBarWebView)    findViewById(R.id.login_progress_webview);

設置自定義WebViewClient

    mProgressBarWebView.setWebViewClient(new   CustomWebViewClient(mProgressBarWebView.getWebView()) {

        @Override
        public String onPageError(String url) {
            //指定網絡加載失敗時的錯誤頁面
            return "file:///android_asset/error.html";
        }

        @Override
        public Map<String, String> onPageHeaders(String url) {

            // 可以加入header

            return null;
        }
    });

加載指定Url

    mProgressBarWebView.loadUrl("file:///android_asset/demo.html");

當然,也可以支持網絡url;

註冊Js回調函數

    ArrayList<String> mHandlerNames = new ArrayList<>();
    mHandlers.add("login");
    mHandlers.add("callNative");
    mHandlers.add("callJs");
    mHandlers.add("open");

回調js的方法

        mProgressBarWebView.registerHandlers(mHandlers, new JsHandler() {
        @Override
        public void OnHandler(String handlerName, String responseData, CallBackFunction function) {

            if (handlerName.equals("login")) {

                Toast.makeText(MainActivity.this, responseData, Toast.LENGTH_SHORT).show();

            } else if (handlerName.equals("callNative")) {

                Toast.makeText(MainActivity.this, responseData, Toast.LENGTH_SHORT).show();

                function.onCallBack("我在上海");

            } else if (handlerName.equals("callJs")) {

                Toast.makeText(MainActivity.this, responseData, Toast.LENGTH_SHORT).show();

                // 想調用你的方法:
                function.onCallBack("好的 這是圖片地址 :xxxxxxx");

            } if (handlerName.equals("open")) {

                mfunction = function;

                pickFile();

            }

        }
    });

Native調用js

    mProgressBarWebView.callHandler("callNative", "hello H5, 我是java", new JavaCallHandler() {
        @Override
        public void OnHandler(String handlerName, String jsResponseData) {
            Toast.makeText(MainActivity.this, "h5返回的數據:" + jsResponseData, Toast.LENGTH_SHORT).show();
        }
    });

Native發送消息給js

    mProgressBarWebView.send("哈嘍", new CallBackFunction() {
        @Override
        public void onCallBack(String data) {
            Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
        }
    });
}

Xml文件和js代碼這裏不做介紹,具體看項目案列中源碼:

GtiHub:https://github.com/NeglectedByBoss/JsWebView

如果喜歡,請star!

結束語

這裏感謝曾技術經理出身的同事的對相關部分代碼的封裝,感謝振南同學!

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