Andorid浏览器:Native与H5交互的那些事

3,854 阅读33分钟

目录
一,简介
二,WebView,WebSettings、WebViewClient、WebChromeClient方法
三,Native与Js交互
四,腾讯浏览服务X5内核
五,X5内核WebView +全屏视频 + JsBridge框架

一,简介

这部分主要介绍下 WebView,WebView 是一个用来显示 Web 网页的控件,继承自 AbsoluteLayout,和使用系统其他控件没什么区别,只是 WeView 控件方法比较多比较丰富。因为它就是一个微型浏览器,包含一个浏览器该有的基本功能,例如:滚动、缩放、前进、后退下一页、搜索、执行 Js等功能。

在 Android 4.4 之前使用 WebKit 作为渲染内核,4.4 之后采用 chrome 内核。Api 使用兼容低版本。

二,WebView,WebSettings、WebViewClient、WebChromeClient方法

主要包含 WebView 的使用方法。 我们基于这些方法能扩展很多其他功能,例如:JsBridge、缓存等。
WebView方法:

方法 说明
void loadUrl(String url) 加载网络链接 url
boolean canGoBack() 判断 WebView 当前是否可以返回上一页
goBack() 回退到上一页
boolean canGoForward() 判断 WebView 当前是否可以向前一页
goForward() 回退到前一页
onPause() 类似 Activity 生命周期,页面进入后台不可见状态
pauseTimers() 方法面向全局整个应用程序的webview,它会暂停所有webview的layout,parsing,JavaScript Timer。当程序进入后台时,该方法的调用可以降低CPU功耗。
onResume() 在调用 onPause()后,可以调用该方法来恢复 WebView 的运行
resumeTimers() 恢复pauseTimers时的所有操作。(注:pauseTimers和resumeTimers 方法必须一起使用,否则再使用其它场景下的 WebView 会有问题)
destroy() 销毁 WebView
clearHistory() 清除当前 WebView 访问的历史记录。
clearCache(boolean includeDiskFiles): 清空网页访问留下的缓存数据。需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。该方法接受一个参数,从命名即可看出作用。若设为false,则只清空内存里的资源缓存,而不清空磁盘里的。
reload() 重新加载当前请求
setLayerType(int layerType, Paint paint) 设置硬件加速、软件加速
removeAllViews() 清除子view。
clearSslPreferences(): 清除ssl信息
clearMatches() 清除网页查找的高亮匹配字符
removeJavascriptInterface(String interfaceName) 删除interfaceName 对应的注入对象
addJavascriptInterface(Object object,String interfaceName) 注入 java 对象
setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) 设置垂直方向滚动条。
setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) 设置横向滚动条
loadUrl(String url, Map additionalHttpHeaders) 加载制定url并携带http header数据
stopLoading() 停止 WebView 当前加载
freeMemory() 释放内存,不过貌似不好用。
clearFormData() 清除自动完成填充的表单数据。需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

WebSettings方法

方法 说明
setJavaScriptEnabled(boolean flag) 是否支持 Js 使用
setCacheMode(int mode) 设置 WebView 的缓存模式
setAppCacheEnabled(boolean flag) 是否启用缓存模式
setAppCachePath(String appCachePath) Android 私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录
setGeolocationEnabled 是否支持定位
setGeolocationDatabasePath 设置定位数据库路径
setSupportZoom(boolean support) 是否支持缩放。
setTextZoom(int textZoom) 设置页面中文本缩放百分比,默认100%
setAllowFileAccess(boolean allow) 是否允许加载本地 html 文件/false
setDatabaseEnabled(boolean flag) 是否开启数据库缓存
setDomStorageEnabled(boolean flag) 是否开启DOM缓存
setUserAgentString(String ua) 设置 UserAgent 属性
setLoadsImagesAutomatically(boolean flag) 支持自动加载图片
boolean getLoadsImagesAutomatically() 是否支持自动加载图片
setMixedContentMode 设置混合模式,当一个安全的https请求加载不安全的http时
setLoadWithOverviewMode 设置webview加载在override模式,就是缩小内容以适应屏幕宽,当加载的内容宽大于Webview控制的宽时
setDisplayZoomControls 设置webview是否显示缩放控制按钮

