Android控件之WebView应用分析

一、引言

WebView在Android平台上是一个特殊的View, 是基于webkit引擎、展现web页面的控件,内部实现是采用渲染引擎来展示view的内容,提供网页前进后退,网页放大,缩小,搜索等。Android的Webview在低版本和高版本采用了不同的webkit版本内核,在4.4及之后直接使用了Chrome内核。

WebView,顾名思义就是放网页的视图。现在很多APP都内置了Web网页,比如说很多电商平台,淘宝、京东等。WebView应用比较灵活,不需要升级客户端,只需要修改网页代码即可。一些经常变化的页面可以用WebView这种方式去加载网页。例如Banner中的页面需要经常更换,如果是用WebView显示的话,只修改修改html页面就行,而不需要升级客户端。

WebView的一些优点:

  • 可以直接显示和渲染web页面,直接显示网页
  • Webview可以直接用html文件(网络上或本地assets中)作布局
  • 和JavaScript交互调用

二、基本使用

2.1 添加权限

访问网络需要添加网络权限

    <uses-permission android:name="android.permission.INTERNET" />

2.2 简单设置

布局文件xml配置:

    <WebView
         android:id="@+id/main_web"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
    </WebView>

建议WebView的height属性设置为 match_parent 或者指定值,而非 wrap_content ,其各个父容器不允许设置height为 wrap_content ,否则可能导致异常发生。

Activity中的初始化代码:

    WebView webview = (WebView)findViewById(R.id.web_view);
    webview.getSettings().setJavaScriptEnabled(true); //启用js功能
    webview.setWebViewClient(new WebViewClient());
    webview.loadUrl("http://www.baidu.com"); //这样就会打开百度主页

可以通过调用webview.getSettings().set*()方法去设置一些浏览器的属性。
setJavaScriptEnabled()方法可以让浏览器支持JavaScript脚本。
setWebViewClient()方法作用是当需要从一个网页跳转到另一个网页时,我们希望还在当前的app内显示,而不是系统的浏览器里。
loadUrl()方法用于加载网页url。

2.3 辅助类

WebViewClient 当可能影响内容渲染的操作发生时会调用到该类,比如错误等...另外,可以通过重写 shouldOverrideUrlLoading() 来中断url的加载;
WebChromeClient 当可能影响webView UI的操作发生时会调用到该类,比如进度变化或者js提示框等;
WebSettings 功能设置,比如放大、缩小、是否允许js代码交互等。

三、定制化应用

3.1 加载html四种方式

WebView的加载方式有四种:

3.1.1 加载一个网页
webView.loadUrl("www.xmamiga.com");
3.1.2 加载APK包中的一个html页面(Asset资源)
webView.loadUrl("file:///android_asset/test.html");
3.1.3 加载手机本地的一个html页面的方法
webView.loadUrl("content://com.xmamiga.webview/sdcard/test.html");
3.1.4 使用webview显示html代码
webView.loadDataWithBaseURL(null,"<html><head><title> 欢迎您 </title></head>" +
        "<body><h2>使用webview显示 html代码</h2></body></html>", "text/html" , "utf-8", null);

在APP开发中,用最多的也就前面两种。

3.2 WebViewClient的使用

使用WebView基本都会使用这个类,WebViewClient主要帮助WebView处理各种通知、请求事件的,有以下常用方法:

  • onPageFinished 页面请求完成
  • onPageStarted 页面开始加载
  • shouldOverrideUrlLoading 拦截url
  • onReceivedError 访问错误时回调,例如访问网页时报错404,在这个方法回调的时候可以加载错误页面。

在打开网页,设置各种你想要做的事情,具体有:

//更新历史记录
doUpdateVisitedHistory(WebView view, String url, boolean isReload)
//应用程序重新请求网页数据
onFormResubmission(WebView view, Message dontResend, Message resend)
// 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次
onLoadResource(WebView view, String url)
//这个事件就是开始载入页面调用的,通常我们可以在这设定一个loading的页面,告诉用户程序在等待网络响应
onPageStarted(WebView view, String url, Bitmap favicon)
//在页面加载结束时调用。同样道理,我们知道一个页面载入完成,于是我们可以关闭loading 条,切换程序动作
onPageFinished(WebView view, String url)
// 报告错误信息
onReceivedError(WebView view, int errorCode, String description, String failingUrl)
// 获取返回信息授权请求
onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host,String realm)
//重写此方法可以让webview处理https请求
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) 
// WebView发生改变时调用
onScaleChanged(WebView view, float oldScale, float newScale)
// Key事件未被加载时调用
onUnhandledKeyEvent(WebView view, KeyEvent event)
// 重写此方法才能够处理在浏览器中的按键事件
shouldOverrideKeyEvent(WebView view, KeyEvent event)
//在点击请求的是链接是才会调用,重写此方法返回true表明点击网页里面的链接还是在当前的webview里跳转,不跳到浏览器那边
shouldOverrideUrlLoading(WebView view, String url) 

