Wolvic 中 UI 组件绘制解析

525 阅读8分钟

前言

Wolvic 的首页中有许多的组件,比如标题栏,托盘,系统设置界面等等,而这些如果用纯 opengl 去绘制会非常的繁琐,所以在 wolvic 中使用了原生安卓的UI框架去设计界面,在绘制时将内容绘制到自己给到的 canvas 上面,传递给 opengl 进行绘制上屏,通过这种方式,实现了用原生安卓的开发过程去创建 VR 环境的平面组件。

原理分析

绘制分析

Wolvic 中的UI组件是用 Widget 这个概念去承载,所有组件都会实现 Widget 这个接口,并基本都会继承自 UIWidget。首先对 Widget 有个概念,一个 Widget 是一个组件,它可以由一个或多个 view 组成。比如 TrayWidget 就是一个托盘组件,具体样式如图: image

相信大多数人对于如何绘制这样一个托盘是比较熟悉的,简单来说可以用一个 xml 描述文件,依次按照自己熟悉的布局绘制出来。那么他是怎么绘制到 VR 空间的呢?先来看看 Widget 的结构关系。 UML 图 (1).jpg

上图简略描述了 UIWidget 类的基本结构,省略了监听,逻辑处理的方法和成员,仅保留了和绘制,事件相关的处理代码。可以看到,首页展现出来的每一个界面,基本都是由 UIWidget 组成的,浏览器界面是由 WidowsWidget 绘制,托盘是由 TrayWidget 绘制,键盘是由 KeyboardWidget 绘制等等。由于 UIWidget 继承了安卓的 FrameLayout, 所以他本身就是一个 View,可以当作孩子被插入到其他 View 当中,也可以接收其他 View 插入其中。

  • UIWidget 组合了一系列的类,是整个组件的核心。其子类需要实现 initializeWidgetPlacement 方法来设置自己的大小位置信息等等。

  • WidgetPlacement 表示一个 UIWidget 的描述信息,包含宽高,偏移位置,分辨率,名称,是否可见等等信息。

  • UISurfaceTextureRenderer 用来准备绘制的画布,他包装了从 native 传递过来的 SurfaceTexture ,创建了 Surface,并在绘制的时候由此创建出一个 Canvas 用于 UIWidget 的绘制。

  • WidgetManagerDelegate 负责组件的创建,更新,添加,删除等,并拥有控制是否可见,监听各种状态的任务,在 wolvic 中实际是 VRBrowserActivity。

在 wolvic 中,UIWidget 相关的类,大多数都会有一个 native 的类进行对应,比如 UIWidget.java 对应 Widget.cpp, WidgetPlacement.java 对应 WidgetPlacement.cpp 等等诸如此类,在创建 java 类的同时,会调用一个 createNative 的方法同时也创建对应的 C++ 类,C++ 类更多的是一个 java 类的映射,保存了 java 类对应的基本信息,供native 侧进行调用。其中 Widget.cpp 有一个重要的功能,就是创建 TextureSurface 或者 Surface ,具体什么时候创建 TextureSurface,什么时候创建 Surface,此处先不表,后文揭晓。

为了理解的更加具体,我们就挑 TrayWidget 和 WindowWidget 来看看具体的实现。为什么挑这两个,因为其比较有代表性,TrayWidget 代表了大多数的组件形态,而 WindowWidget 表示的是整个 GeckoView(等同于安卓的Webview) 界面的绘制,比较重要。

View 树挂载

在讲具体实现之前,先不着急,先来看看 wolvic 当中是如何挂载 view 的,毕竟 wolvic 当中的 activity 的界面不是由 contentView 渲染出来的,所以根本不会挂载 contentView。这部分逻辑主要都在 VrBrowserActivity 中。

@Override
protected void onCreate(Bundle savedInstanceState) {
  ……
  mWidgetContainer = new FrameLayout(this);
  ……
  queueRunnable(() -> {
    createOffscreenDisplay();
    ……
  });
  ……
}

