• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 同层渲染绘制Video和Button组件
2
3Web组件支持同层渲染绘制Video和Button组件。
4
5开发者可通过[enableNativeEmbedMode()](../reference/apis-arkweb/ts-basic-components-web.md#enablenativeembedmode11)控制同层渲染开关。Html文件中需要显式使用embed标签,并且embed标签内type必须以“native/”开头。
6
7
8同层渲染的标签背景是白色的,只支持Web组件嵌套一层Web组件。
9
10
11- 使用前请在module.json5添加如下权限。
12
13  ```ts
14  "ohos.permission.INTERNET"
15  ```
16
17- 应用侧代码,同层渲染组件使用示例。
18
19  ```ts
20  // 创建NodeController
21  import webview from '@ohos.web.webview';
22  import {UIContext} from '@ohos.arkui.UIContext';
23  import {NodeController, BuilderNode, NodeRenderType, FrameNode} from "@ohos.arkui.node";
24  import {AVPlayerDemo} from './PlayerDemo';
25
26  declare class Params {
27    textOne : string
28    textTwo : string
29    width : number
30    height : number
31  }
32  declare class nodeControllerParams {
33    surfaceId : string
34    type : string
35    renderType : NodeRenderType
36    embedId : string
37    width : number
38    height : number
39  }
40  // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。
41  class MyNodeController extends NodeController {
42    private rootNode: BuilderNode<[Params]> | undefined | null;
43    private embedId_ : string = "";
44    private surfaceId_ : string = "";
45    private renderType_ :NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
46    private width_ : number = 0;
47    private height_ : number = 0;
48    private type_ : string = "";
49
50    setRenderOption(params : nodeControllerParams) {
51      this.surfaceId_ = params.surfaceId;
52      this.renderType_ = params.renderType;
53      this.embedId_ = params.embedId;
54      this.width_ = params.width;
55      this.height_ = params.height;
56      this.type_ = params.type;
57    }
58    // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。
59    // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。
60    makeNode(uiContext: UIContext): FrameNode | null{
61      this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_});
62      if (this.type_ === 'native/button') {
63        this.rootNode.build(wrapBuilder(ButtonBuilder), {textOne: "myButton1", textTwo : "myButton2", width : this.width_, height : this.height_});
64      } else if (this.type_ === 'native/video') {
65        this.rootNode.build(wrapBuilder(VideoBuilder), {textOne: "myButton", width : this.width_, height : this.height_});
66      } else {
67        // other
68      }
69      // 返回FrameNode节点。
70      return this.rootNode.getFrameNode();
71    }
72
73    setBuilderNode(rootNode: BuilderNode<Params[]> | null): void{
74      this.rootNode = rootNode;
75    }
76
77    getBuilderNode(): BuilderNode<[Params]> | undefined | null{
78      return this.rootNode;
79    }
80
81    updateNode(arg: Object): void {
82      this.rootNode?.update(arg);
83    }
84    getEmbedId() : string {
85      return this.embedId_;
86    }
87
88    postEvent(event: TouchEvent | undefined) : boolean {
89      return this.rootNode?.postTouchEvent(event) as boolean
90    }
91  }
92
93  @Component
94  struct ButtonComponent {
95    @Prop params: Params
96    @State bkColor: Color = Color.Red
97
98    build() {
99      Column() {
100        Button(this.params.textOne)
101          .height(50)
102          .width(200)
103          .border({ width: 2, color: Color.Red})
104          .backgroundColor(this.bkColor)
105
106        Button(this.params.textTwo)
107          .height(50)
108          .width(200)
109          .border({ width: 2, color: Color.Red})
110          .backgroundColor(this.bkColor)
111      }
112      .width(this.params.width)
113      .height(this.params.height)
114    }
115  }
116
117  @Component
118  struct VideoComponent {
119    @Prop params: Params
120    @State bkColor: Color = Color.Red
121    testController: WebviewController = new webview.WebviewController();
122    mXComponentController: XComponentController = new XComponentController();
123    @State player_changed: boolean = false;
124    player?: AVPlayerDemo;
125
126    build() {
127      Column() {
128        Button(this.params.textOne)
129          .height(50)
130          .width(100)
131          .border({ width: 2, color: Color.Red})
132          .backgroundColor(this.bkColor)
133
134        XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController})
135          .width(300)
136          .height(300)
137          .border({width: 1, color: Color.Red})
138          .onLoad(() => {
139            this.player = new AVPlayerDemo();
140            this.player.setSurfaceID(this.mXComponentController.getXComponentSurfaceId());
141            this.player_changed = !this.player_changed;
142            this.player.avPlayerLiveDemo()
143          })
144      }
145      .width(this.params.width)
146      .height(this.params.height)
147    }
148  }
149  // @Builder中为动态组件的具体组件内容。
150  @Builder
151  function ButtonBuilder(params: Params) {
152    ButtonComponent({ params: params })
153      .backgroundColor(Color.Green)
154  }
155
156  @Builder
157  function VideoBuilder(params: Params) {
158    VideoComponent({ params: params })
159      .backgroundColor(Color.Green)
160  }
161
162  @Entry
163  @Component
164  struct WebIndex {
165    browserTabController: WebviewController = new webview.WebviewController()
166    private nodeControllerMap: Map<string, MyNodeController> = new Map();
167    @State componentIdArr: Array<string> = [];
168
169    aboutToAppear() {
170      // 配置web开启调试模式。
171      webview.WebviewController.setWebDebuggingAccess(true);
172    }
173
174    build(){
175      Row() {
176        Column({ space: 5}) {
177          Stack() {
178            ForEach(this.componentIdArr, (componentId: string) => {
179              NodeContainer(this.nodeControllerMap.get(componentId))
180            }, (embedId: string) => embedId)
181            // web组件加载本地test.html页面。
182            Web({ src: $rawfile("test.html"), controller: this.browserTabController })
183                // 配置同层渲染开关开启。
184              .enableNativeEmbedMode(true)
185                // 获取embed标签的生命周期变化数据。
186              .onNativeEmbedLifecycleChange((embed) => {
187                console.log("NativeEmbed surfaceId" + embed.surfaceId);
188                // 获取web侧embed元素的id。
189                const componentId = embed.info?.id?.toString() as string
190                if (embed.status == NativeEmbedStatus.CREATE) {
191                  console.log("NativeEmbed create" + JSON.stringify(embed.info))
192                  // 创建节点控制器,设置参数并rebuild。
193                  let nodeController = new MyNodeController()
194                  nodeController.setRenderOption({surfaceId : embed.surfaceId as string, type : embed.info?.type as string, renderType : NodeRenderType.RENDER_TYPE_TEXTURE, embedId : embed.embedId as string, width : px2vp(embed.info?.width), height : px2vp(embed.info?.height)})
195                  nodeController.rebuild()
196                  // 根据web传入的embed的id属性作为key,将nodeController存入map。
197                  this.nodeControllerMap.set(componentId, nodeController)
198                  // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后。
199                  this.componentIdArr.push(componentId)
200                } else if (embed.status == NativeEmbedStatus.UPDATE) {
201                  let nodeController = this.nodeControllerMap.get(componentId)
202                  nodeController?.updateNode({text: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject)
203                  nodeController?.rebuild()
204                } else {
205                  let nodeController = this.nodeControllerMap.get(componentId)
206                  nodeController?.setBuilderNode(null)
207                  nodeController?.rebuild()
208                }
209              })// 获取同层渲染组件触摸事件信息。
210              .onNativeEmbedGestureEvent((touch) => {
211                console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent));
212                this.componentIdArr.forEach((componentId: string) => {
213                  let nodeController = this.nodeControllerMap.get(componentId)
214                  if (nodeController?.getEmbedId() === touch.embedId) {
215                    let ret = nodeController?.postEvent(touch.touchEvent)
216                    if (ret) {
217                      console.log("onNativeEmbedGestureEvent success " + componentId)
218                    } else {
219                      console.log("onNativeEmbedGestureEvent fail " + componentId)
220                    }
221                  }
222                })
223              })
224          }
225        }
226      }
227    }
228  }
229  ```
230- 应用侧代码,视频播放示例。
231
232  ```ts
233  import media from '@ohos.multimedia.media';
234  import {BusinessError} from '@ohos.base';
235
236  export class AVPlayerDemo {
237    private count: number = 0;
238    private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法。
239    private isSeek: boolean = true; // 用于区分模式是否支持seek操作。
240
241    setSurfaceID(surface_id: string){
242      console.log('setSurfaceID : ' + surface_id);
243      this.surfaceID = surface_id;
244    }
245    // 注册avplayer回调函数。
246    setAVPlayerCallback(avPlayer: media.AVPlayer) {
247      // seek操作结果回调函数。
248      avPlayer.on('seekDone', (seekDoneTime: number) => {
249        console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
250      })
251      // error回调监听函数,当avplayer在操作过程中出现错误时,调用reset接口触发重置流程。
252      avPlayer.on('error', (err: BusinessError) => {
253        console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
254        avPlayer.reset();
255      })
256      // 状态机变化回调函数。
257      avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
258        switch (state) {
259          case 'idle': // 成功调用reset接口后触发该状态机上报。
260            console.info('AVPlayer state idle called.');
261            avPlayer.release(); // 调用release接口销毁实例对象。
262            break;
263          case 'initialized': // avplayer 设置播放源后触发该状态上报。
264            console.info('AVPlayer state initialized called.');
265            avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置。
266            avPlayer.prepare();
267            break;
268          case 'prepared': // prepared调用成功后上报该状态机。
269            console.info('AVPlayer state prepared called.');
270            avPlayer.play(); // 调用播放接口开始播放。
271            break;
272          case 'playing': // play成功调用后触发该状态机上报。
273            console.info('AVPlayer state prepared called.');
274            if(this.count !== 0) {
275              if (this.isSeek) {
276                console.info('AVPlayer start to seek.');
277                avPlayer.seek(avPlayer.duration); // seek到视频末尾。
278              } else {
279                // 当播放模式不支持seek操作时继续播放到结尾。
280                console.info('AVPlayer wait to play end.');
281              }
282            } else {
283              avPlayer.pause(); // 调用暂停接口暂停播放。
284            }
285            this.count++;
286            break;
287          case 'paused': // pause成功调用后触发该状态机上报。
288            console.info('AVPlayer state paused called.');
289            avPlayer.play(); // 再次播放接口开始播放。
290            break;
291          case 'completed': //播放接口后触发该状态机上报。
292            console.info('AVPlayer state paused called.');
293            avPlayer.stop(); // 调用播放接口接口。
294            break;
295          case 'stopped': // stop接口后触发该状态机上报。
296            console.info('AVPlayer state stopped called.');
297            avPlayer.reset(); // 调用reset接口初始化avplayer状态。
298            break;
299          case 'released': //播放接口后触发该状态机上报。
300            console.info('AVPlayer state released called.');
301            break;
302          default:
303            break;
304        }
305      })
306    }
307
308    // 通过url设置网络地址来实现播放直播码流。
309    async avPlayerLiveDemo(){
310      // 创建avPlayer实例对象
311      let avPlayer: media.AVPlayer = await media.createAVPlayer();
312      // 创建状态机变化回调函数。
313      this.setAVPlayerCallback(avPlayer);
314      this.isSeek = false; // 不支持seek操作。
315      avPlayer.url = 'https://xxx.xxx/demo.mp4';
316    }
317  }
318  ```
319
320- 前端页面示例。
321
322  ```html
323  <!Document>
324  <html>
325  <head>
326      <title>同层渲染测试html</title>
327      <meta name="viewport">
328  </head>
329  <body>
330  <div>
331      <div id="bodyId">
332          <embed id="nativeButton" type = "native/button" width="800" height="800" src="test?params1=xxx?" style = "background-color:red"/>
333      </div>
334      <div id="bodyId1">
335          <embed id="nativeVideo" type = "native/video" width="500" height="500" src="test" style = "background-color:red"/>
336      </div>
337  </div>
338  <div id="button" width="500" height="200">
339      <p>bottom</p>
340  </div>
341  <script>
342  let nativeEmbed = {
343      // 判断设备是否支持touch事件。
344      nativeButton : document.getElementById('nativeButton'),
345      nativeVideo : document.getElementById('nativeVideo'),
346
347      // 事件。
348      events:{},
349      // 初始化。
350      init:function(){
351          let self = this;
352          self.nativeButton.addEventListener('touchstart', self.events, false);
353          self.nativeVideo.addEventListener('touchstart', self.events, false);
354      }
355  };
356  nativeEmbed.init();
357  </script>
358
359  </body>
360  </html>
361  ```
362
363  ![web-same-layer](figures/web-same-layer.png)