WebView异步加载自定义CSS/JS

近来接到一个需求,就是 webview 要动态加载 css 文件,替换 html 文件原来的,达到定制界面的目的。自己做demo 试验证了一下,webview 本身提供这样的接口,可以达到要求。主要就是使用 webview 的 shouldInterceptRequest 接口,拦截请求。

拦截接口

WebView 调用 loadUrl 后,会首先根据传入的URL获取响应,然后再将响应显示到页面上。根据这个原理,我们可以在获取响应过程中重新改变请求URL或者直接将响应替换。要想拦截 WebView 加载网页我们必须重写 WebViewClient 类,在 WebViewClient 类中我们重写 shouldInterceptRequest() 方法。

通过查看 WebViewClient 的源码我们可以发现,有两个同名方法,分别是:

/**
     * Notify the host application of a resource request and allow the
     * application to return the data.  If the return value is null, the WebView
     * will continue to load the resource as usual.  Otherwise, the return
     * response and data will be used.  NOTE: This method is called on a thread
     * other than the UI thread so clients should exercise caution
     * when accessing private data or the view system.
     *
     * @param view The {@link android.webkit.WebView} that is requesting the
     *             resource.
     * @param url The raw url of the resource.
     * @return A {@link android.webkit.WebResourceResponse} containing the
     *         response information or null if the WebView should load the
     *         resource itself.
     * @deprecated Use {@link #shouldInterceptRequest(WebView, WebResourceRequest)
     *             shouldInterceptRequest(WebView, WebResourceRequest)} instead.
     */
    @Deprecated
    public WebResourceResponse shouldInterceptRequest(WebView view,
            String url) {
        return null;
    }

为了让每次加载 html 页面都能够回调 shouldInterceptRequest 函数,需要在创建 webview 的时候调用 webView.clearCache(true) 清空缓存。单独设置 WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE),不使用缓存,只从网络获取数据,也没有用。

另外一个:

/**
     * Notify the host application of a resource request and allow the
     * application to return the data.  If the return value is null, the WebView
     * will continue to load the resource as usual.  Otherwise, the return
     * response and data will be used.  NOTE: This method is called on a thread
     * other than the UI thread so clients should exercise caution
     * when accessing private data or the view system.
     *
     * @param view The {@link android.webkit.WebView} that is requesting the
     *             resource.
     * @param request Object containing the details of the request.
     * @return A {@link android.webkit.WebResourceResponse} containing the
     *         response information or null if the WebView should load the
     *         resource itself.
     */
    public WebResourceResponse shouldInterceptRequest(WebView view,
            WebResourceRequest request) {
        return shouldInterceptRequest(view, request.getUrl().toString());
    }

第一个已经@Deprecated,即在 SDK API 20 及以下的会执行方法1,SDK API 20 以上的会执行方法2。

demo 实例

我们用一个 map 来传异步加载的 css、js 文件,key为文件名(与现有html文件上加载的一致,方便判断),value为绝对路径。
map 是不能直接用 intent 传输的,所以我们封装一个实现了 Serializable 接口的类,用来传相应的参数。

/**
 * Created on 17-2-8.
 */
public class SerializableMap implements Serializable {

    private Map<String, String> map;

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }
}

下面,我们将实现修改打开页面的背景色。页面的原始背景如下图:

我们要将页面的背景变成红色,首先修改一下 css 文件的 body 背景:

body {
    background: red !important;
}

首先打开一个页面:

//封装异步加载的css
Map<String, String> map = new HashMap<>();
        map.put("style.css", "file:///android_asset/style.css");

Intent intent = new Intent(MzAccountManager.getInstance().getContext(), HtmlLoginActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if(map != null && map.size() > 0){
            SerializableMap serializableMap = new SerializableMap();
            serializableMap.setMap(map);
            Bundle bundle = new Bundle();
            bundle.putSerializable("map", serializableMap);//传给activity
            intent.putExtras(bundle);
        }
        getContext().startActivity(intent);

然后在 activity 里面取出来使用即可:

/**
     * 加载自定义的css/js
     */
    private Map mExtraCssJsMap;

    SerializableMap serializableMap = (SerializableMap) intent.getSerializableExtra("map");
                if(serializableMap != null){
                    mExtraCssJsMap = serializableMap.getMap();
                }

接下来我们重载 shouldInterceptRequest 接口,首先实现 SDK 20 版本以上的:

private class AccountWebViewClient extends WebViewClient {

        public AccountWebViewClient() {
        }

        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//            handleSslError(handler);
            handler.proceed();//遇到证书问题时,继续执行
        }

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();
            if(mExtraCssJsMap != null && mExtraCssJsMap.size() > 0){
                Iterator<Map.Entry<String, String>> iterator = mExtraCssJsMap.entrySet().iterator();
                while (iterator.hasNext()){
                    Map.Entry<String, String> entry = iterator.next();
                    String key = entry.getKey();
                    String value = entry.getValue();
                    if(url.contains(key)){
                        WebResourceResponse response = null;
                        File fileCss = new File(value);
                        if(fileCss.exists()){
                            try {
                                InputStream inputStream = new FileInputStream(fileCss);//读取文件,返回自定义的 response
                                response = new WebResourceResponse(getMimeType(url), "UTF-8", inputStream);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        } else {
                            try {
                                response = new WebResourceResponse(getMimeType(url), "UTF-8", getAssets().open(key));
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        if(response != null){
                            return response;
                        }
                    }
                }
            }
            return super.shouldInterceptRequest(view, request);
        }
    }

    //get mime type by url
   public String getMimeType(String url) {
       String type = null;
       String extension = MimeTypeMap.getFileExtensionFromUrl(url);
       if (extension != null) {
           if (extension.equals("js")) {
               return "text/javascript";
           }
           else if (extension.equals("woff")) {
               return "application/font-woff";
           }
           else if (extension.equals("woff2")) {
               return "application/font-woff2";
           }
           else if (extension.equals("ttf")) {
               return "application/x-font-ttf";
           }
           else if (extension.equals("eot")) {
               return "application/vnd.ms-fontobject";
           }
           else if (extension.equals("svg")) {
               return "image/svg+xml";
           }
           type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
       }
       return type;
   }

其实 SDK 20 以下的实现是差不多的,就是重载的方法不一样而已:

private class AccountWebViewClient extends WebViewClient {

        public AccountWebViewClient() {
        }

        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//            handleSslError(handler);
            handler.proceed();//遇到证书问题时,继续执行
        }

        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            if(mExtraCssJsMap != null && mExtraCssJsMap.size() > 0){
                Iterator<Map.Entry<String, String>> iterator = mExtraCssJsMap.entrySet().iterator();
                while (iterator.hasNext()){
                    Map.Entry<String, String> entry = iterator.next();
                    String key = entry.getKey();
                    String value = entry.getValue();
                    if(url.contains(key)){
                        WebResourceResponse response = null;
                        File fileCss = new File(value);
                        if(fileCss.exists()){
                            try {
                                InputStream inputStream = new FileInputStream(fileCss);
                                response = new WebResourceResponse("text/css", "UTF-8", inputStream);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        } else {
                            try {
                                response = new WebResourceResponse("text/css", "UTF-8", getAssets().open(key));
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        if(response != null){
                            return response;
                        }
                    }
                }
            }
            return super.shouldInterceptRequest(view, url);
        }
    }

通过拦截请求,异步加载我们自己定义的 style.css 文件,打开页面可以看到,页面的背景色已经变成了红色:

大家可以参考下面这个文章,比较详细的实现:
webview-shouldinterceptrequest-example

文章目录
  1. 1. 拦截接口
  2. 2. demo 实例
|