其中 mWidgetContainer 是整个 View 树的根节点,所有的 UIWidget 都会通过 addView 的方式挂载到这个 View 节点上。queueRunnable 将在 native 运行的线程执行任务,也就是创建 OffscreenDisplay.

void createOffscreenDisplay() {
    final SurfaceTexture texture = createSurfaceTexture();
    runOnUiThread(() -> {
        mOffscreenDisplay = new OffscreenDisplay(VRBrowserActivity.this, texture, 16, 16);
        mOffscreenDisplay.setContentView(mWidgetContainer);
    });
}
private SurfaceTexture createSurfaceTexture() {
    int[] ids = new int[1];
    GLES20.glGenTextures(1, ids, 0);
    ……
    return new SurfaceTexture(ids[0]);
}

在 createOffscreenDisplay 方法中,可以看到先创建了一个 SurfaceTexture,这就是为什么必须要 queueRunnable,因为需要在 native 运行的线程生成 SurfaceTexture.之后再抛到主线程创建 OffscreenDisplay。再来看一下 OffscreenDisplay 类是怎么实现的。 UML 图 (2).jpg

可以看出这个类实际上是对 VirtualDisplay 和 Presenation 的一个封装,这两个类的具体用法可以参考安卓官方文档。所以上述的调用会执行以下两部分逻辑:

public void onResume() {
    if (mVirtualDisplay == null) {
        ……
        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
        ……
        mVirtualDisplay = manager.createVirtualDisplay("OffscreenViews Overlay", mWidth, mHeight,
                mDefaultMetrics.densityDpi, mSurface, flags);
    }
    if (mPresentation == null) {
        mPresentation = new OffscreenPresentation(mContext, mVirtualDisplay.getDisplay());
        mPresentation.show();
        if (mContentView != null) {
            mPresentation.setContentView(mContentView);
        }
    }
}
public void setContentView(View aView) {
    mContentView = aView;
    if (mPresentation != null) {
        mPresentation.setContentView(aView);
    }
}

首先创建了 mVirtualDisplay 和 mPresentation,并将 mWidgetContainer 设置为了 mPresentation 的 contentView。通过此种方式,就相当于在虚拟屏上挂载了一颗 view 树,整个渲染流程都是和正常的 activity 上面挂载一棵树是一样的,只不过不在当前屏幕当中。

TrayWidget 实现

Wolvic 中使用了 Databinding,如果不熟悉,可以先查看相关文档。首先在构造时进行了 View 的初始化:

public void updateUI() {
    removeAllViews();
    LayoutInflater inflater = LayoutInflater.from(getContext());
    // Inflate this data binding layout
    mBinding = DataBindingUtil.inflate(inflater, R.layout.tray, this, true);
    ……
}

TrayWidget 的视图是由 R.layout.tray 描述。通过 DataBinding 的方式 attach 到了当前 View 当中。在 TrayWidget 中没有重写 draw 相关的方法,所以没有绘制的特殊逻辑,所以看其父类 UIWidget 是如何执行绘制即可。 流程图 (7).jpg 上述就是 UIWidget 的 draw 方法的执行过程。可以看到view的绘制被拦截到了 renderer 生成的 canvas 上面。如果 widget 设置了 proxifyLayer,则会再绘制一次到 proxyRenderer 的 canvas 上面。通过这种方式,当前的 Widget 就已经被绘制到了 renderer 里面的 surface 上面了。proxifyLayer 目前看起来是用于做动画防止闪烁的,无动画的 UIWidget 暂时用不到。

WindowWidget 实现

WindowWidget 代表了一个 GeckoView 绘制界面。 他重写了 UIWidget 的 draw 方法,没有使用父类的 draw 的方式,而是走了原生的 draw 方式:

@Override
public void draw(Canvas aCanvas) {
    if (mView != null) {
        super.draw(aCanvas);
    }
}

mView 是 GeckoView 上面的覆盖层,比如弹窗,比如设置界面等等。正常情况下为空,则 draw 的时候,什么都不执行。那么他是怎么绘制 GeckoView 的网页界面呢?大致流程如下图: 流程图 (8).jpg

在代码中查找会发现,在 setSurface 和 setSurfaceTexture 的时候会调用一个 callSurfaceChanged 方法,把 surface 对象传递到了 mSession 当中。mSession 中又会把这个 surface 传递给 GeckoSession 中,交给 GeckoView 内核进行使用,具体的绘制都在 GeckoView 内核中实现,此处不再继续深究。

Texture(Surface) 生成过程

上面分析了 UI 的绘制过程,这一节说一下画布是怎么生成的。上文提到过 UIWidget 的两个方法, setSurface 和 setSurfaceTexture,这两个方法就是画布生成的关键。先来看看 UIWidget 创建过程中,native的执行逻辑:

流程图 (9).jpg

从上图可以看出,在主线程创建 Widget 的时候,native 也需要创建对应的 native 对象,并且会区分是否是 layer,目前只有 oculcus 打开了 layer 的设置选项,其他设备统统都没有走 layer 类型。这个layer 类型是 OpenXR 的概念,相关链接见这里。XrCompositionLayerQuad 层对于渲染到虚拟世界中的用户界面元素或 2D 内容很有用。该层的 XrSwapchainSubImage::swapchain 图像应用于虚拟世界空间中的四边形。

在 Widget的 UpdateSurface 方法中,也判断了是否是 layer,是的话等待 surfaceChanged 事件再执行下一步,否则就会创建一个 TextureSurface 类型,注意此时仅仅是创建了一个壳 TextureSurface ,其中的 opengl 的 texture 还未真正创建,此类会在 SurfaceTextureFactory 中记录自己需要一个 texture,等到 SurfaceTextureFactory 根据记录创建好了 texture 之后,会回设给 TextureSurface。和 layer 类型等待 surfaceChanged 事件一致,等到合适的时机才会去创建真正的 texture 或者 surface。后面就是给 quad 绑定上面创建的 textureSurface,更新给 opengl 的 program,因为可能会有一些关键字不一样,需要去动态修改 program。

native 的 Widget 创建完成了,再来看看当不是 layer 类型的时候具体是什么时候生成 texture 或者 surface:

流程图 (10).jpg

已经讲过 StartFrame 方法了,此处着重看 RenderContext::Update 方法,此方法中会去遍历前文 TextureSurface::Create 留下的需要创建 texture 集合,逐个进行 glGenTexture 创建真正的 texture.并且会根据这个 texture 去创建 java 层的 SurfaceTexture 对象,并把它分发给 java 层的 UIWidget 对象。到这里,就看到了绘制分析中提到的 surface 和 surfaceTexture 是从何而来的了。最后再通过 updateTexImage 将当前的图像提交到纹理中,就实现了当前 Widget 的绘制。

再来看一下如果是 Layer 类型,是什么过程: 流程图 (11).jpg

在 layer 打开时,从上图可以看到,每一个平面的 UIWidget 都会去创建一个对应的 OpenXRLayerQuad 对象,他会去初始化 surface,他是通过 OpenXR 的 xrCreateSwapchainAndroidSurfaceKHR** **方法去创建的。再通过 DispatchCreateWidgetLayer 方法传递到 java 层,并设置给 UIWidget。*和非 layer 类型的区别就在于一个是通过 jni 的方式手动去创建的 surface, 一个是通过 OpenXR 的方式去创建的 surface。*有了这个 surface,上文的渲染就水到渠成了。

结束

结合 wolvic 浏览器openxr 支持现状 可以大致了解 wolvic 中是如何将安卓的组件渲染到 3D 空间了。