在某个项目中,由于用到第三方的接口,登录和授权的界面也由第三方提供,而我们是在车机产品上使用,分辨率为800X480。第三方并没有针对这个分辨率的布局,并且不为我们进行修改,就算是高德去推也没有推动。因此,只能使用偏门的方法,之所以说偏门,是因为注入css和js的方式是有风险的,假如web端改了页面,可能我们的app就得跟着升级了。好了,下面开始正题
- 首先,要在WebView注入JS,我们需要开启JS的支持:
mWebView = (WebView) findViewById(R.id.wb_login); //访问的html中支持js mWebView.getSettings().setJavaScriptEnabled(true); //设置背景色 mWebView.setBackgroundColor(0); //设置透明度 mWebView.getBackground().setAlpha(0); mWebView.setWebViewClient(new WebViewClient() {
- 重写WebViewClient中的关键方法
shouldOverrideUrlLoading(WebView view, WebResourceRequest request) Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView.
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //在当前的webview中跳转到新的url //获取cookies CookieManager cm = CookieManager.getInstance(); cookies = cm.getCookie(url); Log.d(TAG, "shouldOverrideUrlLoading url=" + url); Log.d(TAG, "shouldOverrideUrlLoading cookies ... " + cookies); if (url.contains("code=")) { mAutoMapCode = utils.getURLValue(url, "code"); loginLayout.setVisibility(View.GONE); registerLayout.setVisibility(View.VISIBLE); } view.loadUrl(url); return true; }
我在demo中重写shouldOverrideUrlLoading这个函数主要是为了在Web进行重定向时,显示和隐藏一些Android界面的按钮,如果没有特殊的需求,不需要重写这个函数
onPageFinished(WebView view, String url) Notify the host application that a page has finished loading.
@Override public void onPageFinished(WebView view, String url) { Log.d(TAG, "onPageFinished url = " + url); if (url.contains("passport.xiami.com/qrcode-login")) { view.loadUrl("javascript:(function() {" + "var parent = document.getElementsByTagName('head').item(0);" + "var style = document.createElement('style');" + "style.type = 'text/css';" + "style.innerHTML = window.atob('" + qrcss + "');" + "parent.appendChild(style)" + "})()"); } else if (url.contains("oauth.xiami.com/authorize")) { view.loadUrl("javascript:(function() {" + "var parent = document.getElementsByTagName('head').item(0);" + "var style = document.createElement('style');" + "style.type = 'text/css';" + "style.innerHTML = window.atob('" + authcss + "');" + "parent.appendChild(style);" + "})()"); } else { super.onPageFinished(view, url); } }我们可以在onPageFinished回调函数中,通过js脚本的方式动态注入css,上面就是示例代码。但是不建议在onPageFinished函数中完成脚本的注入,因为执行到onPageFinished的时候页面已经加载完成了,这样的修改会导致界面有一个闪烁的变化的过程,我们有更好的方式来完成css和js的注入或者替换,下面会说到。
shouldInterceptRequest(WebView view, WebResourceRequest request)
Notify the host application of a resource request and allow the application to return the data.
重头戏来了,shouldInterceptRequest是在非UI线程中运行的,那么我们可以在这里做网络访问等耗时的操作。其次,网络的响应数据是在这里返回的,因此在这里进行处理就可以解决在onPageFinished中的闪烁问题。
@Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { Log.d(TAG, "shouldInterceptRequest url=" + request.getUrl()); if (request.getUrl().toString().contains("https://passport.xiami.com/qrcode-login")) { //通过URL主动请求整个html回来,然后注入CSS和JS String page = mCssUtils.getUrlData(request.getUrl().toString(),"css/qr_code.css", "js/disableclick.js"); return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(page.getBytes())); } else if(request.getUrl().toString().contains("oauth.xiami.com/authorize")){ //通过URL主动请求整个html回来,然后注入CSS和JS String page = mCssUtils.getUrlData(request, cookies,"css/auth.css", null); return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(page.getBytes())); } //因为授权界面的修改复杂一些,针对特定的资源一个个修改 //左方向箭头 else if (request.getUrl().toString().contains("https://gtms02.alicdn.com/tps/i2")) { try { return new WebResourceResponse("image/png", "UTF-8", getAssets().open("img/left.png")); } catch (IOException e) { e.printStackTrace(); } } //右方向箭头 else if (request.getUrl().toString().contains("https://gtms02.alicdn.com/tps/i3")) { try { return new WebResourceResponse("image/png", "UTF-8", getAssets().open("img/right.png")); } catch (IOException e) { e.printStackTrace(); } }//加载CSS的时候,附加我们自己的css上去 else if (request.getUrl().toString().contains("https://oauth.xiami.com/assets/css/oauth-us.css")) { String css = mCssUtils.appendCss(request.getUrl().toString(), "css/auth.css"); return new WebResourceResponse("text/css", "UTF-8", new ByteArrayInputStream(css.getBytes())); } return super.shouldInterceptRequest(view, request); }在这里,我列举了两种方法,对返回的html进行css和js注入。
1、方法一是通过请求的url,主动去访问,请求整个html回来,注入css和js,注意因为这个时候此方法还没有返回,所以界面不会刷新,也就解决了闪烁的问题,当我们返回注入过css和js的WebResourceResponse对象时,界面才会加载。
2、方法二是针对要请求的资源类型,如image、css、js等,拦截后给系统返回我们自定义的css和js,达到注入修改的目的
注入的过程中,注意css注入在head最后位置,js注入在body最后位置,其实就是要让自己的css和js最后执行,防止被后面的代码覆盖。我在项目中注入的html中就在</body>的结束前还有一段js代码,一开始没注意,我注入的js一直未起效。
原始的二维码界面:
经过css和js注入后的界面如下:
原始的授权界面:
注入css和js后的界面:
代码中我自定义了一个cssutil工具类,里面完成了关键代码如下:
/** * 描述:根据给定的网址,获取返回的html注入css和js * 作者:LeoHo * * @param * @return * @throw */ public String getUrlData(WebResourceRequest request, String cookies, String cssPath, String jsPath) { String page = getHtml(request, cookies); String css = ""; if (cssPath != null && !cssPath.isEmpty()) { css = buildCss(cssPath); } String js = ""; if (jsPath != null && !jsPath.isEmpty()) { js = buildJS(jsPath); } page = inject(page, css, js); return page; }/** * 描述:请求指定的url并返回html页字符串 * 作者:LeoHo * * @param * @return * @throw */ private String getHtml(WebResourceRequest request, String cookies) { StringBuilder total = new StringBuilder(); try { URL url = new URL(request.getUrl().toString()); Map<String, String> headers = request.getRequestHeaders(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); //设置请求的header for (Map.Entry<String, String> entry : headers.entrySet()) { System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); connection.setRequestProperty(entry.getKey(), entry.getValue()); } //扫码请求登录网页cookies一定要设置,不然后台判断登录状态会出错 if (cookies != null && !cookies.isEmpty()) { connection.setRequestProperty("Cookie", cookies); } connection.setRequestMethod(request.getMethod()); connection.connect(); InputStream is = connection.getInputStream(); String encoding = connection.getContentEncoding(); if (encoding == null) { encoding = mDefaultEncoding; } BufferedReader r = new BufferedReader(new InputStreamReader(is, encoding)); String line; while ((line = r.readLine()) != null) { total.append(line); } is.close(); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } return total.toString(); }/** * 描述:根据指定路径读取css文件 * 作者:LeoHo * * @param * @return * @throw */ private String buildCss(String css) { StringBuilder contents = new StringBuilder(); InputStreamReader reader; try { reader = new InputStreamReader(mContext.getAssets().open(css), mDefaultEncoding); BufferedReader br = new BufferedReader(reader); String line; while ((line = br.readLine()) != null) { contents.append(line); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "<style type=\"text/css\">" + contents.toString().trim().replace("\n", "") + "</style>"; }/** * 描述:根据给定路径读取js文件 * 作者:LeoHo * * @param * @return * @throw */ private String buildJS(String jsPath) { StringBuilder contents = new StringBuilder(); InputStreamReader reader; try { reader = new InputStreamReader(mContext.getAssets().open(jsPath), mDefaultEncoding); BufferedReader br = new BufferedReader(reader); String line; while ((line = br.readLine()) != null) { contents.append(line); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "<script type=\"text/javascript\">" + contents.toString().trim().replace("\n", "") + "</script>"; }/** * 描述:给定的HTML中插入css和js代码 * 作者:LeoHo * * @param * @return * @throw */ private String inject(String page, String css, String js) { int headEnd = page.indexOf("</head>"); String res; if (headEnd > 0) { res = page.substring(0, headEnd) + css + page.substring(headEnd, page.length()); } else { res = "<head>" + css + "</head>" + page; } int bodyEnd = res.indexOf("</body>"); if (bodyEnd > 0) { res = res.substring(0, bodyEnd) + js + res.substring(bodyEnd, res.length()); } return res; }