项目中使用设置:

 WebSettings settings = mWebView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setDatabaseEnabled(false);
        settings.setSaveFormData(false);
        settings.setBuiltInZoomControls(true);
        if (Build.VERSION.SDK_INT >= 11)
            settings.setDisplayZoomControls(false);
        settings.setAllowFileAccess(true);
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);
        settings.setDomStorageEnabled(true);
        settings.setAppCacheEnabled(true);
        settings.setGeolocationEnabled(true);
        settings.setGeolocationDatabasePath(getActivity().getCacheDir().toString());
        settings.setUserAgentString(settings.getUserAgentString() + " " + DeviceUtil.getUserAgent(getActivity()));
        // android 5.0以上默认不支持Mixed Content
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            settings.setMixedContentMode(
                    WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
        }

WebViewClient方法:
主要作用:主要帮助WebView处理各种通知、请求事件的

方法 说明
onPageStarted(WebView view, String url, Bitmap favicon) WebView 开始加载页面时回调,一次Frame加载对应一次回调
onLoadResource(WebView view, String url) WebView 加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前 url 对应有缓存,否则就会加载
shouldInterceptRequest(WebView view, String url) WebView 可以拦截某一次的 request 来返回我们自己加载的数据,这个方法在后面缓存会有很大作用
shouldOverrideUrlLoading(WebView view, String url) 是否在 WebView 内加载页面,返回true代表当前Webview完成此次加载,false代表交给系统浏览器加载
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) WebView ssl 访问证书出错,handler.cancel()取消加载,handler.proceed()对然错误也继续加载
onPageFinished(WebView view, String url) WebView 完成加载页面时回调,一次Frame加载对应一次回调
onReceivedError(WebView view, int errorCode, String description, String failingUrl) WebView 访问 url 出错

WebChromeClient方法
要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等

方法 说明
onConsoleMessage(String message, int lineNumber,String sourceID) 输出 Web 端日志
onProgressChanged(WebView view, int newProgress) 当前 WebView 加载网页进度
onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) 处理 JS 中的 Prompt对话框
onJsAlert(WebView view, String url, String message, JsResult result) Js 中调用 alert() 函数,产生的对话框
onReceivedTitle(WebView view, String title): 接收web页面的 Title
onReceivedIcon(WebView view, Bitmap icon) 接收web页面的icon
View getVideoLoadingProgressView() 获取在全屏视频正在缓冲的同时显示的视图。宿主应用程序可以重写此方法,以提供包含旋转器或类似物的视图。
onShowCustomView(View view, CustomViewCallback callback) 网页中有H5播放flash video的时候按下全屏按钮将会调用到这个方法,一般用作设置网页播放全屏操作
onHideCustomView() 取消全屏方法

三,Native与Js交互

首先说Native调用js代码,或者说执行js代码很简单:

Native 调用 Js:mWebView.loadUrl(js);

Js代码调用Native的代码,一共有三种方式:

  1. 使用系统方法 addJavascriptInterface 注入 java 对象来实现。---这是标准方式
  2. 利用 WebViewClient 中 shouldOverrideUrlLoading (WebView view, String url) 接口,拦截操作。这个就是很多公司在用的 scheme 方式,通过制定url协议,双方各自解析,使用iframe来调用native代码,实现互通。 --- 这是最常用的方式
  3. 利用 WebChromeClient 中的 onJsAlert、onJsConfirm、onJsPrompt 提示接口,同样也是拦截操作。--- 不常用

标准方式使用清单:

//开启Js可用
mWebView.getSettings().setJavaScriptEnabled(true);

// 创建要注入的 Java 类
public class NativeInterface {

    private Context mContext;

    public NativeInterface(Context context) {
        mContext = context;
    }