3.3 WebChromeClient的使用

使用WebView也基本都会使用这个类,WebChromeClient主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等,有以下常用方法:

  • onJsAlert webview不支持js的alert弹窗,需要自己监听然后通过dialog弹窗
  • onReceivedTitle 获取网页标题
  • onReceivedIcon 获取网页icon
  • onProgressChanged 加载进度回调

方法中的代码都是由Android端自己处理:

webView.setWebChromeClient(mWebChromeClient);

WebChromeClient mWebChromeClient = new WebChromeClient() {

    //获得网页的加载进度,显示在右上角的TextView控件中
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        if (newProgress < 100) {
            String progress = newProgress + "%";  //加载中
        } else {
          // 网页加载完成
        }
    }

    //获取Web页中的title用来设置自己界面中的title
    //当加载出错的时候,比如无网络,这时onReceiveTitle中获取的标题为 找不到该网页,
    //因此建议当触发onReceiveError时,不要使用获取到的title
    @Override
    public void onReceivedTitle(WebView view, String title) {
        MainActivity.this.setTitle(title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
        //
    }

    @Override
    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
        //
        return true;
    }

    @Override
    public void onCloseWindow(WebView window) {
    }

    //处理alert弹出框,html 弹框的一种方式
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        //
        return true;
    }

    //处理confirm弹出框
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult
            result) {
        //
        return true;
    }

    //处理prompt弹出框
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        //
        return true;
    }
};

3.4 Websettings配置

基本设置

WebSettings webSettings = webView.getSettings();

//支持缩放,默认为true。
webSettings.setSupportZoom(false);
//调整图片至适合webview的大小
webSettings.setUseWideViewPort(true);
// 缩放至屏幕的大小
webSettings.setLoadWithOverviewMode(true);
//设置默认编码
webSettings.setDefaultTextEncodingName("utf-8");
//设置自动加载图片
webSettings.setLoadsImagesAutomatically(true);

高级设置

//多窗口
supportMultipleWindows();
//允许访问文件
setAllowFileAccess(true);
//开启javascript
setJavaScriptEnabled(true);
//支持通过JS打开新窗口
setJavaScriptCanOpenWindowsAutomatically(true);
//提高渲染的优先级
webSettings.setRenderPriority(RenderPriority.HIGH);
//支持内容重新布局
setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);
//关闭webview中缓存
setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//支持插件
setPluginsEnabled(true);

四、WebView 的一些常用方法

4.1 缓存

优先使用缓存

webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

不使用缓存

webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

4.2 网页前进 & 后退

  • canGoForward()//是否可以前进
  • canGoBack() //是否可以后退
  • goBack()//后退
  • goForward()//前进
  • goBackOrForward(intsteps) //以当前的index为起始点前进或者后退到历史记录中指定的steps,如果steps为负数则为后退,正数则为前进

如果想按下返回键退回上一页,而不是退出webView,需要改写物理按键(返回键)的逻辑:

@Override
public boolean onKeyDown(int keyCode,KeyEvent event){
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        if (webview.canGoBack()) {
            webview.goBack();//返回上一界面
            return true;
        }else{
            System.exit(0);//退出程序
        }

    }
    return super.onKeyDown(keyCode,event);
}

4.3 清除缓存数据

  • clearCache(true);//清除网页访问留下的缓存,由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
  • clearHistory()//清除当前webview访问的历史记录,只会webview访问历史记录里的所有记录除了当前访问记录.
  • clearFormData()//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

4.4 判断WebView是否已经滚动到页面底端 & 顶端:

getScrollY() //方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离.
getHeight()或者getBottom() //方法都返回当前WebView这个容器的高度
getContentHeight()返回的是整个html的高度,但并不等同于当前整个页面的高度,因为WebView有缩放功能,所以当前整个页面的高度实际上应该是原始html的高度再乘上缩放比例.

if(webView.getScrollY() == 0){
    //处于顶端
}

if (webView.getContentHeight() * webView.getScale() == (webView.getHeight() + webView.getScrollY())) {
    //已经处于底端
}

4.5 退出优化

Webview调用destory时,Webview仍绑定在Activity上,这是由于自定义Webview构建时传入了该Activity的context对象,因此需要先从父容器中移除webview,然后再销毁webview,最后置空。代码如下:

@Override
protected void onDestroy() {
    if (mWebView != null) {
        mWebView.setWebViewClient(null);
        mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        mWebView.clearHistory();

        ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        mWebView.destroy();
        mWebView = null;
    }
    super.onDestroy();
}

4.6 WebView加载提升网页打开的速度

加载时先加载文本,后加载图片调用方式如下
WebSettings settings = wView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setBuiltInZoomControls(true);
settings.setBlockNetworkImage(true);
wWebView.setWebChromeClient(new WebChromeClient() {
    @Override  
    public void onProgressChanged(WebView view, int newProgress) {
        if (newProgress == 100) {
            // 网页加载完成
            loadDialog.dismiss();
            wWebView.getSettings().setBlockNetworkImage(false);
        } else {
            // 网页加载中
            loadDialog.show();
        }
    }
});
加入线程
runOnUiThread(new Runnable() {
    @Override
    public void run() {
        WebSettings wSet = wWebView.getSettings();
        wSet.setJavaScriptEnabled(true);
        webUrl = “http://www.baidu.com";
        wWebView.loadUrl(webUrl);
    }
});

五、Webview Js与本地程序互动

一般如时使用html的话,都或多或少的需要与本地应用互动。

5.1 Android调用Js

对于Android调用Js代码的方法有2种:

5.1.1 通过WebView的loadUrl()

将需要调用的Js代码以.html格式放到src/main/assets文件夹里(可远程),需要加载JS代码:javascript.html:

// 文本名:javascript
<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
        <title>Carson_Ho</title>

        // JS代码
        <script>
            // Android需要调用的方法
            function callJS(){
                alert("Android调用了JS的callJS方法");
            }
        </script>

    </head>

</html>

在Android里通过WebView设置调用Js代码

WebSettings webSettings = mWebView.getSettings();

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);
// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

// 先载入JS代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");

button.setOnClickListener(new View.OnClickListener() {
    @Override
        public void onClick(View v) {
        // 通过Handler发送消息
        mWebView.post(new Runnable() {
            @Override
                public void run() {

                // 注意调用的JS方法名要对应上
                // 调用javascript的callJS()方法
                mWebView.loadUrl("javascript:callJS()");
            }
        });

    }
});

特别注意:JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。onPageFinished()属于WebViewClient类的方法,主要在页面加载结束时调用。

5.1.2 通过WebView的evaluateJavascript()

该方法比第一种方法效率更高、使用更简洁。但该方法的执行不会使页面刷新,而第一种方法(loadUrl )的执行则会。缺点就是不支持Android 4.4 以前的版本,便现在Android手机基本上都在4.4之后了。

// 只需要将第一种方法的loadUrl()换成下面该方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
    @Override
        public void onReceiveValue(String value) {
        //此处为 js 返回的结果
    }
});

5.2 Js调用Android

对于Js调用Android代码的方法有3种:

5.2.1 通过WebView的addJavascriptInterface()进行对象映射

Java 与 Js 交互需要定义一个带有 @JavascriptInterface 注解方法的对象,如AndroidtoJs(AndroidtoJs.java):

// 继承自Object类
public class AndroidtoJs extends Object {

    // 定义JS需要调用的方法
    // 被JS调用的方法必须加入@JavascriptInterface注解
    @JavascriptInterface
    public void hello(String msg) {
        System.out.println("JS调用了Android的hello方法");
    }
}

将需要调用的Js代码以.html格式放到src/main/assets文件夹里,需要加载Js代码:javascript.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Amiga</title>  
        <script>


            function callAndroid(){
                // 由于对象映射,所以调用test对象等于调用Android映射的对象
                test.hello("js调用了android中的hello方法");
            }
        </script>
    </head>
    <body>
        //点击按钮则调用callAndroid函数
        <button type="button" id="button1" onclick="callAndroid()"></button>
    </body>
