1# 同层渲染 2 3在系统中,应用可以使用Web组件加载Web网页。在非原生框架的UI组件功能或性能不如原生组件时,可使用同层渲染,使用ArkUI组件渲染这些组件(简称为同层组件)。 4 5## 使用场景 6### Web网页 7小程序的地图组件,可以使用ArkUI的XComponent组件渲染来提升性能。小程序的输入框组件,可以使用ArkUI的TextInput组件渲染,达到与原生应用一致的输入体验。 8- 在网页侧,应用开发者可将<embed>、<object>的网页UI组件(简称为同层标签),按一定规则进行同层渲染,详细规格见同层渲染规格小节。 9 10- 在应用侧,应用开发者可以通过Web组件的同层渲染事件上报接口,感知到H5同层标签的生命周期以及输入事件,进行同层渲染组件的相应业务逻辑处理。 11 12- 在应用侧,应用开发者可以使用ArkUI的NodeContainer等接口,构建H5同层标签对应的同层渲染组件。可支持同层渲染的ArkUI常用组件包括:[TextInput](../reference/apis-arkui/arkui-ts/ts-basic-components-textinput.md), [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md), [Canvas](../reference/apis-arkui/arkui-ts/ts-components-canvas-canvas.md), [Video](../reference/apis-arkui/arkui-ts/ts-media-components-video.md), [Web](../reference/apis-arkweb/ts-basic-components-web.md)。具体规格可参见[同层渲染规格小节](#规格约束)。 13 14### 三方UI框架 15Flutter提供了PlatformView与Texture抽象组件,这些组件可使用原生组件渲染,用来支持Flutter组件功能不足的部分。Weex2.0框架的Camera、Video、Canvas组件。 16 17- 在三方框架页面侧,由于Flutter、Weex等三方框架不在操作系统范围,本文不列举可被同层渲染的三方框架UI组件的范围与使用方式。 18 19- 在应用侧,应用开发者可以使用ArkUI的NodeContainer等接口,构建三方框架同层标签对应的同层渲染组件。可支持同层渲染的ArkUI常用组件包括:[TextInput](../reference/apis-arkui/arkui-ts/ts-basic-components-textinput.md), [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md), [Canvas](../reference/apis-arkui/arkui-ts/ts-components-canvas-canvas.md), [Video](../reference/apis-arkui/arkui-ts/ts-media-components-video.md), [Web](../reference/apis-arkweb/ts-basic-components-web.md)。具体规格可参见[同层渲染规格](#规格约束)。 20 21## 整体架构 22ArkWeb同层渲染特性主要提供两种能力:同层标签生命周期和事件命中转发处理。 23 24同层标签生命周期主要关联前端标签(<embed>/<object>),同时命中到同层标签的事件会被上报到开发者侧,由开发者分发到对应组件树。整体框架如下图所示: 25 26**图1** 同层渲染整体架构 27 28 29 30## 规格约束 31### 可被同层渲染的ArkUI组件 32 33以下规格对Web网页和三方框架场景均生效。 34 35**支持的组件范围:** 36 37- 基础组件:[AlphabetIndexer](../reference/apis-arkui/arkui-ts/ts-container-alphabet-indexer.md), [Blank](../reference/apis-arkui/arkui-ts/ts-basic-components-blank.md), [Button](../reference/apis-arkui/arkui-ts/ts-basic-components-button.md), [CalendarPicker](../reference/apis-arkui/arkui-ts/ts-basic-components-calendarpicker.md), [Checkbox](../reference/apis-arkui/arkui-ts/ts-basic-components-checkbox.md), [CheckboxGroup](../reference/apis-arkui/arkui-ts/ts-basic-components-checkboxgroup.md), [ContainerSpan](../reference/apis-arkui/arkui-ts/ts-basic-components-containerspan.md), [DataPanel](../reference/apis-arkui/arkui-ts/ts-basic-components-datapanel.md), [DatePicker](../reference/apis-arkui/arkui-ts/ts-basic-components-datepicker.md), [Divider](../reference/apis-arkui/arkui-ts/ts-basic-components-divider.md), [Gauge](../reference/apis-arkui/arkui-ts/ts-basic-components-gauge.md), [Hyperlink](../reference/apis-arkui/arkui-ts/ts-container-hyperlink.md), [Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md), [ImageAnimator](../reference/apis-arkui/arkui-ts/ts-basic-components-imageanimator.md), [ImageSpan](../reference/apis-arkui/arkui-ts/ts-basic-components-imagespan.md), [LoadingProgress](../reference/apis-arkui/arkui-ts/ts-basic-components-loadingprogress.md), [Marquee](../reference/apis-arkui/arkui-ts/ts-basic-components-marquee.md), [PatternLock](../reference/apis-arkui/arkui-ts/ts-basic-components-patternlock.md), [Progress](../reference/apis-arkui/arkui-ts/ts-basic-components-progress.md), [QRCode](../reference/apis-arkui/arkui-ts/ts-basic-components-qrcode.md), [Radio](../reference/apis-arkui/arkui-ts/ts-basic-components-radio.md), [Rating](../reference/apis-arkui/arkui-ts/ts-basic-components-rating.md), [Refresh](../reference/apis-arkui/arkui-ts/ts-container-refresh.md), [ScrollBar](../reference/apis-arkui/arkui-ts/ts-container-scroll.md), [Search](../reference/apis-arkui/arkui-ts/ts-basic-components-search.md), [Span](../reference/apis-arkui/arkui-ts/ts-basic-components-span.md), [Select](../reference/apis-arkui/arkui-ts/ts-basic-components-select.md), [Slider](../reference/apis-arkui/arkui-ts/ts-basic-components-slider.md), [Text](../reference/apis-arkui/arkui-ts/ts-basic-components-text.md), [TextArea](../reference/apis-arkui/arkui-ts/ts-basic-components-textarea.md), [TextClock](../reference/apis-arkui/arkui-ts/ts-basic-components-textclock.md), [TextInput](../reference/apis-arkui/arkui-ts/ts-basic-components-textinput.md), [TextPicker](../reference/apis-arkui/arkui-ts/ts-basic-components-textpicker.md), [TextTimer](../reference/apis-arkui/arkui-ts/ts-basic-components-texttimer.md), [TimePicker](../reference/apis-arkui/arkui-ts/ts-basic-components-timepicker.md), [Toggle](../reference/apis-arkui/arkui-ts/ts-basic-components-toggle.md) 38 39- 容器类组件:[Badge](../reference/apis-arkui/arkui-ts/ts-container-badge.md), [Column](../reference/apis-arkui/arkui-ts/ts-container-column.md), [ColumnSplit](../reference/apis-arkui/arkui-ts/ts-container-columnsplit.md), [Counter](../reference/apis-arkui/arkui-ts/ts-container-counter.md), [Flex](../reference/apis-arkui/arkui-ts/ts-container-flex.md), [GridCol](../reference/apis-arkui/arkui-ts/ts-container-gridcol.md), [GridRow](../reference/apis-arkui/arkui-ts/ts-container-gridrow.md), [Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md), [GridItem](../reference/apis-arkui/arkui-ts/ts-container-griditem.md),[List](../reference/apis-arkui/arkui-ts/ts-container-list.md), [ListItem](../reference/apis-arkui/arkui-ts/ts-container-listitem.md), [ListItemGroup](../reference/apis-arkui/arkui-ts/ts-container-listitemgroup.md), [RelativeContainer](../reference/apis-arkui/arkui-ts/ts-container-relativecontainer.md), [Row](../reference/apis-arkui/arkui-ts/ts-container-row.md), [RowSplit](../reference/apis-arkui/arkui-ts/ts-container-rowsplit.md), [Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md), [Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md), [Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md), [Tabs](../reference/apis-arkui/arkui-ts/ts-container-tabs.md), [TabContent](../reference/apis-arkui/arkui-ts/ts-container-tabcontent.md), [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md), [SideBarContainer](../reference/apis-arkui/arkui-ts/ts-container-sidebarcontainer.md), [Stepper](../reference/apis-arkui/arkui-ts/ts-basic-components-stepper.md), [StepperItem](../reference/apis-arkui/arkui-ts/ts-basic-components-stepperitem.md), [WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md), [FlowItem](../reference/apis-arkui/arkui-ts/ts-container-flowitem.md) 40 41- 自绘制类组件:[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md), [Canvas](../reference/apis-arkui/arkui-ts/ts-components-canvas-canvas.md), [Video](../reference/apis-arkui/arkui-ts/ts-media-components-video.md), [Web](../reference/apis-arkweb/ts-basic-components-web.md) 42 43- 命令式自定义绘制节点:[BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md), [ComponentContent](../reference/apis-arkui/js-apis-arkui-ComponentContent.md), [ContentSlot](../reference/apis-arkui/arkui-ts/ts-components-contentSlot.md), [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md), [Graphics](../reference/apis-arkui/js-apis-arkui-graphics.md), [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md), [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md), [XComponentNode](../reference/apis-arkui/js-apis-arkui-xcomponentNode.md), [AttributeUpdater](../reference/apis-arkui/js-apis-arkui-AttributeUpdater.md),[CAPI](../reference/apis-arkui/_ark_u_i___native_module.md)(支持同层渲染的组件范围同ArkTS) 44 45**支持的组件通用属性与事件:** 46 47- 不支持的通用属性:[分布式迁移标识](../reference/apis-arkui/arkui-ts/ts-universal-attributes-restoreId.md),[特效绘制合并](../reference/apis-arkui/arkui-ts/ts-universal-attributes-use-effect.md)。 48 49- 其他未明确标注不支持的属性与事件及组件能力,均默认支持。 50 51### Web网页的同层渲染标签 52此规格仅针对Web网页,不适用于三方框架场景。 53 54如果应用需要在Web组件加载的网页中使用同层渲染,需要按照以下规格将网页中的<embed>、<object>标签指定为同层渲染组件。 55 56**支持的产品形态:** 57当前仅支持移动设备和平板形态。 58 59**支持的H5标签:** 60- 支持<embed>标签:在开启同层渲染后,仅支持type类型为native前缀的标签识别为同层组件,不支持自定义属性。 61 62- 支持<object>标签:在开启同层渲染后,支持将非标准MIME type的object标签识别为同层组件,支持通过param/value的自定义属性解析。 63 64- 不支持W3C规范标准标签(如<input>、<video>)定义为同层标签。 65 66- 不支持同时配置<object>标签和<embed>标签作为同层标签。 67 68- 标签类型只支持英文字符,不区分大小写。 69 70**同层标签的属性支持范围:** 71支持满足W3C标准的CSS样式属性。 72 73**同层标签的生命周期管理:** 74当Embed标签生命周期变化时触发[onNativeEmbedLifecycleChange()](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedlifecyclechange11)回调。 75 76- 支持创建、销毁、位置宽高变化、不支持可见状态变化。 77 78- 支持同层组件所在Web页面进入前进后退缓存。 79 80**同层标签的输入事件分发处理:** 81- 支持触摸事件TouchEvent的DOWN/UP/MOVE/CANCEL。支持[配置触摸事件消费结果](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedgestureevent11),默认为应用侧消费。 82 83- 不支持同层标签所在的应用页面缩放和[initialScale](../reference/apis-arkweb/ts-basic-components-web.md#initialscale)、[zoom](../reference/apis-arkweb/js-apis-webview.md#zoom)、[zoomIn](../reference/apis-arkweb/js-apis-webview.md#zoomin)、[zoomOut](../reference/apis-arkweb/js-apis-webview.md#zoomout)等缩放接口。 84 85- 暂不支持鼠标、键盘、触摸板事件。 86 87**约束限制:** 88 89- Web页面内不建议超过5个同层标签。超过5个后,渲染性能将会下降。 90 91- 受GPU限制,同层标签最大高度不超过8000px,最大纹理大小为8000px。 92 93- 开启同层渲染后,Web组件打开的所有Web页面将不支持同步渲染模式[RenderMode](../reference/apis-arkweb/ts-basic-components-web.md#rendermode12枚举说明)。 94 95- Video组件:在非全屏Video变为全屏时,Video组件变为非纹理导出模式,视频播放状态保持延续;恢复为非全屏时,变为纹理导出模式,视频播放状态保持延续。 96 97- Web组件:仅支持一层同层渲染嵌套,不支持多层同层渲染嵌套。输入事件只支持滑动、点击、缩放、长按 ,不支持拖拽、旋转。 98 99- 涉及界面交互的ArkUI组件(如TextInput等):建议在页面布局中使用Stack包裹同层组件容器与BuilderNode,并使两者位置一致,NodeContainer要与<embed>/<object>标签对齐,以保障组件正常交互。如两者位置不一致,可能出现的问题有:TextInput/TextArea等附属的文本选择框位置错位(如下图)、LoadingProgress/Marquee等组件的动画启停与组件可见状态不匹配。 100 101 **图2** 未使用Stack包裹,TextInput的位置错位 102 103  104 105 **图3** 使用Stack包裹,TextInput的位置正常 106 107  108 109## Web页面中同层渲染输入框 110在Web页面中,可以使用ArkUI原生的TextInput组件进行同层渲染。此处利用同层渲染展示三个输入框,渲染效果图如下: 111 112**图4** 同层渲染输入框 113 114  115 1161. 在Web页面中标记需要同层渲染的HTML标签。 117 118 同层渲染支持<embed>/<object>两种标签。type类型可任意指定,两个字符串参数均不区分大小写,ArkWeb内核将会统一转换为小写。其中,tag字符串使用全字符串匹配,type使用字符串前缀匹配。 119 120 若开发者不使用该接口或该接口接收的为非法字符串(空字符串)时,ArkWeb内核将使用默认设置,即"embed" + "native/"前缀模式。若指定类型与w3c定义的object或embed标准类型重合,如registerNativeEmbedRule("object", "application/pdf"),ArkWeb将遵循w3c标准行为,不会将其识别为同层标签。 121 122 - 采用<embed>标签。 123 124 ```html 125 <!--HAP's src/main/resources/rawfile/text.html--> 126 <!DOCTYPE html> 127 <html> 128 <head> 129 <title>同层渲染测试html</title> 130 <meta name="viewport"> 131 </head> 132 133 <body style="background:white"> 134 135 <embed id = "input1" type="native/view" style="width: 100%; height: 100px; margin: 30px; margin-top: 600px"/> 136 137 <embed id = "input2" type="native/view2" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"/> 138 139 <embed id = "input3" type="native/view3" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"/> 140 141 </body> 142 </html> 143 ``` 144 145 - 采用<object>标签。 146 147 需要使用registerNativeEmbedRule注册object标签。 148 ```ts 149 // ... 150 Web({src: $rawfile("text.html"), controller: this.browserTabController}) 151 // 注册同层标签为"object",类型为"test"前缀 152 .registerNativeEmbedRule("object", "test") 153 // ... 154 ``` 155 156 与registerNativeEmbedRule相对应的前端页面代码,类型可使用"test"及以"test"为前缀的字串。 157 158 ```html 159 <!--HAP's src/main/resources/rawfile/text.html--> 160 <!DOCTYPE html> 161 <html> 162 <head> 163 <title>同层渲染测试html</title> 164 <meta name="viewport"> 165 </head> 166 167 <body style="background:white"> 168 169 <object id = "input1" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 600px"></object> 170 171 <object id = "input2" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"></object> 172 173 <object id = "input3" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"></object> 174 175 </body> 176 </html> 177 ``` 178 1792. 在应用侧开启同层渲染功能。 180 181 同层渲染功能默认不开启,如果要使用同层渲染的功能,可通过enableNativeEmbedMode来开启。 182 183 ```ts 184 // xxx.ets 185 import { webview } from '@kit.ArkWeb'; 186 @Entry 187 @Component 188 struct WebComponent { 189 controller: webview.WebviewController = new webview.WebviewController(); 190 191 build() { 192 Column() { 193 Web({ src: 'www.example.com', controller: this.controller }) 194 // 配置同层渲染开关开启。 195 .enableNativeEmbedMode(true) 196 } 197 } 198 } 199 ``` 200 2013. 创建自定义组件。 202 203 同层渲染功能开启后,展示在对应区域的原生组件。 204 205 ```ts 206 @Component 207 struct TextInputComponent { 208 @Prop params: Params 209 @State bkColor: Color = Color.White 210 211 build() { 212 Column() { 213 TextInput({text: '', placeholder: 'please input your word...'}) 214 .placeholderColor(Color.Gray) 215 .id(this.params?.elementId) 216 .placeholderFont({size: 13, weight: 400}) 217 .caretColor(Color.Gray) 218 .width(this.params?.width) 219 .height(this.params?.height) 220 .fontSize(14) 221 .fontColor(Color.Black) 222 } 223 //自定义组件中的最外层容器组件宽高应该为同层标签的宽高 224 .width(this.params.width) 225 .height(this.params.height) 226 } 227 } 228 229 @Builder 230 function TextInputBuilder(params:Params) { 231 TextInputComponent({params: params}) 232 .width(params.width) 233 .height(params.height) 234 .backgroundColor(Color.White) 235 } 236 ``` 237 2384. 创建节点控制器。 239 240 用于控制和反馈对应NodeContainer上的节点行为。 241 242 ```ts 243 class MyNodeController extends NodeController { 244 private rootNode: BuilderNode<[Params]> | undefined | null; 245 private embedId_: string = ""; 246 private surfaceId_: string = ""; 247 private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; 248 private width_: number = 0; 249 private height_: number = 0; 250 private type_: string = ""; 251 private isDestroy_: boolean = false; 252 253 setRenderOption(params: NodeControllerParams) { 254 this.surfaceId_ = params.surfaceId; 255 this.renderType_ = params.renderType; 256 this.embedId_ = params.embedId; 257 this.width_ = params.width; 258 this.height_ = params.height; 259 this.type_ = params.type; 260 } 261 262 // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。 263 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。 264 makeNode(uiContext: UIContext): FrameNode | null { 265 if (this.isDestroy_) { // rootNode为null 266 return null; 267 } 268 if (!this.rootNode) {// rootNode 为undefined时 269 this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ }); 270 if(this.rootNode) { 271 this.rootNode.build(wrapBuilder(TextInputBuilder), { textOne: "myTextInput", width: this.width_, height: this.height_ }) 272 return this.rootNode.getFrameNode(); 273 }else{ 274 return null; 275 } 276 } 277 // 返回FrameNode节点。 278 return this.rootNode.getFrameNode(); 279 } 280 281 setBuilderNode(rootNode: BuilderNode<Params[]> | null): void { 282 this.rootNode = rootNode; 283 } 284 285 getBuilderNode(): BuilderNode<[Params]> | undefined | null { 286 return this.rootNode; 287 } 288 289 updateNode(arg: Object): void { 290 this.rootNode?.update(arg); 291 } 292 293 getEmbedId(): string { 294 return this.embedId_; 295 } 296 297 setDestroy(isDestroy: boolean): void { 298 this.isDestroy_ = isDestroy; 299 if (this.isDestroy_) { 300 this.rootNode = null; 301 } 302 } 303 304 postEvent(event: TouchEvent | undefined): boolean { 305 return this.rootNode?.postTouchEvent(event) as boolean 306 } 307 } 308 ``` 309 3105. 监听同层渲染的生命周期变化。 311 312 开启该功能后,每当网页中存在同层渲染支持的标签时,ArkWeb内核会触发由[onNativeEmbedLifecycleChange](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedlifecyclechange11)注册的回调函数。 313 314 开发者则需要调用[onNativeEmbedLifecycleChange](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedlifecyclechange11)来监听同层渲染标签的生命周期变化。 315 316 ```ts 317 build() { 318 Row() { 319 Column() { 320 Stack() { 321 ForEach(this.componentIdArr, (componentId: string) => { 322 NodeContainer(this.nodeControllerMap.get(componentId)) 323 .position(this.positionMap.get(componentId)) 324 .width(this.widthMap.get(componentId)) 325 .height(this.heightMap.get(componentId)) 326 }, (embedId: string) => embedId) 327 // Web组件加载本地text.html页面 328 Web({src: $rawfile("text.html"), controller: this.browserTabController}) 329 // 配置同层渲染开关开启 330 .enableNativeEmbedMode(true) 331 // 注册同层标签为"object",类型为"test"前缀 332 .registerNativeEmbedRule("object", "test") 333 // 获取embed标签的生命周期变化数据 334 .onNativeEmbedLifecycleChange((embed) => { 335 console.log("NativeEmbed surfaceId" + embed.surfaceId); 336 // 如果使用embed.info.id作为映射nodeController的key,请在h5页面显式指定id 337 const componentId = embed.info?.id?.toString() as string 338 if (embed.status == NativeEmbedStatus.CREATE) { 339 console.log("NativeEmbed create" + JSON.stringify(embed.info)); 340 // 创建节点控制器、设置参数并rebuild 341 let nodeController = new MyNodeController() 342 // embed.info.width和embed.info.height单位是px格式,需要转换成ets侧的默认单位vp 343 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, 344 type : embed.info?.type as string, 345 renderType : NodeRenderType.RENDER_TYPE_TEXTURE, 346 embedId : embed.embedId as string, 347 width : px2vp(embed.info?.width), 348 height : px2vp(embed.info?.height)}) 349 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 350 nodeController.setDestroy(false); 351 //根据web传入的embed的id属性作为key,将nodeController存入Map 352 this.nodeControllerMap.set(componentId, nodeController); 353 this.widthMap.set(componentId, px2vp(embed.info?.width)); 354 this.heightMap.set(componentId, px2vp(embed.info?.height)); 355 this.positionMap.set(componentId, this.edges); 356 // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后 357 this.componentIdArr.push(componentId) 358 } else if (embed.status == NativeEmbedStatus.UPDATE) { 359 let nodeController = this.nodeControllerMap.get(componentId); 360 console.log("NativeEmbed update" + JSON.stringify(embed)); 361 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 362 this.positionMap.set(componentId, this.edges); 363 this.widthMap.set(componentId, px2vp(embed.info?.width)); 364 this.heightMap.set(componentId, px2vp(embed.info?.height)); 365 nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject) 366 } else if (embed.status == NativeEmbedStatus.DESTROY) { 367 console.log("NativeEmbed destroy" + JSON.stringify(embed)); 368 let nodeController = this.nodeControllerMap.get(componentId); 369 nodeController?.setDestroy(true) 370 this.nodeControllerMap.clear(); 371 this.positionMap.delete(componentId); 372 this.widthMap.delete(componentId); 373 this.heightMap.delete(componentId); 374 this.componentIdArr.filter((value: string) => value != componentId) 375 } else { 376 console.log("NativeEmbed status" + embed.status); 377 } 378 }) 379 }.height("80%") 380 } 381 } 382 } 383 ``` 384 3856. 同层渲染手势事件。 386 387 开启该功能后,每当在同层渲染的区域进行触摸操作时,ArkWeb内核会触发[onNativeEmbedGestureEvent](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedgestureevent11)注册的回调函数。 388 389 开发者则需要调用[onNativeEmbedGestureEvent](../reference/apis-arkweb/ts-basic-components-web.md#onnativeembedgestureevent11)来监听同层渲染同层渲染区域的手势事件。 390 391 ```ts 392 build() { 393 Row() { 394 Column() { 395 Stack() { 396 ForEach(this.componentIdArr, (componentId: string) => { 397 NodeContainer(this.nodeControllerMap.get(componentId)) 398 .position(this.positionMap.get(componentId)) 399 .width(this.widthMap.get(componentId)) 400 .height(this.heightMap.get(componentId)) 401 }, (embedId: string) => embedId) 402 // Web组件加载本地text.html页面。 403 Web({src: $rawfile("text.html"), controller: this.browserTabController}) 404 // 配置同层渲染开关开启。 405 .enableNativeEmbedMode(true) 406 // 获取embed标签的生命周期变化数据。 407 .onNativeEmbedLifecycleChange((embed) => { 408 // 生命周期变化实现 409 }) 410 .onNativeEmbedGestureEvent((touch) => { 411 console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent)); 412 this.componentIdArr.forEach((componentId: string) => { 413 let nodeController = this.nodeControllerMap.get(componentId); 414 // 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上 415 if(nodeController?.getEmbedId() == touch.embedId) { 416 let ret = nodeController?.postEvent(touch.touchEvent) 417 if(ret) { 418 console.log("onNativeEmbedGestureEvent success " + componentId); 419 } else { 420 console.log("onNativeEmbedGestureEvent fail " + componentId); 421 } 422 if(touch.result) { 423 // 通知Web组件手势事件消费结果 424 touch.result.setGestureEventResult(ret); 425 } 426 } 427 }) 428 }) 429 } 430 } 431 } 432 } 433 ``` 434 435**完整示例:** 436 437使用前请在module.json5中添加网络权限,添加方法请参考[在配置文件中声明权限](../security/AccessToken/declare-permissions.md)。 438 439 ``` 440 "requestPermissions":[ 441 { 442 "name" : "ohos.permission.INTERNET" 443 } 444 ] 445 ``` 446 447应用侧代码。 448 449 ```ts 450 // 创建NodeController 451 import webview from '@ohos.web.webview'; 452 import { UIContext } from '@ohos.arkui.UIContext'; 453 import { NodeController, BuilderNode, NodeRenderType, FrameNode } from "@ohos.arkui.node"; 454 455 @Observed 456 declare class Params{ 457 elementId: string 458 textOne: string 459 textTwo: string 460 width: number 461 height: number 462 } 463 464 declare class NodeControllerParams { 465 surfaceId: string 466 type: string 467 renderType: NodeRenderType 468 embedId: string 469 width: number 470 height: number 471 } 472 473 // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。 474 class MyNodeController extends NodeController { 475 private rootNode: BuilderNode<[Params]> | undefined | null; 476 private embedId_: string = ""; 477 private surfaceId_: string = ""; 478 private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; 479 private width_: number = 0; 480 private height_: number = 0; 481 private type_: string = ""; 482 private isDestroy_: boolean = false; 483 484 setRenderOption(params: NodeControllerParams) { 485 this.surfaceId_ = params.surfaceId; 486 this.renderType_ = params.renderType; 487 this.embedId_ = params.embedId; 488 this.width_ = params.width; 489 this.height_ = params.height; 490 this.type_ = params.type; 491 } 492 493 // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。 494 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。 495 makeNode(uiContext: UIContext): FrameNode | null { 496 if (this.isDestroy_) { // rootNode为null 497 return null; 498 } 499 if (!this.rootNode) {// rootNode 为undefined时 500 this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ }); 501 if(this.rootNode) { 502 this.rootNode.build(wrapBuilder(TextInputBuilder), { textOne: "myTextInput", width: this.width_, height: this.height_ }) 503 return this.rootNode.getFrameNode(); 504 }else{ 505 return null; 506 } 507 } 508 // 返回FrameNode节点。 509 return this.rootNode.getFrameNode(); 510 } 511 512 setBuilderNode(rootNode: BuilderNode<Params[]> | null): void { 513 this.rootNode = rootNode; 514 } 515 516 getBuilderNode(): BuilderNode<[Params]> | undefined | null { 517 return this.rootNode; 518 } 519 520 updateNode(arg: Object): void { 521 this.rootNode?.update(arg); 522 } 523 524 getEmbedId(): string { 525 return this.embedId_; 526 } 527 528 setDestroy(isDestroy: boolean): void { 529 this.isDestroy_ = isDestroy; 530 if (this.isDestroy_) { 531 this.rootNode = null; 532 } 533 } 534 535 postEvent(event: TouchEvent | undefined): boolean { 536 return this.rootNode?.postTouchEvent(event) as boolean 537 } 538 } 539 540 @Component 541 struct TextInputComponent { 542 @Prop params: Params 543 @State bkColor: Color = Color.White 544 545 build() { 546 Column() { 547 TextInput({text: '', placeholder: 'please input your word...'}) 548 .placeholderColor(Color.Gray) 549 .id(this.params?.elementId) 550 .placeholderFont({size: 13, weight: 400}) 551 .caretColor(Color.Gray) 552 .fontSize(14) 553 .fontColor(Color.Black) 554 } 555 //自定义组件中的最外层容器组件宽高应该为同层标签的宽高 556 .width(this.params.width) 557 .height(this.params.height) 558 } 559 } 560 561 // @Builder中为动态组件的具体组件内容。 562 @Builder 563 function TextInputBuilder(params:Params) { 564 TextInputComponent({params: params}) 565 .width(params.width) 566 .height(params.height) 567 .backgroundColor(Color.White) 568 } 569 570 @Entry 571 @Component 572 struct Page{ 573 browserTabController: WebviewController = new webview.WebviewController() 574 private nodeControllerMap: Map<string, MyNodeController> = new Map(); 575 @State componentIdArr: Array<string> = []; 576 @State posMap: Map<string, Position | undefined> = new Map(); 577 @State widthMap: Map<string, number> = new Map(); 578 @State heightMap: Map<string, number> = new Map(); 579 @State positionMap: Map<string, Edges> = new Map(); 580 @State edges: Edges = {}; 581 582 build() { 583 Row() { 584 Column() { 585 Stack() { 586 ForEach(this.componentIdArr, (componentId: string) => { 587 NodeContainer(this.nodeControllerMap.get(componentId)) 588 .position(this.positionMap.get(componentId)) 589 .width(this.widthMap.get(componentId)) 590 .height(this.heightMap.get(componentId)) 591 }, (embedId: string) => embedId) 592 // Web组件加载本地text.html页面。 593 Web({src: $rawfile("text.html"), controller: this.browserTabController}) 594 // 配置同层渲染开关开启。 595 .enableNativeEmbedMode(true) 596 // 获取embed标签的生命周期变化数据。 597 .onNativeEmbedLifecycleChange((embed) => { 598 console.log("NativeEmbed surfaceId" + embed.surfaceId); 599 // 如果使用embed.info.id作为映射nodeController的key,请在h5页面显式指定id 600 const componentId = embed.info?.id?.toString() as string 601 if (embed.status == NativeEmbedStatus.CREATE) { 602 console.log("NativeEmbed create" + JSON.stringify(embed.info)); 603 // 创建节点控制器、设置参数并rebuild 604 let nodeController = new MyNodeController() 605 // embed.info.width和embed.info.height单位是px格式,需要转换成ets侧的默认单位vp 606 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, 607 type : embed.info?.type as string, 608 renderType : NodeRenderType.RENDER_TYPE_TEXTURE, 609 embedId : embed.embedId as string, 610 width : px2vp(embed.info?.width), 611 height : px2vp(embed.info?.height)}) 612 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 613 nodeController.setDestroy(false); 614 //根据web传入的embed的id属性作为key,将nodeController存入Map 615 this.nodeControllerMap.set(componentId, nodeController); 616 this.widthMap.set(componentId, px2vp(embed.info?.width)); 617 this.heightMap.set(componentId, px2vp(embed.info?.height)); 618 this.positionMap.set(componentId, this.edges); 619 // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后 620 this.componentIdArr.push(componentId) 621 } else if (embed.status == NativeEmbedStatus.UPDATE) { 622 let nodeController = this.nodeControllerMap.get(componentId); 623 console.log("NativeEmbed update" + JSON.stringify(embed)); 624 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 625 this.positionMap.set(componentId, this.edges); 626 this.widthMap.set(componentId, px2vp(embed.info?.width)); 627 this.heightMap.set(componentId, px2vp(embed.info?.height)); 628 nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject) 629 } else if (embed.status == NativeEmbedStatus.DESTROY) { 630 console.log("NativeEmbed destroy" + JSON.stringify(embed)); 631 let nodeController = this.nodeControllerMap.get(componentId); 632 nodeController?.setDestroy(true) 633 this.nodeControllerMap.clear(); 634 this.positionMap.delete(componentId); 635 this.widthMap.delete(componentId); 636 this.heightMap.delete(componentId); 637 this.componentIdArr.filter((value: string) => value != componentId) 638 } else { 639 console.log("NativeEmbed status" + embed.status); 640 } 641 })// 获取同层渲染组件触摸事件信息。 642 .onNativeEmbedGestureEvent((touch) => { 643 console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent)); 644 this.componentIdArr.forEach((componentId: string) => { 645 let nodeController = this.nodeControllerMap.get(componentId); 646 // 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上 647 if(nodeController?.getEmbedId() == touch.embedId) { 648 let ret = nodeController?.postEvent(touch.touchEvent) 649 if(ret) { 650 console.log("onNativeEmbedGestureEvent success " + componentId); 651 } else { 652 console.log("onNativeEmbedGestureEvent fail " + componentId); 653 } 654 if(touch.result) { 655 // 通知Web组件手势事件消费结果 656 touch.result.setGestureEventResult(ret); 657 } 658 } 659 }) 660 }) 661 } 662 } 663 } 664 } 665 } 666 ``` 667 668## 绘制XComponent+AVPlayer和Button组件 669 670开发者可通过[enableNativeEmbedMode()](../reference/apis-arkweb/ts-basic-components-web.md#enablenativeembedmode11)控制同层渲染开关。Html文件中需要显式使用embed标签,并且embed标签内type必须以“native/”开头。同层标签对应的元素区域的背景为透明。 671 672- 应用侧代码组件使用示例。 673 674 ```ts 675 // HAP's src/main/ets/pages/Index.ets 676 // 创建NodeController 677 import { webview } from '@kit.ArkWeb'; 678 import { UIContext, NodeController, BuilderNode, NodeRenderType, FrameNode } from "@kit.ArkUI"; 679 import { AVPlayerDemo } from './PlayerDemo'; 680 681 @Observed 682 declare class Params { 683 textOne : string 684 textTwo : string 685 width : number 686 height : number 687 } 688 689 declare class NodeControllerParams { 690 surfaceId : string 691 type : string 692 renderType : NodeRenderType 693 embedId : string 694 width : number 695 height : number 696 } 697 698 // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。 699 class MyNodeController extends NodeController { 700 private rootNode: BuilderNode<[Params]> | undefined | null; 701 private embedId_ : string = ""; 702 private surfaceId_ : string = ""; 703 private renderType_ :NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; 704 private width_ : number = 0; 705 private height_ : number = 0; 706 private type_ : string = ""; 707 private isDestroy_ : boolean = false; 708 709 setRenderOption(params : NodeControllerParams) { 710 this.surfaceId_ = params.surfaceId; 711 this.renderType_ = params.renderType; 712 this.embedId_ = params.embedId; 713 this.width_ = params.width; 714 this.height_ = params.height; 715 this.type_ = params.type; 716 } 717 // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。 718 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。 719 makeNode(uiContext: UIContext): FrameNode | null{ 720 if (this.isDestroy_) { // rootNode为null 721 return null; 722 } 723 if (!this.rootNode) { // rootNode 为undefined时 724 this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_}); 725 if (this.type_ === 'native/video') { 726 this.rootNode.build(wrapBuilder(VideoBuilder), {textOne: "myButton", width : this.width_, height : this.height_}); 727 } else { 728 // other 729 } 730 } 731 // 返回FrameNode节点。 732 return this.rootNode.getFrameNode(); 733 } 734 735 setBuilderNode(rootNode: BuilderNode<Params[]> | null): void{ 736 this.rootNode = rootNode; 737 } 738 739 getBuilderNode(): BuilderNode<[Params]> | undefined | null{ 740 return this.rootNode; 741 } 742 743 updateNode(arg: Object): void { 744 this.rootNode?.update(arg); 745 } 746 getEmbedId() : string { 747 return this.embedId_; 748 } 749 750 setDestroy(isDestroy : boolean) : void { 751 this.isDestroy_ = isDestroy; 752 if (this.isDestroy_) { 753 this.rootNode = null; 754 } 755 } 756 757 postEvent(event: TouchEvent | undefined) : boolean { 758 return this.rootNode?.postTouchEvent(event) as boolean 759 } 760 } 761 762 @Component 763 struct VideoComponent { 764 @ObjectLink params: Params 765 @State bkColor: Color = Color.Red 766 mXComponentController: XComponentController = new XComponentController(); 767 @State player_changed: boolean = false; 768 player?: AVPlayerDemo; 769 770 build() { 771 Column() { 772 Button(this.params.textOne) 773 774 XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController}) 775 .border({width: 1, color: Color.Red}) 776 .onLoad(() => { 777 this.player = new AVPlayerDemo(); 778 this.player.setSurfaceID(this.mXComponentController.getXComponentSurfaceId()); 779 this.player_changed = !this.player_changed; 780 this.player.avPlayerLiveDemo() 781 }) 782 .width(300) 783 .height(200) 784 } 785 //自定义组件中的最外层容器组件宽高应该为同层标签的宽高 786 .width(this.params.width) 787 .height(this.params.height) 788 } 789 } 790 // @Builder中为动态组件的具体组件内容。 791 @Builder 792 function VideoBuilder(params: Params) { 793 VideoComponent({ params: params }) 794 .backgroundColor(Color.Gray) 795 } 796 797 @Entry 798 @Component 799 struct WebIndex { 800 browserTabController: WebviewController = new webview.WebviewController() 801 private nodeControllerMap: Map<string, MyNodeController> = new Map(); 802 @State componentIdArr: Array<string> = []; 803 804 aboutToAppear() { 805 // 配置web开启调试模式。 806 webview.WebviewController.setWebDebuggingAccess(true); 807 } 808 809 build(){ 810 Row() { 811 Column() { 812 Stack() { 813 ForEach(this.componentIdArr, (componentId: string) => { 814 NodeContainer(this.nodeControllerMap.get(componentId)) 815 }, (embedId: string) => embedId) 816 // Web组件加载本地test.html页面。 817 Web({ src: $rawfile("test.html"), controller: this.browserTabController }) 818 // 配置同层渲染开关开启。 819 .enableNativeEmbedMode(true) 820 // 获取embed标签的生命周期变化数据。 821 .onNativeEmbedLifecycleChange((embed) => { 822 console.log("NativeEmbed surfaceId" + embed.surfaceId); 823 // 1. 如果使用embed.info.id作为映射nodeController的key,请在h5页面显式指定id 824 const componentId = embed.info?.id?.toString() as string 825 if (embed.status == NativeEmbedStatus.CREATE) { 826 console.log("NativeEmbed create" + JSON.stringify(embed.info)) 827 // 创建节点控制器,设置参数并rebuild。 828 let nodeController = new MyNodeController() 829 // 1. embed.info.width和embed.info.height单位是px格式,需要转换成ets侧的默认单位vp 830 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, type : embed.info?.type as string, 831 renderType : NodeRenderType.RENDER_TYPE_TEXTURE, embedId : embed.embedId as string, 832 width : px2vp(embed.info?.width), height : px2vp(embed.info?.height)}) 833 nodeController.setDestroy(false); 834 // 根据web传入的embed的id属性作为key,将nodeController存入map。 835 this.nodeControllerMap.set(componentId, nodeController) 836 // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后。 837 this.componentIdArr.push(componentId) 838 } else if (embed.status == NativeEmbedStatus.UPDATE) { 839 let nodeController = this.nodeControllerMap.get(componentId) 840 nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject) 841 } else { 842 let nodeController = this.nodeControllerMap.get(componentId); 843 nodeController?.setDestroy(true) 844 this.nodeControllerMap.clear(); 845 this.componentIdArr.length = 0; 846 } 847 })// 获取同层渲染组件触摸事件信息。 848 .onNativeEmbedGestureEvent((touch) => { 849 console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent)); 850 this.componentIdArr.forEach((componentId: string) => { 851 let nodeController = this.nodeControllerMap.get(componentId) 852 // 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上 853 if (nodeController?.getEmbedId() === touch.embedId) { 854 let ret = nodeController?.postEvent(touch.touchEvent) 855 if (ret) { 856 console.log("onNativeEmbedGestureEvent success " + componentId) 857 } else { 858 console.log("onNativeEmbedGestureEvent fail " + componentId) 859 } 860 if (touch.result) { 861 // 通知Web组件手势事件消费结果 862 touch.result.setGestureEventResult(ret); 863 } 864 } 865 }) 866 }) 867 } 868 } 869 } 870 } 871 } 872 ``` 873 874- 应用侧代码,视频播放示例,使用时需替换正确的视频链接地址。 875 876 ```ts 877 // HAP's src/main/ets/pages/PlayerDemo.ets 878 import { media } from '@kit.MediaKit'; 879 import { BusinessError } from '@ohos.base'; 880 881 export class AVPlayerDemo { 882 private count: number = 0; 883 private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法。 884 private isSeek: boolean = true; // 用于区分模式是否支持seek操作。 885 886 setSurfaceID(surface_id: string){ 887 console.log('setSurfaceID : ' + surface_id); 888 this.surfaceID = surface_id; 889 } 890 // 注册avplayer回调函数。 891 setAVPlayerCallback(avPlayer: media.AVPlayer) { 892 // seek操作结果回调函数。 893 avPlayer.on('seekDone', (seekDoneTime: number) => { 894 console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); 895 }) 896 // error回调监听函数,当avplayer在操作过程中出现错误时,调用reset接口触发重置流程。 897 avPlayer.on('error', (err: BusinessError) => { 898 console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); 899 avPlayer.reset(); 900 }) 901 // 状态机变化回调函数。 902 avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => { 903 switch (state) { 904 case 'idle': // 成功调用reset接口后触发该状态机上报。 905 console.info('AVPlayer state idle called.'); 906 avPlayer.release(); // 调用release接口销毁实例对象。 907 break; 908 case 'initialized': // avplayer 设置播放源后触发该状态上报。 909 console.info('AVPlayer state initialized called.'); 910 avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置。 911 avPlayer.prepare(); 912 break; 913 case 'prepared': // prepared调用成功后上报该状态机。 914 console.info('AVPlayer state prepared called.'); 915 avPlayer.play(); // 调用播放接口开始播放。 916 break; 917 case 'playing': // play成功调用后触发该状态机上报。 918 console.info('AVPlayer state prepared called.'); 919 if(this.count !== 0) { 920 if (this.isSeek) { 921 console.info('AVPlayer start to seek.'); 922 avPlayer.seek(avPlayer.duration); // seek到视频末尾。 923 } else { 924 // 当播放模式不支持seek操作时继续播放到结尾。 925 console.info('AVPlayer wait to play end.'); 926 } 927 } else { 928 avPlayer.pause(); // 调用暂停接口暂停播放。 929 } 930 this.count++; 931 break; 932 case 'paused': // pause成功调用后触发该状态机上报。 933 console.info('AVPlayer state paused called.'); 934 avPlayer.play(); // 再次播放接口开始播放。 935 break; 936 case 'completed': //播放接口后触发该状态机上报。 937 console.info('AVPlayer state paused called.'); 938 avPlayer.stop(); // 调用播放接口接口。 939 break; 940 case 'stopped': // stop接口后触发该状态机上报。 941 console.info('AVPlayer state stopped called.'); 942 avPlayer.reset(); // 调用reset接口初始化avplayer状态。 943 break; 944 case 'released': //播放接口后触发该状态机上报。 945 console.info('AVPlayer state released called.'); 946 break; 947 default: 948 break; 949 } 950 }) 951 } 952 953 // 通过url设置网络地址来实现播放直播码流。 954 async avPlayerLiveDemo(){ 955 // 创建avPlayer实例对象 956 let avPlayer: media.AVPlayer = await media.createAVPlayer(); 957 // 创建状态机变化回调函数。 958 this.setAVPlayerCallback(avPlayer); 959 this.isSeek = false; // 不支持seek操作。 960 // 使用时需要自行替换视频链接 961 avPlayer.url = 'https://xxx.xxx/demo.mp4'; 962 } 963 } 964 ``` 965 966- 前端页面示例。 967 968 ```html 969 <!--HAP's src/main/resources/rawfile/test.html--> 970 <!DOCTYPE html> 971 <html> 972 <head> 973 <title>同层渲染测试html</title> 974 <meta name="viewport"> 975 </head> 976 <body> 977 <div> 978 <div id="bodyId"> 979 <embed id="nativeVideo" type = "native/video" width="1000" height="1500" src="test" style = "background-color:red"/> 980 </div> 981 </div> 982 </body> 983 </html> 984 ``` 985 986 