   // 使用JavascriptInterface注解标注的方法可以被JS代码调用
    @JavascriptInterface
    public void hello() {
        Toast.makeText(mContext, "hello", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void hello(String params) {
        Toast.makeText(mContext, params, Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public String getAndroid() {
        Toast.makeText(mContext, "getAndroid", Toast.LENGTH_SHORT).show();
        return "Android data";
    }

}

// WebView 注入即可,第二个参数是注入的java对象的别名
mWebView.addJavascriptInterface(new NativeInterface(this), "AndroidNative");

//Js编写
<script>
    function callHello(){
        AndroidNative.hello();
    }

    function callHello1(){
        // 在js代码中别名AndroidNative就相当于java中实例对象new NativeInterface(this),可调用方法
        AndroidNative.hello('hello Android');
    }

    function callAndroid(){
        var temp = AndroidNative.getAndroid();
        console.log(temp);
        alert(temp);
    }  
</script>

最常用的方式使用清单:
shouldOverrideUrlLoading接口下,拦截js回调操作,使用scheme协议,即scheme://host[:post]/path/jsonparams,双方定义格式解析

假如 :协定的格式为 appname://jsbridge/call/func/jsonparams
jsonparams 的是格式为:
{ "callback" : "js用于区分此操作的标识", // 这一项是必须的
"key1": value1,
"key2": value2,
"key3": value3
……
}

  // js获取userinfo为例:
  // func = userinfo
  // jsonparams 的是格式为:
  //  { "callback" : "cbUserInfo_1"
  //  }
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (!TextUtils.isEmpty(url) && url.startsWith("appname://jsbridge/call/")) {
         String[] values = url.replace("appname://jsbridge/call/""").split("/"2);
         String func = values[0];
         String params = values[1];
         //  1,js调用native代码,处理Native端的逻辑
         switch(func) {
             case "userinfo":
                JSONObject job = new JSONObject(params);
                String callback = job.optString("callback"); // 此处是“cbUserInfo_1”
                // 客户端获取一些信息等等
                JSONObject result = new JSONObject();
                result.put("isLogin",Config.isLogin());
                result.put("nickName", Config.getUserName());
                result.put("userId", Config.getUserId());
                result.put("headimgUrl", Config.getPicUrl());
              /// 等等

                // 2, Native调用js代码将处理的结果返回给js,Js需要用callback区分此次请求   

                String js = "callback 和 params组成js";
// 这里需要把callback和params组成js,需要和前端商定,然后使用loadUrl("javascript:" + js) 就可以将结果传给js
                 if (mWebView != null) {
                       mWebView.loadUrl("javascript:" + js);
                 }
             break;
         }
         return true;
    }
  }

以上就是JsBridge的原理,当然,在业务复杂时可以进行封装

四,腾讯浏览服务X5内核

腾讯浏览服务网址:https://x5.tencent.com/
接入指南:https://x5.tencent.com/tbs/guide/sdkInit.html
使用很简单和系统的WebView一样。

  1. 为什么要更换X5内核
    Android 4.4以下web内核是WebKit,4.4 之后采用 chrome 内核,当前端在开发时,会出现前端使用的新特性在低版本安卓手机上不能表现出来,所以需要统一内核

  2. X5内核的技术特性
    腾讯浏览服务是致力于优化移动端webview体验的整套解决方案。该方案由SDK、手机QQ浏览器X5内核和X5云端服务组成,解决移动端webview使用过程中出现的一切问题,优化用户的浏览体验。同时,腾讯还将持续提供后续的更新和优化,为开发者提供最新最优秀的功能和服务。
    其中,SDK是通过共享使用用户手机上微信、手机QQ、空间等软件已经下载好的X5内核,低成本实现对系统webview的替代。该SDK大小只有200+K,接入时仅需修改几行代码。
    其中,X5内核相对于系统webview,具有下述明显优势:
    1) 速度快:相比系统webview的网页打开速度有30+%的提升;
    2) 省流量:使用云端优化技术使流量节省20+%;
    3) 更安全:安全问题可以在24小时内修复;
    4) 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;
    5) 兼容好:无系统内核的碎片化问题,更少的兼容性问题;
    6) 体验优:支持夜间模式、适屏排版、字体设置等浏览增强功能;
    7) 功能全:在Html5、ES6上有更完整支持;
    8) 更强大:集成强大的视频播放器,支持视频格式远多于系统webview;
    9) 视频和文件格式的支持x5内核多于系统内核
    10) 防劫持是x5内核的一大亮点

  3. 下载TbsSDK,会有很多文档:

tbssdk
tbssdk
常见FQA
常见FQA
常见FQA
常见FQA

五,X5内核WebView +全屏视频 + JsBridge框架

三个核心类封装
三个核心类封装
// 注意:代码中的WebView,均为腾讯的WebView
import com.tencent.smtt.sdk.WebChromeClient;
import com.tencent.smtt.sdk.WebView;
import com.tencent.smtt.sdk.WebChromeClient;

// X5WebView.java:继承自腾讯的x5内核的WebView
public class X5WebView extends WebView {

    public static final String TAG = X5WebView.class.getSimpleName();

    private boolean addedJavascriptInterface;

    public X5WebView(Context context) {
        super(context);
        addedJavascriptInterface = false;
    }

    public X5WebView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        addedJavascriptInterface = false;
    }

    public X5WebView(Context context, AttributeSet attributeSet, int i) {
        super(context, attributeSet, i);
        addedJavascriptInterface = false;
    }

    public X5WebView(Context context, AttributeSet attributeSet, int i, boolean b) {
        super(context, attributeSet, i, b);
        addedJavascriptInterface = false;
    }

    public X5WebView(Context context, AttributeSet attributeSet, int i, Map<String, Object> map, boolean b) {
        super(context, attributeSet, i, map, b);
        addedJavascriptInterface = false;
    }

    @Override
    @SuppressLint("SetJavaScriptEnabled")
    public void setWebChromeClient(WebChromeClient client) {
        getSettings().setJavaScriptEnabled(true);
        super.setWebChromeClient(client);
    }

    @Override
    public void loadData(String data, String mimeType, String encoding) {
        addJavascriptInterface();
        super.loadData(data, mimeType, encoding);
    }

    @Override
    public void loadDataWithBaseURL(String baseUrl, String data,
                                    String mimeType, String encoding,
                                    String historyUrl)
 
{
        addJavascriptInterface();
        super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
    }

    @Override
    public void loadUrl(String url) {
        addJavascriptInterface();
        super.loadUrl(url);
    }

    @Override
    public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
        addJavascriptInterface();
        super.loadUrl(url, additionalHttpHeaders);
    }

    private void addJavascriptInterface() {
        if (!addedJavascriptInterface) {
            // Add javascript interface to be called when the video ends (must be done before page load)
            addJavascriptInterface(new Object() {
            }, "_VideoEnabledWebView"); // Must match Javascript interface name of VideoEnabledWebChromeClient
            addedJavascriptInterface = true;
        }
    }
// X5WebViewClient.java: 继承自腾讯的x5内核的WebViewClient
public class X5WebViewClient extends WebViewClient {

}
// X5WebChromeClient.java :构建一个全屏显示Video的WebChromeClient, 网上可以找到实现,只是做了继承X5的WebChromeClient
public class X5WebChromeClient extends WebChromeClient implements MediaPlayer.OnPreparedListenerMediaPlayer.OnCompletionListenerMediaPlayer.OnErrorListener {

    // 将被隐藏的View
    private View willHiddenView;
    // 将全屏显示的ViewGroup
    private ViewGroup willDisplayedViewGroup;
    // 加载View
    private View loadingView;
    // 核心的WebView
    private X5WebView webView;
    // 进度条
    private ProgressBar mProgressBar;

    // 表明是否在使用自定义View 显示全屏Video
    private boolean isVideoFullscreen;

    // 保存全屏Video容器
    private FrameLayout videoViewContainer;
    // 保存自定义View回调
    private IX5WebChromeClient.CustomViewCallback videoViewCallback;

    // 全屏Video回调
    private ToggledFullscreenCallback toggledFullscreenCallback;
    // 接收标题回调
    private onReceivedTitleCallback receivedTitleCallback;


    /**
     * Never use this constructor alone.
     * This constructor allows this class to be defined as an inline inner class in which the user can override methods
     */

    public X5WebChromeClient() {
    }

    /**
     * 构建一个全屏显示Video的WebChromeClient
     *
     * @param willHiddenView           当video全屏时,activity布局中的应该被隐藏的View
     * @param willDisplayedViewGroup   activity布局中,将要显示的 ViewGroup,特别是你想使此ViewGroup全屏显示。
     */

    public X5WebChromeClient(View willHiddenView, ViewGroup willDisplayedViewGroup) {
        this.willHiddenView = willHiddenView;
        this.willDisplayedViewGroup = willDisplayedViewGroup;
        this.loadingView = null;
        this.webView = null;
        this.isVideoFullscreen = false;
    }

