关于 Flutter 的 PlatformView 混合开发,我们在过去已经讨论过多次,尤其是在 Android 平台,它已经拥有了 VD、HC、TLHC、HCPP 等多种兼容实现。而本文将深入探讨 Flutter 在鸿蒙平台的 PlatformView 实现,核心在于解析其如何实现“同层渲染”。
同层渲染
Flutter 是一个自渲染的跨平台框架,在鸿蒙平台,它通过 XComponent 支持 skia 和 Impeller 渲染,XComponent 提供了一个用于渲染的 Surface(NativeWindow)。在鸿蒙中,XComponent 可以直接获取到底层的 OHNativeWindow 实例,然后通过扩展 VK_OHOS_surface 将窗口转为 Vulkan 中的 VKSurface,进而通过 VKSwapchain 实现窗口绘制。
接下来重点讨论鸿蒙平台 Flutter 的 PlatformView 如何实现“同层渲染”,简单来说,就是将 ArkUI 渲染到 Flutter 内部,类似于 ArkUI 中将控件渲染到 Web 组件的机制。
事实上,在鸿蒙官方 ArkUI 的 Web 组件中,可以通过开启 enableNativeEmbedMode 来启用 WebView 的“同层渲染”。其基本原理是:底层使用空白的 H5 页面,用 Embed 标签占位,ArkTS 使用 NodeContainer 占位,最后将 Web 侧的 surfaceId 与原生组件绑定,使原生组件渲染到 Web 中。
具体步骤包括:
- 用
Stack组件层叠NodeContainer和 Web 组件,并开启enableNativeEmbedMode模式。 - 封装一个继承
NodeController的控制器(如SearchNodeController)。 - Web 组件加载
nativeembed_view.html文件,解析到 Embed 标签后,通过onNativeEmbedLifecycleChange接口上报创建消息。 - 在回调中根据
embed.status,将配置传入控制器后执行 rebuild 方法,重新触发makeNode。 makeNode触发后,NodeContainer获取BuilderNode对象,BuilderNode承载了原生控件的纹理,从而使页面显示原生组件。
核心在于通过 NodeContainer 占位,并实现其对应 NodeController 的 makeNode 方法,将原生控件绘制到 BuilderNode,而 BuilderNode 通过 surfaceId 关联到一个可绘制区域。代码示例如下:
makeNode(uiContext: UIContext): FrameNode | null {
this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId, type: this.renderType });
if (this.componentType === 'native/component') {
this.rootNode.build(wrapBuilder(searchBuilder), { width: this.componentWidth, height: this.componentHeight });
}
return this.rootNode.getFrameNode();
}
@Builder
function searchBuilder(params: Params) {
SearchComponent({ params: params })
.backgroundColor($r('app.color.ohos_id_color_sub_background'))
}
因此,关键组件是 NodeContainer 和 BuilderNode,它们是 ArkUI 上混合开发的基础。Flutter 在鸿蒙的 PlatformView 实现也类似:通过 BuilderNode 导出 ArkUI 控件的纹理,导出的纹理在 XComponent 中实现“同层渲染”,这与 Flutter 在 Android 上的 VD 实现较为接近。
各种 Node
首先需要了解 BuilderNode 是什么。BuilderNode 在 ArkUI 中是一个自定义声明式节点,支持采用无状态的 UI 方式,可以通过全局自定义构建函数 @Builder 定制组件树。定制得到的 FrameNode 节点,可以直接由 NodeController 返回并挂载于 NodeContainer 节点下。
BuilderNode 还提供了组件预创建的能力,例如可以结合它提前对 ArkWeb 组件进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过 NodeController 动态挂载与显示。
通过 BuilderNode,我们也能理解 NodeContainer 的作用:用于挂载自定义节点(如 FrameNode 或 BuilderNode),并通过 NodeController 动态控制节点的上树和下树。组件接受一个 NodeController 的实例接口,因此 NodeContainer 需要和 NodeController 组合使用。
严格来说,NodeContainer 仅支持挂载自定义节点 FrameNode,对于 BuilderNode,实际上是获取它的根节点 FrameNode。
ArkUI 和 Flutter 类似,也有三棵树:Component Tree、Element Tree 和 RenderNode Tree,它们的作用与 Flutter 三棵树基本一致,其中 RenderNode Tree 是存在于 C++ 后端引擎中的最终渲染结构。
FrameNode 可以视为三棵树中 Component Tree 的特殊实体节点,与自定义占位容器组件 NodeContainer 配合,可以在占位容器内构建一棵自定义的节点树。
FrameNode 作为特殊 Component 节点,提供了节点创建和删除的能力,即在 ArkUI 这种声明式开发场景中,提供了命令式操作的支持。此外,FrameNode 还提供了 getRenderNode 接口,用于获取 FrameNode 中的 RenderNode,从而直接提供绘制的渲染节点。
简单总结:
FrameNode+NodeContainer提供自定义节点支持,通过FrameNode提供RenderNode。BuilderNode提供构建支持和纹理导出,并通过getFrameNode获取对应的FrameNode对象。
因此可以分类:
BuilderNode是一个自定义的声明式节点。FrameNode是一个自定义组件节点。RenderNode是一个自渲染节点。
Flutter
Flutter 鸿蒙在鸿蒙平台的实现方式接近于 Android 平台的 VD,即通过 NodeContainer 挂载节点,并实现事件传递,最终将提取的纹理合并到 Flutter 内进行渲染。
对应到 Flutter 鸿蒙实现中,是 EmbeddingNodeController 的实现。它通过继承 NodeContainer 并实现 makeNode 来创建和管理 ArkUI 的 BuilderNode,而 BuilderNode 中的 wrappedBuilder 则来自封装好的 PlatformView 里的 ArkUI 的 @Builder 实现。
另一方面,EmbeddingNodeController 作为 NodeController 的具体实现,用于管理 NodeContainer。在 Flutter 鸿蒙中,创建和加载 NodeContainer 的对象是 DynamicView,它是一个基于 DVModel 数据驱动的对象,可以在 FlutterPage 的默认实现中看到其身影。
简单来说,鸿蒙 Flutter 的页面默认在一个 Stack 下:
XComponent提供 surface 绘制,是 Flutter 的渲染画板。- 基于
this.rootDvModel列表的DynamicView,主要提供 PlatformView 所需的NodeContainer。
这里 DynamicView 基于 DVModel 实体作为驱动,而 DVModel 的存在,是为了用鸿蒙 ArkUI 的声明式范式来管理和渲染由 Flutter 发出的 PlatformView 命令式 UI 操作。因为 ArkUI 是纯粹的声明式 UI 框架,不能像传统 Android 命令式编程那样直接调用 parent.addView(child),而是需要通过状态驱动,让框架根据新状态重新渲染 UI。
DVModel 就是这个“状态”,它是一个用 @Observed 装饰的可观察树状数据结构,用纯数据完整描述整个界面布局,包括哪个位置应该有一个 PlatformView、大小、参数等。
当 DVModel 作为状态发生变化时,相关的 DynamicView 也会发生变化,这是 Flutter 鸿蒙在 PlatformView 实现上的特殊之处。例如,在鸿蒙 Flutter 中运行 webview_flutter 后,在 ArkUI Inspector 中可以看到布局:通过 DynamicView 构建了一个 NodeContainer,而这里的 NodeContainer 通过 BuilderNode 承载了 Web 组件的纹理。
当存在两个 Web 组件时,控件树里就会有两个 DynamicView,这对应前面 FlutterPage 中基于 this.rootDvModel 列表的实现。如果堆叠两个 Web 并在其上添加一个 Flutter 红色控件,通过 ArkUI Inspector 可以看到对应 DynamicView 的 NodeContainer 存在的节点,但该节点的内容并没有渲染在原来的位置。
由于 Web 组件通过纹理方式渲染到 Flutter Engine 中,在事件触摸上,触摸事件需要从 Dart 层发出,经过中转,最后通过 EmbeddingNodeController 的 postEvent 转发到 BuilderNode,这点与 Flutter Android 的 VD 模式类似。当然,与 VD 不同的是,因为 Web 是真实存在的节点,键盘输入不会像 VD 那样有太多的连接问题,从这点看又类似 TLHC 实现。
接下来是 Dart 如何触发 PlatformView 构建的实现对象:PlatformViewsChannel 和 PlatformViewsController。
当用户在 Dart 层使用 OhosView 或 OhosViewSurface 创建鸿蒙 PlatformView 时,会触发 PlatformViewsChannel 的 create。虽然和 Android 一样存在两个入口,可以根据 Dart 层是否配置了 hybrid 来决定使用 createForPlatformViewLayer 还是 createForTextureLayer,但实际上目前只有 createForTextureLayer 一种可用。如果配置了 hybrid 模式,运行后会发现 view_embedder 为空。
createForPlatformViewLayer 在 Android 走的是 HC 的实现,它把自己作为一个合成边界,渲染路径分叉,把 Flutter 内容渲染到离屏缓冲区,然后通过 SurfaceFlinger 将这些缓冲区和独立渲染的原生视图组合。但在鸿蒙 Flutter 的实现中,关于 createForPlatformViewLayer 的 HC 并没有实现,因此实际上只有 createForTextureLayer 纹理合成这一种 PlatformView 场景。
对于 createForTextureLayer,流程会来到 PlatformViewsController 对象,核心流程包括:
- 创建一个
platformView,即调用 Plugin 中开发者PlatformViewFactory的实现。 - 通过 Engine 注册得到一个与 Flutter Engine 关联的 Surface id。
- 创建
EmbeddingNodeController,关联 Surface id 和关联platformView的 ArkUI 控件。 - 创建
DVModel,添加到队列驱动创建DynamicView。
获取 surface id 时,是在 Engine 底层利用系统 Graphic2D 的 NativeImage 能力,通过 OH_NativeImage_Create 创建一个 OH_NativeImage 实例。OH_NativeImage 支持将数据和 OpenGL 纹理对接,或由开发者自行获取 buffer 进行渲染处理。
注意 OH_NativeImage_SetOnFrameAvailableListener,会在 Frame 数据可用时触发 MarkTextureFrameAvailable,这类似于在 C++ 层面将纹理对象标记为“脏”或“过时”,类似 setState 的作用。此操作会触发两个 TaskRunner 的工作:
- 在 Raster 线程触发设置 texture 为“脏”。
- 在 UI 线程执行
ScheduleFrame(false),false 表示 Widget 完全相同,Engine 可以跳过整个构建/布局/绘制过程,只需获取最后生成的 layer tree 并在光栅线程上重新渲染。
回到主流程,在得到 DVModel 后,DynamicView 会构建出 NodeContainer,从而触发 EmbeddingNodeController 的 makeNode,进而 BuilderNode 构建并提取 PlatformView 里的 ArkUI 控件纹理,最终渲染出画面。
具体到 webview_flutter 中,WebViewPlatformView 继承了 PlatformView,并实现了 getView() 方法,返回 OhosWebView:
getView(): WrappedBuilder<[Params]> {
return new WrappedBuilder(WebBuilder);
}
@Builder
export function WebBuilder(params: Params) {
OhosWebView({
params: params,
webView: params.platformView as WebViewPlatformView,
controller: (params.platformView as WebViewPlatformView).getController()
})
}
这个 getView(): WrappedBuilder 会在 EmbeddingNodeController 中被获取,并在 makeNode 里由 BuilderNode 使用,从而实现最终的纹理提取和渲染。
最后,整个 PlatformView 的整体流程核心仍在 NodeContainer 和 BuildNode 的基础上展开,然后基于 DVModel 驱动 DynamicView 更新,进而 makeNode 构建出纹理,触发 Engine 更新 Texture 区域实现绘制。