</html>

在Android里通过WebView设置Android类与Js代码的映射

mWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);

// 通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:Javascript对象名
//参数2:Java对象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS类对象映射到js的test对象

// 加载JS代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
5.2.2 通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url

在Js约定所需要的Url协议,Js代码:javascript.html(以.html格式放到src/main/assets文件夹里):

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
        <title>xmamiga</title>

        <script>
            function callAndroid(){
                /*约定的url协议为:js://webview?arg1=111&arg2=222*/
                document.location = "js://webview?arg1=111&arg2=222";
            }
        </script>
    </head>

    <!-- 点击按钮则调用callAndroid()方法  -->
    <body>
        <button type="button" id="button1" onclick="callAndroid()">点击调用Android代码</button>
    </body>
</html>

当该Js通过Android的mWebView.loadUrl("file:///android_asset/javascript.html")加载后,就会回调shouldOverrideUrlLoading (),在Android通过WebViewClient复写shouldOverrideUrlLoading ()

WebSettings webSettings = mWebView.getSettings();

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);
// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

// 步骤1:加载JS代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");


// 复写WebViewClient类的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
    @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

        // 步骤2:根据协议的参数,判断是否是所需要的url
        // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
        //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

        Uri uri = Uri.parse(url);
        // 如果url的协议 = 预先约定的 js 协议
        // 就解析往下解析参数
        if ( uri.getScheme().equals("js")) {

            // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
            // 所以拦截url,下面JS开始调用Android需要的方法
            if (uri.getAuthority().equals("webview")) {

                //  步骤3:
                // 执行JS所需要调用的逻辑
                System.out.println("js调用了Android的方法");
                // 可以在协议上带有参数并传递到Android上
                HashMap<String, String> params = new HashMap<>();
                Set<String> collection = uri.getQueryParameterNames();

            }

            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
}


5.2.3 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息

Android通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调分别拦截JS对话框(即上述三个方法),得到他们的消息内容,然后解析即可。

加载Js代码,javascript.html(以.html格式放到src/main/assets文件夹里)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>xmamiga</title>

        <script>

            function clickprompt(){
                // 调用prompt()
                var result=prompt("js://demo?arg1=111&arg2=222");
                alert("demo " + result);
            }

        </script>
    </head>

    <!-- 点击按钮则调用clickprompt()  -->
    <body>
        <button type="button" id="button1" onclick="clickprompt()">点击调用Android代码</button>
    </body>
</html>

当使用mWebView.loadUrl("file:///android_asset/javascript.html")加载了上述Js代码后,就会触发回调onJsPrompt(); 在Android通过WebChromeClient复写onJsPrompt()

WebSettings webSettings = mWebView.getSettings();

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);
// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

// 先加载JS代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");


mWebView.setWebChromeClient(new WebChromeClient() {
    // 拦截输入框(原理同方式2)
    // 参数message:代表promt()的内容(不是url)
    // 参数result:代表输入框的返回值
    @Override
        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 根据协议的参数,判断是否是所需要的url(原理同方式2)
        // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
        //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

        Uri uri = Uri.parse(message);
        // 如果url的协议 = 预先约定的 js 协议
        // 就解析往下解析参数
        if ( uri.getScheme().equals("js")) {

            // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
            // 所以拦截url,下面JS开始调用Android需要的方法
            if (uri.getAuthority().equals("webview")) {

                //
                // 执行JS所需要调用的逻辑
                System.out.println("js调用了Android的方法");
                // 可以在协议上带有参数并传递到Android上
                HashMap<String, String> params = new HashMap<>();
                Set<String> collection = uri.getQueryParameterNames();

                //参数result:代表消息框的返回值(输入值)
                result.confirm("js调用了Android的方法成功啦");
            }
            return true;
        }
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }

    // 通过alert()和confirm()拦截的原理相同,此处不作过多讲述

    // 拦截JS的警告框
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        return super.onJsAlert(view, url, message, result);
    }

    // 拦截JS的确认框
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        return super.onJsConfirm(view, url, message, result);
    }
}


六、总结

WebView控件功能强大,除了具有一般View的属性和设置外,还可以对url请求、页面加载、渲染、页面交互进行强大的处理。学习该组件可以为APP开发提升扩展性。


参考: