【浏览器技术】解锁X5内核WebView同层渲染能力

5,716

前言

WebView同层渲染,并不是一个新技术,国内一线互联网产品广泛应用,比如小程序的原生组件,电商H5嵌原生播放器等场景;

场景分析技术博客
微信小程序小程序同层渲染原理剖析
百度小程序【走进小程序原理】揭秘组件同层渲染
淘宝系播放器618淘系前端技术分享

如果您了解其原理,会发现这玩意在Android端,需要修改浏览器内核才能搞定,所以上手难度高;以至于读完文章,心血澎湃直呼牛逼,但是回过头翻看原生WebView代码,并没有找到合适的API;

同层渲染简介

在Android平台,H5内容依托WebView视图渲染,它和原生的View是独立平等的关系, 从绘制层次上来看,WebView和原生必然存在相互覆盖遮挡的,更没法做到同步滚动;

在网页DOM树中,间杂一部分原生的组件,且保留原本的层次和样式,这就是同层渲染;

图片来源微信技术文章

同层渲染能解决什么问题?

使用web前端技术实现困难,或者稳定性、性能受限等情况

例如:视频播放器、地图、游戏引擎、直播推拉流、摄像头预览等场景;

领略X5内核浏览器同层渲染

准备工作:

准备一个占位标签

X5同层渲染的原理是用原生接管在H5页面里的特定标签,所以得准备一个H5页面,然后插入一个自定义的标签, 标签名可以随意定义,比如mytag,样式就按照标准的css来设置

<mytag id = "mytag" src="https://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4"
       style="position:absolute;width:350px; height:500px;">
    占位的标签
</mytag>

强制开启X5WebView同层渲染

X5同层渲染能力默认是关闭的,通过云端开关控制,通过分析发现,可以强制修改本地SP属性,强制打开

if (mWebView.getX5WebViewExtension()!=null){
   //强制设置EMBEDDED云控开关enable
   SharedPreferences tbs_public_settings = getSharedPreferences("tbs_public_settings", Context.MODE_PRIVATE);
   SharedPreferences.Editor edit = tbs_public_settings.edit();
   edit.putInt("MTT_CORE_EMBEDDED_WIDGET_ENABLE",1);
   edit.apply();
}else {
   Log.d(TAG, "init: 非x5内核");
}

向浏览器注册目标占位标签的原生控件

使用registerEmbeddedWidget方法,可以想浏览器内核注册,需要原生来接管的占位的标签,第一个参数是需要接管的标签名,第二个参数是工厂创建对应原生标签对象的工厂接口

//注册dom树中占位标签,创建对应的原生组件
boolean result = mWebView.getX5WebViewExtension().registerEmbeddedWidget(new String[]{"mytag"}, new IEmbeddedWidgetClientFactory() {
   @Override
   public IEmbeddedWidgetClient createWidgetClient(String s, Map<String, String> map, IEmbeddedWidget iEmbeddedWidget) {
      Log.d(TAG, "init: createWidgetClient s"+s);
      Log.d(TAG, "init: createWidgetClient map"+map.toString());
      return new VideoEmbeddedWidgetClient(BrowserActivity.this);
   }
});

其中createWidgetClient方法参数意义

  • s 标签名,大写
  • map 该标签的属性,在html中指定的
  • iEmbeddedWidget 提供的原生的该标签的代理接口

下面是map打印的内容