    /**
     * 构建一个全屏显示Video的WebChromeClient
     *
     * @param willHiddenView          当video全屏时,activity布局中的应该被隐藏的View
     * @param willDisplayedViewGroup  activity布局中,将要显示的 ViewGroup,特别是你想使此ViewGroup全屏显示
     * @param loadingView             加载视频时显示的视图(通常仅在API级别<11时使用)。必须被预先inflate,且无父布局
     */

    public X5WebChromeClient(View willHiddenView, ViewGroup willDisplayedViewGroup, View loadingView) {
        this.willHiddenView = willHiddenView;
        this.willDisplayedViewGroup = willDisplayedViewGroup;
        this.loadingView = loadingView;
        this.webView = null;
        this.isVideoFullscreen = false;
    }

    /**
     * 构建一个全屏显示Video的WebChromeClient
     *
     * @param willHiddenView         当video全屏时,activity布局中的应该被隐藏的View
     * @param willDisplayedViewGroup activity布局中,将要显示的 ViewGroup,特别是你想使此ViewGroup全屏显示
     * @param loadingView            加载视频时显示的视图(通常仅在API级别<11时使用)。必须被预先inflate,且无父布局
     * @param webView                X5WebView
     *
     * 通过它将使X5WebChromeClient检测HTML5视频结束事件并退出全屏。
     * 注意:为了使HTML5视频结束事件正常工作,网页只能包含一个视频标记。如果需要的话,可以改进这一点(参见javascript代码)。
     */

    public X5WebChromeClient(View willHiddenView, ViewGroup willDisplayedViewGroup, View loadingView, X5WebView webView) {
        this.willHiddenView = willHiddenView;
        this.willDisplayedViewGroup = willDisplayedViewGroup;
        this.loadingView = loadingView;
        this.webView = webView;
        this.isVideoFullscreen = false;
    }

    /**
     * 手动 进去全屏时 调用
     * @param view
     * @param callback
     */

    @Override
    public void onShowCustomView(View view, IX5WebChromeClient.CustomViewCallback callback) {
        if (view instanceof FrameLayout) {
            PalLog.d(X5WebView.TAG, "onShowCustomView");
            // A video wants to be shown
            FrameLayout frameLayout = (FrameLayout) view;
            // 焦点View
            View focusedChild = frameLayout.getFocusedChild();
            // 保存Video相关变量
            this.isVideoFullscreen = true;
            this.videoViewContainer = frameLayout;
            this.videoViewCallback = callback;
            // 隐藏non-video的View
            willHiddenView.setVisibility(View.GONE);
            // 添加video的View并显示
            willDisplayedViewGroup.addView(videoViewContainer, new RelativeLayout.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.MATCH_PARENT));
            willDisplayedViewGroup.setVisibility(View.VISIBLE);

            // 焦点View是VideoView
            if (focusedChild instanceof VideoView) {
                // VideoView (typically API level <11)
                VideoView videoView = (VideoView) focusedChild;
                // VideoView设置准备监听器,完成监听器,错误监听器
                videoView.setOnPreparedListener(this);
                videoView.setOnCompletionListener(this);
                videoView.setOnErrorListener(this);
            } else {
                // Usually android.webkit.HTML5VideoFullScreen$VideoSurfaceView, sometimes android.webkit.HTML5VideoFullScreen$VideoTextureView
                // HTML5VideoFullScreen (typically API level 11+)
                // Handle HTML5 video ended event
                if (webView != null && webView.getSettings().getJavaScriptEnabled()) {
                    // Run javascript code that detects the video end and notifies the interface
                    String js = "javascript:";
                    js += "_ytrp_html5_video = document.getElementsByTagName('video')[0];";
                    js += "if (_ytrp_html5_video !== undefined) {";
                    {
                        js += "function _ytrp_html5_video_ended() {";
                        {
                            js += "_ytrp_html5_video.removeEventListener('ended', _ytrp_html5_video_ended);";
                            js += "_VideoEnabledWebView.notifyVideoEnd();"// Must match Javascript interface name and method of VideoEnableWebView
                        }
                        js += "}";
                        js += "_ytrp_html5_video.addEventListener('ended', _ytrp_html5_video_ended);";
                    }
                    js += "}";
                    webView.loadUrl(js);
                }
            }

            // 最后通知全屏改变
            if (toggledFullscreenCallback != null) {
                toggledFullscreenCallback.toggledFullscreen(true);
            }
        }
    }

    @Override
    public void onShowCustomView(View view, int requestedOrientation, IX5WebChromeClient.CustomViewCallback callback) // Only available in API level 14+
    
{
        onShowCustomView(view, callback);
    }

    /**
     * 手动取消全屏时调用
     */

    @Override
    public void onHideCustomView() {
        PalLog.d(X5WebView.TAG, "onHideCustomView");
        // This method must be manually (internally) called on video end in the case of VideoView (typically API level <11)
        // This method must be manually (internally) called on video end in the case of HTML5VideoFullScreen (typically API level 11+) because it's not always called automatically
        // This method must be manually (internally) called on back key press (from this class' onBackPressed() method)
        if (isVideoFullscreen) {
            // Hide the video view, remove it, and show the non-video view
            willDisplayedViewGroup.setVisibility(View.GONE);//播放视频的
            willDisplayedViewGroup.removeView(videoViewContainer);
            willHiddenView.setVisibility(View.VISIBLE);

            // Call back
            if (videoViewCallback != null) {
                videoViewCallback.onCustomViewHidden();
            }

            // Reset video related variables
            isVideoFullscreen = false;
            videoViewContainer = null;
            videoViewCallback = null;

            // Notify full-screen change
            if (toggledFullscreenCallback != null) {
                toggledFullscreenCallback.toggledFullscreen(false);
            }
        }
    }

    /**
     * Video will start loading, only called in the case of VideoView (typically API level <11)
     * @return
     */

    @Override
    public View getVideoLoadingProgressView() {
        if (loadingView != null) {
            loadingView.setVisibility(View.VISIBLE);
            return loadingView;
        } else {
            return super.getVideoLoadingProgressView();
        }
    }

    /**
     * Video will start playing, only called in the case of VideoView (typically API level <11)
     * @param mp
     */

    @Override
    public void onPrepared(MediaPlayer mp) {
        PalLog.d(X5WebView.TAG, "onPrepared");
        if (loadingView != null) {
            loadingView.setVisibility(View.GONE);
        }
    }

    /**
     * Video finished playing, only called in the case of VideoView (typically API level <11)
     * @param mp
     */

    @Override
    public void onCompletion(MediaPlayer mp) {
        PalLog.d(X5WebView.TAG, "onCompletion");
        onHideCustomView();
    }

    /**
     *  Error while playing video, only called in the case of VideoView (typically API level <11)
     * @param mp
     * @param what
     * @param extra
     * @return
     */

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // By returning false, onCompletion() will be called
        return false;
    }

    /**
     * 设置标题回调
     * @param callback
     */

    public void setOnReceivedTitleCallback(onReceivedTitleCallback callback) {
        receivedTitleCallback = callback;
    }

    /**
     * 设置进度条
     * @param progressBar
     */

    public void setProgressBar(ProgressBar progressBar) {
        this.mProgressBar = progressBar;
    }

    /**
     * 指示是否使用自定义视图(通常为全屏)显示视频
     *
     * @return true 使用自定义视图(通常为全屏)显示视频
     */

    public boolean isVideoFullscreen() {
        return isVideoFullscreen;
    }

    /**
     * 设置全屏切换回调
     *
     * @param callback 全屏切换回调
     */

    public void setOnToggledFullscreen(ToggledFullscreenCallback callback) {
        this.toggledFullscreenCallback = callback;
    }

    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
        if (mProgressBar != null) {
            mProgressBar.setProgress(newProgress);
            if (newProgress == 100) {
                // onPageFinished
                mProgressBar.setVisibility(View.GONE);
            }
        }
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
        if (receivedTitleCallback != null) {
            receivedTitleCallback.onReceivedTitle(title);
        }
    }

    /**
     * Notifies the class that the back key has been pressed by the user.
     * This must be called from the Activity's onBackPressed(), and if it returns false, the activity itself should handle it. Otherwise don't do anything.
     *
     * @return Returns true if the event was handled, and false if it is not (video view is not visible)
     */

    public boolean onBackPressed() {
        if (isVideoFullscreen) {
            onHideCustomView();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {

        return true;
    }

    /**
     * 切换全屏,取消全屏时回调
     */

    public interface ToggledFullscreenCallback {
        public void toggledFullscreen(boolean fullscreen);
    }

    /**
     * 接收标题回调
     */

    public interface onReceivedTitleCallback {
        public void onReceivedTitle(String title);
    }
}
// Native调Js代码的接口
public interface JSCallBack {
    // callback 和 params 是 上面“三,Native与Js交互# 最常见方式的使用清单”
    void jsCallback(String callback, String params);
    void jsCallback(String callback, Object object);
}

下面重点介绍是Js调用Native代码的封装AppJsBridge.java

// IJSBridge是Js调用Native代码的接口
public interface IJSBridge {
   // 见“三,Js与Native交互”
    void processJsBridge(String func, String params);
    void setJSCallBack(JSCallBack jsCallBack);
}
// AppJsBridge.java  是Js调用Native代码的封装
public class AppJSBridge implements IJSBridge {
    private static final String TAG = "AppJSBridge";

    public static final String KEY_CALLBACK = "callback";

   // Activity
    private BaseActivity mActivity;
    // 实际执行Native代码的接口
    private OnInvokeCallback mOnInvokeCallback;
    // 客户端执行js代码,把结果传给js
    private JSCallBack mJSCallBack;
    //当前页面的host,用于判断白名单
    private String mCurHost; 

    //白名单
    private List<JSBridgeSpecialBean> jsList;

    @Override
    public void setJSCallBack(JSCallBack jsCallBack) {
        mJSCallBack = jsCallBack;
    }

    /**
     * 是否是白名单
     */

    private boolean isJsBridgeSpecial(String func) {
        if (jsList == null || mCurHost == null)
            return false;
        for (JSBridgeSpecialBean jsSpecial : jsList) {
            if (jsSpecial.allow == null || jsSpecial.allow.isEmpty()) continue;
            if (jsSpecial.domain.equals(JSBridgeSpecialBean.TAG_ALL) ||
                    mCurHost.endsWith(jsSpecial.domain.toLowerCase())) {
                if (jsSpecial.allow.get(0).equals(JSBridgeSpecialBean.TAG_ALL))
                    return true;
                else if (jsSpecial.allow.contains(func))
                    return true;
            }
        }
        return false;
    }


    @Override
    public void processJsBridge(String func, String params, String callbackId) {
        PalLog.d(TAG, "func=" + func);
        PalLog.d(TAG, "params=" + params);
        if (!isJsBridgeSpecial(func)) {
            PalLog.e(TAG, func + " 不属于白名单,禁止使用该JS接口");
            return;
        }
        switch (func) {

            case "deviceInfo"//获取设备信息
                getDeviceInfo(params);
                break;
            case "userInfo"//获取用户信息
                getUserInfo(params);
                break;
            /// 等等其他case
            default:
                PalLog.e(TAG, "无法处理的事件: " + func);
                break;
        }
    }

    /**
     * 获取用户信息
     */

    private void getUserInfo(String params) {
        if (mJSCallBack == nullreturn;
        try {
            // 1,执行Native代码
            JSONObject job = new JSONObject(params);
            String callback = job.optString(KEY_CALLBACK);
            // 从客户端获取一些信息等等
            JSONObject result = new JSONObject();
            result.put("isLogin", Config.isLogin());
            result.put("nickName", Config.getUserName());
            result.put("userId", Config.getUserId());
            result.put("headimgUrl", Config.getPicUrl());
            /// ....
            // 2,Native调用Js代码,将结果回传给JS
            mJSCallBack.jsCallback(callback, result.toString());
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取设备信息
     */

    private void getDeviceInfo(String params) {
        if (mJSCallBack == nullreturn;
        try {
            // 1,执行Native代码
            JSONObject job = new JSONObject(params);
            String callback = job.optString(KEY_CALLBACK);
            // 从客户端获取一些信息等等
            JSONObject result = new JSONObject();
            result.put("os""Android");
            result.put("version", Config.getVersion());
            result.put("deviceId", Config.getVersion());
            ///...
            // 2,Native调用Js代码,将结果回传给JS
            mJSCallBack.jsCallback(callback, result.toString());
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}