init: createWidgetClient map{src=https://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4, style=position:absolute;width:350px; height:500px;, id=mytag}

处理原生占位控件的实现IEmbeddedWidgetClient

原生如何接管替换占位标签,主要的实现类是IEmbeddedWidgetClient

public interface IEmbeddedWidgetClient {
    
    void onSurfaceCreated(Surface var1);

    void onSurfaceDestroyed(Surface var1);

    boolean onTouchEvent(MotionEvent var1);

    void onRectChanged(Rect var1);

    void onVisibilityChanged(boolean var1);

    void onDestroy();

    void onActive();

    void onDeactive();

    void onRequestRedraw();
}

首先,IEmbeddedWidgetClient并不是一个原生的View,而是给原生提供的该标签区域的绘制的入口,从onSurfaceCreatedonSurfaceDestroyed可以理解;

既然不是原生的View绘制,X5还是提供了类比View的属性,从API命名可以简单看出来其作用;

  • onSurfaceCreated 该标签可以绘制,请求原生API处理
  • onSurfaceDestroyed 该标签视图销毁,请求原生销毁
  • onTouchEvent 触摸事件分发
  • onRectChanged 该标签在WebView中坐标变化回调(例如滚动,改变宽高)
  • onVisibilityChanged 该标签显示隐藏
  • onDestroy 该标签被移除,或者display = none

演示个Demo

熟悉了X5的API,写个简单的Demo玩一下

Demo的设计思路是在一个垂直滚动的网页中,嵌入一个原生的相机,原生的相机要正常的采集显示,且前端代码可以控制相机标签的显示隐藏,同步滚动等基本操作;

网页

<!DOCTYPE html>
<html>
<head>
    <title>测试网页</title>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no"
          name="viewport">
    <script type="text/javascript">
            window.onload = touchtouch;
            function touchtouch(){
                var camera = document.getElementById('camera');
                console.log("camera:"+camera)
                camera.addEventListener("touchStart",handlerTouch,false);
                camera.addEventListener("touchend",handlerTouch,false);
                camera.addEventListener("touchcancel",handlerTouch,false);
                camera.addEventListener("touchleave",handlerTouch,false);
                camera.addEventListener("touchmove",handlerTouch,false);
            }
            function handlerTouch(evt){
                log(evt);
            }
        </script>
</head>

<body>
<p>测试网页哈哈哈</p>
<div>
    <div style="background-color: aqua; height: 100px;text-align: center;">1</div>
    <div style="background-color: gray; height: 100px;text-align: center;">2</div>
    <div style="background-color: rgb(80, 69, 69);height:500px;">
        <camera cameraId=0
               style="position:absolute;width:350px; height:500px;">
            相机占位标签
        </camera>
        <a href="https://www.baidu.com"
           style="position:absolute;color:rgba(187, 255, 0, 0.482)"> 点击跳转哈哈哈</a>
    </div>
    <div style="background-color: gold; height: 100px;text-align: center;">4</div>
    <div style="background-color: red; height: 100px;text-align: center;">5</div>
    <div style="background-color: green; height: 100px;text-align: center;">6</div>
</div>

</body>

</html>

Java实现

public class CameraEmbeddedWidgetClient implements IEmbeddedWidgetClient {

    private String TAG = "VideoEmbeddedWidgetClient";

    private Rect rect;

    private CameraHelper cameraHelper;

    public CameraEmbeddedWidgetClient(Context c) {
        cameraHelper = new CameraHelper(c);
    }

    @Override
    public void onSurfaceCreated(Surface surface) {
        Log.d(TAG, "onSurfaceCreated: ");
//        Canvas canvas = surface.lockCanvas(rect);
//        canvas.drawColor(Color.parseColor("#7f000000"));
//        surface.unlockCanvasAndPost(canvas);
        cameraHelper.preview(surface);
    }

    @Override
    public void onSurfaceDestroyed(Surface surface) {
        Log.d(TAG, "onSurfaceDestroyed: ");
        cameraHelper.release();
    }
}

snap.jpeg

前端样式改变和Native事件触发

  • 指定样式display:none 原生回调onVisibilityChanged(false)onDestroy
  • 指定样式display:block 重新创建新的Client
  • 指定样式visibility:visible 原生回调onVisibilityChanged(true)
  • 指定样式visibility:hidden 原生回调onVisibilityChanged(false)
  • 移除当前dom 等效于display:none

触摸事件的验证

必须得在js中设置改标签的事件监听

camera.addEventListener("touchStart",handlerTouch,false);
camera.addEventListener("touchend",handlerTouch,false);
camera.addEventListener("touchcancel",handlerTouch,false);
camera.addEventListener("touchleave",handlerTouch,false);
camera.addEventListener("touchmove",handlerTouch,false);

原生接受事件处理

IEmbeddedWidgetClient实现类

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
    Log.d(TAG, "onTouchEvent: "+motionEvent.toString());
    float x = motionEvent.getX();
    float y = motionEvent.getY();
    int action = motionEvent.getAction();
    switch (action){
        case MotionEvent.ACTION_DOWN:
            initX = x;
            initY = y;
            intercepted = false;
            return false;
        case MotionEvent.ACTION_MOVE:
            float dx = x - initX;
            float dy = y - initY;
            if (!intercepted && Math.abs(dy)>Math.abs(dx) && Math.abs(dy)>16){
                intercepted = true;
            }
            break;

        case MotionEvent.ACTION_UP:

            break;
    }
    return intercepted;
}

总结

本文主要分享X5内核隐藏的同层渲染能力,用于学习交流,但是官方没有明确的文档说明,所以引入项目请谨慎;如果您对此该话题兴趣,不妨亲手折腾一下。