目录

Using Same-Layer Rendering

In the system, applications can use the Web component to load web pages. If the capability or performance of non-system UI components is inferior to that of system components, you can use the ArkUI component to render these components (same-layer components).

When to Use

[h2]On the Web Page

To improve the performance of an applet, you can use the ArkUI XComponent component to render the map component, and use the ArkUI TextInput component to render the input box component.

[h2]On the Third-Party UI Framework

Flutter provides the PlatformView and Texture abstract components that can be rendered using system components, which complete the functionalities of the Flutter components. In the Weex 2.0 framework, the Camera, Video, and Canvas components can be rendered using system components to enhance functionality and performance.

Overall Architecture

The ArkWeb same-layer rendering feature supports same-layer tag lifecycle and event hit forwarding.

The lifecycle of same-layer tags is associated with front-end tags (<embed>/<object>). Events that hit the same-layer tags are reported to you, and you should distribute them to the corresponding component tree. The following figure shows the overall framework.

Figure 1 Overall architecture of same-layer rendering

Specifications and Constraints

[h2]ArkUI Components That Can Be Rendered at the Same Layer

The following specifications take effect in both web pages and third-party frameworks.

Supported Components:

Supported Common Component Attributes and Events:

[h2]Same-Layer Rendering Tags of the Web Page

This specification applies only to web pages and does not apply to third-party frameworks.

If an application needs to use same-layer rendering on a web page loaded by the Web component, you need to specify the <embed> and <object> tags on the web page as the same-layer rendered components based on the following specifications.

Supported HTML5 Tags:

Supported CSS Attributes:

display, position, z-index, visibility, opacity, background-color, background-image, width, height, padding, padding-left, padding-top, padding-right, padding-bottom, margin, margin-left, margin-top, margin-right, margin-bottom, border-width, border-style, border-color, border-left-width, border-left-style, border-left-color, border-top-width, border-top-style, border-top-color, border-right-width, border-right-style, border-right-color, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-right, border-top, border-bottom, border, border-top-left-radius, border-top-right-radius, border-bottom-left-radius, border-bottom-right-radius, border-radius, transition, and transform (Only translate and scale are supported. The value of scale must be greater than or equal to 0.)

Other CSS attributes, such as rotate and skew in the transform attribute, may not meet the expectation.

Lifecycle Management:

When the lifecycle of a tag at the same layer changes, the onNativeEmbedLifecycleChange() callback is triggered.

Distribution and Processing of the Input Events:

Visibility Changes:

The onNativeEmbedVisibilityChange callback is triggered when the visibility of the same-layer tag changes.

Status change of the param element embedded in the same-layer rendering tag <object>:

The onNativeEmbedObjectParamChange() callback is triggered when the param element of the same-layer rendering tag <object> changes.

Constraints:

Rendering Text Boxes at the Same Layer on Web Pages

On web pages, you can render the system ArkUI TextInput components at the same layer. The following figure shows the effect of three text boxes that are rendered at the same layer.

Figure 4 Same-layer rendering text boxes

  1. Mark the HTML tags that need to be rendered at the same layer on the web page.

    The <embed> and <object> tags support same-layer rendering, and the type can be specified randomly. They are case insensitive and will be converted to lowercase letters by the ArkWeb kernel. The tag string is matched using the entire string, and the type string is matched using the prefix.

    If you do not use this API, or if the API receives an invalid string (an empty string), the ArkWeb kernel uses the default settings, that is, the "embed" + "native/" prefix pattern. If the specified type overlaps with the standard types of the <embed> or <object> tags defined by W3C, for example, registerNativeEmbedRule("object", "application/pdf"), ArkWeb follows the W3C standard behavior and does not recognize it as a same-layer tag.

    • Use the <embed> tags.

      <!--HAP's src/main/resources/rawfile/text.html-->
      <!DOCTYPE html>
      <html>
      <head>
          <title>Same-layer Rendering HTML</title>
          <meta name="viewport">
      </head>
      
      <body style="background:white">
      
      <embed id = "input1" type="native/view" style="width: 100%; height: 100px; margin: 30px; margin-top: 600px"/>
      
      <embed id = "input2" type="native/view2" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"/>
      
      <embed id = "input3" type="native/view3" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"/>
      
      </body>
      </html>
    • Use the <object> tags.

      Call registerNativeEmbedRule to register a <object> tag.

      Web({src: $rawfile('test2.html'), controller: this.browserTabController})
        // Register the same-layer tag as 'object' with the 'test' prefix.
        .registerNativeEmbedRule('object', 'test')

      Example of using registerNativeEmbedRule on the frontend page, with the tag of "object" and type of "test":

      <!--HAP's src/main/resources/rawfile/test2.html-->
      <!DOCTYPE html>
      <html>
      <head>
          <title>Same-layer Rendering HTML</title>
          <meta name="viewport">
      </head>
      
      <body style="background:white">
      
      <object id = "input1" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 600px"></object>
      
      <object id = "input2" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"></object>
      
      <object id = "input3" type="test/input" style="width: 100%; height: 100px; margin: 30px; margin-top: 50px"></object>
      
      </body>
      </html>
  2. Use enableNativeEmbedMode to enable same-layer rendering on the application.

    The same-layer rendering feature is disabled by default. To enable this feature, set enableNativeEmbedMode.

    Web({src: $rawfile('test2.html'), controller: this.browserTabController})
    // ...
      // Enable the same-layer rendering switch.
      .enableNativeEmbedMode(true)
  3. Create a custom component.

    The custom component is displayed as a system component in the corresponding area after same-layer rendering is enabled.

    @Component
    struct TextInputComponent {
      @Prop params: Params;
      @State bkColor: Color = Color.White;
    
      build() {
        Column() {
          TextInput({text: '', placeholder: 'please input your word...'})
            .placeholderColor(Color.Gray)
            .id(this.params?.elementId)
            .placeholderFont({size: 13, weight: 400})
            .caretColor(Color.Gray)
            .fontSize(14)
            .fontColor(Color.Black)
        }
        // The width and height of the outermost container component in the custom component should match those of the same-layer tag.
        .width(this.params.width)
        .height(this.params.height)
      }
    }
    
    // The @Builder contains the specific component content of the dynamic component.
    @Builder
    function textInputBuilder(params:Params) {
      TextInputComponent({params: params})
        .backgroundColor(Color.White)
    }
  4. Create a node controller.

    The node controller is used to control and report node behaviors of the corresponding NodeContainer.

    // Used to control and provide feedback on the behavior of the node on the corresponding NodeContainer. It must be used together with NodeContainer.
    class MyNodeController extends NodeController {
      private rootNode: BuilderNode<[Params]> | undefined | null;
      private embedId_: string = '';
      private surfaceId_: string = '';
      private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
      private width_: number = 0;
      private height_: number = 0;
      private type_: string = '';
      private isDestroy_: boolean = false;
    
      setRenderOption(params: NodeControllerParams) {
        this.surfaceId_ = params.surfaceId;
        this.renderType_ = params.renderType;
        this.embedId_ = params.embedId;
        this.width_ = params.width;
        this.height_ = params.height;
        this.type_ = params.type;
      }
    
      // A method that must be overridden. It is used to build the node tree and return the node tree to be mounted on the corresponding NodeContainer.
      // Called when the corresponding NodeContainer is created or called by the rebuild method.
      makeNode(uiContext: UIContext): FrameNode | null {
        if (this.isDestroy_) { // rootNode is null.
          return null;
        }
        if (!this.rootNode) { // When rootNode is undefined.
          this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });
          if (this.rootNode) {
            this.rootNode.build(
              wrapBuilder(textInputBuilder), {  textOne: 'myTextInput', width: this.width_, height: this.height_  });
            return this.rootNode.getFrameNode();
          }else{
            return null;
          }
        }
        // Return the FrameNode object.
        return this.rootNode.getFrameNode();
      }
    
      updateNode(arg: Object): void {
        this.rootNode?.update(arg);
      }
    
      getEmbedId(): string {
        return this.embedId_;
      }
    
      setDestroy(isDestroy: boolean): void {
        this.isDestroy_ = isDestroy;
        if (this.isDestroy_) {
          this.rootNode = null;
        }
      }
    
      postEvent(event: TouchEvent | undefined): boolean {
        return this.rootNode?.postTouchEvent(event) as boolean;
      }
    
      postInputEvent(event: MouseEvent | undefined): boolean {
        return this.rootNode?.postInputEvent(event) as boolean;
      }
    }
  5. Listen for the lifecycle changes of the same-layer rendered tags.

    After this feature is enabled, the ArkWeb kernel triggers the callback registered by onNativeEmbedLifecycleChange each time a same-layer rendered tag is used on the web page.

    Call onNativeEmbedLifecycleChange to listen for the lifecycle changes of the same-layer rendered tags.

    // Obtain the lifecycle change data of the embed tag.
    .onNativeEmbedLifecycleChange((embed) => {
      console.info('NativeEmbed surfaceId' + embed.surfaceId);
      // If embed.info.id is used as the key for mapping nodeController, explicitly specify the id on the H5 page.
      const componentId = embed.info?.id?.toString() as string;
      if (embed.status === NativeEmbedStatus.CREATE) {
        console.info('NativeEmbed create' + JSON.stringify(embed.info));
        // Create the node controller and set parameters.
        let nodeController = new MyNodeController();
        // embed.info.width and embed.info.height are in px and need to be converted to vp, the default unit on the ets side.
        nodeController.setRenderOption({surfaceId : embed.surfaceId as string,
          type : embed.info?.type as string,
          renderType : NodeRenderType.RENDER_TYPE_TEXTURE,
          embedId : embed.embedId as string,
          width : this.uiContext.px2vp(embed.info?.width),
          height : this.uiContext.px2vp(embed.info?.height)});
        this.edges = {
          left: `${embed.info?.position?.x as number}px`,
          top: `${embed.info?.position?.y as number}px`
        };
        nodeController.setDestroy(false);
        // Use the id attribute of the embed passed by Web as the key to store nodeController in the Map.
        this.nodeControllerMap.set(componentId, nodeController);
        this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
        this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
        this.positionMap.set(componentId, this.edges);
        // Store the id attribute of the embed passed by Web in the @State state array variable for dynamically creating the nodeContainer node container. The push action must be placed after set.
        this.componentIdArr.push(componentId);
      } else if (embed.status === NativeEmbedStatus.UPDATE) {
        let nodeController = this.nodeControllerMap.get(componentId);
        console.info('NativeEmbed update' + JSON.stringify(embed));
        this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`};
        this.positionMap.set(componentId, this.edges);
        this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
        this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
        interface UpdateNodeParams {
          textOne: string;
          width: number;
          height: number;
        }
        const updateParams: UpdateNodeParams = {
          textOne: 'update',
          width: this.uiContext.px2vp(embed.info?.width),
          height: this.uiContext.px2vp(embed.info?.height)
        }
        nodeController?.updateNode(updateParams);
      } else if (embed.status === NativeEmbedStatus.DESTROY) {
        console.info('NativeEmbed destroy' + JSON.stringify(embed));
        let nodeController = this.nodeControllerMap.get(componentId);
        nodeController?.setDestroy(true);
        this.nodeControllerMap.delete(componentId);
        this.positionMap.delete(componentId);
        this.widthMap.delete(componentId);
        this.heightMap.delete(componentId);
        this.componentIdArr = this.componentIdArr.filter((value: string) => value != componentId);
      } else {
        console.info('NativeEmbed status' + embed.status);
      }
    })
  6. Listen for gesture events of same-layer rendered region.

    When this feature is enabled, the ArkWeb kernel triggers the callback registered by onNativeEmbedGestureEvent each time a touch operation is performed in the same-layer rendered region.

    Call onNativeEmbedGestureEvent to listen for gesture events of the same-layer rendered region.

    // Obtain the touch event information of the same-layer rendering component.
    .onNativeEmbedGestureEvent((touch) => {
      console.info('NativeEmbed onNativeEmbedGestureEvent' + JSON.stringify(touch.touchEvent));
      this.componentIdArr.forEach((componentId: string) => {
        let nodeController = this.nodeControllerMap.get(componentId);
        // Send the obtained same-layer area event to the nodeController corresponding to the embedId of the area.
        if (nodeController?.getEmbedId() === touch.embedId) {
          let ret = nodeController?.postEvent(touch.touchEvent);
          if (ret) {
            console.info('onNativeEmbedGestureEvent success ' + componentId);
          } else {
            console.info('onNativeEmbedGestureEvent fail ' + componentId);
          }
          if (touch.result) {
            // Notify the Web component of the gesture event consumption result.
            touch.result.setGestureEventResult(ret);
          }
        }
      })
    })
  7. Listen for mouse events of the same-layer rendered region.

    After this feature is enabled, the ArkWeb kernel triggers the callback registered by onNativeEmbedMouseEvent when the following operations are performed in the same-layer rendered area:

    • Tapping or holding with the left, middle, or right mouse button.
    • Tapping or holding the left, middle, or right mouse button using the touchpad.

    Call onNativeEmbedMouseEvent to listen for the mouse events in the same-layer rendering region.

    .onNativeEmbedMouseEvent((mouse) => {
      console.info('NativeEmbed onNativeEmbedMouseEvent' + JSON.stringify(mouse.mouseEvent));
      this.componentIdArr.forEach((componentId: string) => {
        let nodeController = this.nodeControllerMap.get(componentId);
        // Send the obtained same-layer area event to the nodeController corresponding to the embedId of the area.
        if (nodeController?.getEmbedId() === mouse.embedId) {
          let ret = nodeController?.postInputEvent(mouse.mouseEvent);
          if (ret) {
            console.info('onNativeEmbedMouseEvent success ' + componentId);
          } else {
            console.info('onNativeEmbedMouseEvent fail ' + componentId);
          }
          if (mouse.result) {
            // Notify the Web component of the mouse event consumption result.
            mouse.result.setMouseEventResult(ret);
          }
        }
      })
    })

Sample Code

To start with, add network permissions to module.json5. For details, see Declaring Permissions in the Configuration File.

"requestPermissions":[
  {
    "name" : "ohos.permission.INTERNET"
  }
],

Code on the application side:

import { webview } from '@kit.ArkWeb';
import { UIContext, NodeController, BuilderNode, NodeRenderType, FrameNode } from '@kit.ArkUI';

@Observed
declare class Params{
  public elementId: string
  public textOne: string
  public textTwo: string
  public width: number
  public height: number
}

declare class NodeControllerParams {
  public surfaceId: string
  public type: string
  public renderType: NodeRenderType
  public embedId: string
  public width: number
  public height: number
}

// The NodeController instance must be used with a NodeContainer for controlling and feeding back the behavior of the nodes in the container.
class MyNodeController extends NodeController {
  private rootNode: BuilderNode<[Params]> | undefined | null;
  private embedId_: string = '';
  private surfaceId_: string = '';
  private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
  private width_: number = 0;
  private height_: number = 0;
  private type_: string = '';
  private isDestroy_: boolean = false;

  setRenderOption(params: NodeControllerParams) {
    this.surfaceId_ = params.surfaceId;
    this.renderType_ = params.renderType;
    this.embedId_ = params.embedId;
    this.width_ = params.width;
    this.height_ = params.height;
    this.type_ = params.type;
  }

  // A method that must be overridden. It builds the node tree and returns the node tree to be mounted in the corresponding NodeContainer.
  // Called when the corresponding NodeContainer is created or called by the rebuild method.
  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.isDestroy_) { // rootNode is null.
      return null;
    }
    if (!this.rootNode) { // When rootNode is undefined.
      this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });
      if (this.rootNode) {
        this.rootNode.build(
          wrapBuilder(textInputBuilder), {  textOne: 'myTextInput', width: this.width_, height: this.height_  });
        return this.rootNode.getFrameNode();
      }else{
        return null;
      }
    }
    // Return the FrameNode object.
    return this.rootNode.getFrameNode();
  }

  updateNode(arg: Object): void {
    this.rootNode?.update(arg);
  }

  getEmbedId(): string {
    return this.embedId_;
  }

  setDestroy(isDestroy: boolean): void {
    this.isDestroy_ = isDestroy;
    if (this.isDestroy_) {
      this.rootNode = null;
    }
  }

  postEvent(event: TouchEvent | undefined): boolean {
    return this.rootNode?.postTouchEvent(event) as boolean;
  }

  postInputEvent(event: MouseEvent | undefined): boolean {
    return this.rootNode?.postInputEvent(event) as boolean;
  }
}

@Component
struct TextInputComponent {
  @Prop params: Params;
  @State bkColor: Color = Color.White;

  build() {
    Column() {
      TextInput({text: '', placeholder: 'please input your word...'})
        .placeholderColor(Color.Gray)
        .id(this.params?.elementId)
        .placeholderFont({size: 13, weight: 400})
        .caretColor(Color.Gray)
        .fontSize(14)
        .fontColor(Color.Black)
    }
    // The width and height of the outermost container component in the custom component should match those of the same-layer tag.
    .width(this.params.width)
    .height(this.params.height)
  }
}

// In @Builder, add the specific dynamic component content.
@Builder
function textInputBuilder(params:Params) {
  TextInputComponent({params: params})
    .backgroundColor(Color.White)
}

@Entry
@Component
struct Page{
  browserTabController: WebviewController = new webview.WebviewController()
  private nodeControllerMap: Map<string, MyNodeController> = new Map();
  @State componentIdArr: Array<string> = [];
  @State widthMap: Map<string, number> = new Map();
  @State heightMap: Map<string, number> = new Map();
  @State positionMap: Map<string, Edges> = new Map();
  @State edges: Edges = {};
  uiContext: UIContext = this.getUIContext();

  build() {
    Row() {
      Column() {
        Stack() {
          ForEach(this.componentIdArr, (componentId: string) => {
            NodeContainer(this.nodeControllerMap.get(componentId))
              .position(this.positionMap.get(componentId))
              .width(this.widthMap.get(componentId))
              .height(this.heightMap.get(componentId))
          }, (embedId: string) => embedId)
          // The Web component loads the local test2.html page.
          Web({src: $rawfile('test2.html'), controller: this.browserTabController})
            // Register the same-layer tag as 'object' with the 'test' type prefix.
            .registerNativeEmbedRule('object', 'test')
            // Enable same-layer rendering.
            .enableNativeEmbedMode(true)
            // Obtain the lifecycle change data of the embed tag.
            .onNativeEmbedLifecycleChange((embed) => {
              console.info('NativeEmbed surfaceId' + embed.surfaceId);
              // If embed.info.id is used as the key for mapping nodeController, explicitly specify the id in the H5 page.
              const componentId = embed.info?.id?.toString() as string;
              if (embed.status === NativeEmbedStatus.CREATE) {
                console.info('NativeEmbed create' + JSON.stringify(embed.info));
                // Create the node controller and set parameters.
                let nodeController = new MyNodeController();
                // embed.info.width and embed.info.height are in px and need to be converted to vp, the default unit on the ets side.
                nodeController.setRenderOption({surfaceId : embed.surfaceId as string,
                  type : embed.info?.type as string,
                  renderType : NodeRenderType.RENDER_TYPE_TEXTURE,
                  embedId : embed.embedId as string,
                  width : this.uiContext.px2vp(embed.info?.width),
                  height : this.uiContext.px2vp(embed.info?.height)});
                this.edges = {
                  left: `${embed.info?.position?.x as number}px`,
                  top: `${embed.info?.position?.y as number}px`
                };
                nodeController.setDestroy(false);
                // Use the id attribute of the embed passed by Web as the key to store nodeController in the Map.
                this.nodeControllerMap.set(componentId, nodeController);
                this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
                this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
                this.positionMap.set(componentId, this.edges);
                // Store the id attribute of the embed passed by Web in the @State array variable for dynamically creating the nodeContainer node container. The push action must be placed after set.
                this.componentIdArr.push(componentId);
              } else if (embed.status === NativeEmbedStatus.UPDATE) {
                let nodeController = this.nodeControllerMap.get(componentId);
                console.info('NativeEmbed update' + JSON.stringify(embed));
                this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`};
                this.positionMap.set(componentId, this.edges);
                this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
                this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
                interface UpdateNodeParams {
                  textOne: string;
                  width: number;
                  height: number;
                }
                const updateParams: UpdateNodeParams = {
                  textOne: 'update',
                  width: this.uiContext.px2vp(embed.info?.width),
                  height: this.uiContext.px2vp(embed.info?.height)
                }
                nodeController?.updateNode(updateParams);
              } else if (embed.status === NativeEmbedStatus.DESTROY) {
                console.info('NativeEmbed destroy' + JSON.stringify(embed));
                let nodeController = this.nodeControllerMap.get(componentId);
                nodeController?.setDestroy(true);
                this.nodeControllerMap.delete(componentId);
                this.positionMap.delete(componentId);
                this.widthMap.delete(componentId);
                this.heightMap.delete(componentId);
                this.componentIdArr = this.componentIdArr.filter((value: string) => value != componentId);
              } else {
                console.info('NativeEmbed status' + embed.status);
              }
            })
            // Obtain the touch event information of the same-layer rendering component.
            .onNativeEmbedGestureEvent((touch) => {
              console.info('NativeEmbed onNativeEmbedGestureEvent' + JSON.stringify(touch.touchEvent));
              this.componentIdArr.forEach((componentId: string) => {
                let nodeController = this.nodeControllerMap.get(componentId);
                // Send the obtained event of the region at the same layer to the nodeController corresponding to embedId of the region.
                if (nodeController?.getEmbedId() === touch.embedId) {
                  let ret = nodeController?.postEvent(touch.touchEvent);
                  if (ret) {
                    console.info('onNativeEmbedGestureEvent success ' + componentId);
                  } else {
                    console.info('onNativeEmbedGestureEvent fail ' + componentId);
                  }
                  if (touch.result) {
                    // Notify the Web component of the consumption result of the gesture event.
                    touch.result.setGestureEventResult(ret);
                  }
                }
              })
            })
            .onNativeEmbedMouseEvent((mouse) => {
              console.info('NativeEmbed onNativeEmbedMouseEvent' + JSON.stringify(mouse.mouseEvent));
              this.componentIdArr.forEach((componentId: string) => {
                let nodeController = this.nodeControllerMap.get(componentId);
                // Send the obtained event of the region at the same layer to the nodeController corresponding to embedId of the region.
                if (nodeController?.getEmbedId() === mouse.embedId) {
                  let ret = nodeController?.postInputEvent(mouse.mouseEvent);
                  if (ret) {
                    console.info('onNativeEmbedMouseEvent success ' + componentId);
                  } else {
                    console.info('onNativeEmbedMouseEvent fail ' + componentId);
                  }
                  if (mouse.result) {
                    // Notify the Web component of the consumption result of the mouse event.
                    mouse.result.setMouseEventResult(ret);
                  }
                }
              })
            })
        }
      }
    }
  }
}

Drawing the XComponent+AVPlayer and Button Components

Setting the Same-Layer Tag to the Highest Level

The same-layer rendering supports the private attribute arkwebnativestyle, which takes effect only in <embed> and <object> after same-layer rendering is enabled. The display attribute of arkwebnativestyle is used to set the level of the same-layer tag to be above other web elements. When multiple same-layer tags have the same display value in arkwebnativestyle, their stacking order follows the W3C standard: first by z-index, then by their order in the DOM if z-index values are the same. The values of the display attribute are listed in the following table.

Value Description
overlay Sets the level of the same-layer tag to be higher than that of other web elements.
overlay-infinity Sets the level of the same-layer tag to be higher than that of other web elements and the same-layer tag with the overlay value.
import { webview } from '@kit.ArkWeb';
import { UIContext, NodeController, BuilderNode, NodeRenderType, FrameNode } from '@kit.ArkUI';

@Observed
declare class Params{
  public elementId: string
  public textOne: string
  public textTwo: string
  public width: number
  public height: number
}

declare class NodeControllerParams {
  public surfaceId: string
  public type: string
  public renderType: NodeRenderType
  public embedId: string
  public width: number
  public height: number
}

// Used to control and report the behavior of the node on the corresponding NodeContainer. It must be used together with NodeContainer.
class MyNodeController extends NodeController {
  private rootNode: BuilderNode<[Params]> | undefined | null;
  private embedId_: string = '';
  private surfaceId_: string = '';
  private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
  private width_: number = 0;
  private height_: number = 0;
  private type_: string = '';
  private isDestroy_: boolean = false;

  setRenderOption(params: NodeControllerParams) {
    this.surfaceId_ = params.surfaceId;
    this.renderType_ = params.renderType;
    this.embedId_ = params.embedId;
    this.width_ = params.width;
    this.height_ = params.height;
    this.type_ = params.type;
  }

  // A method that must be overridden. It is used to build the node tree and return the node tree to be mounted in the corresponding NodeContainer.
  // Called when the corresponding NodeContainer is created, or refreshed by calling the rebuild method.
  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.isDestroy_) { // rootNode is null.
      return null;
    }
    if (!this.rootNode) {// When rootNode is undefined.
      this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });
      if (this.type_ === 'native/view1') {
        this.rootNode.build(
          wrapBuilder(textInputBuilder1), {  textOne: 'myTextInput', width: this.width_, height: this.height_  });
        return this.rootNode.getFrameNode();
      } else if (this.type_ === 'native/view2') {
        this.rootNode.build(
          wrapBuilder(textInputBuilder2), {  textOne: 'myTextInput', width: this.width_, height: this.height_  });
        return this.rootNode.getFrameNode();
      } else{
        return null;
      }
    }
    // Return the FrameNode node.
    return this.rootNode.getFrameNode();
  }

  updateNode(arg: Object): void {
    this.rootNode?.update(arg);
  }

  getEmbedId(): string {
    return this.embedId_;
  }

  setDestroy(isDestroy: boolean): void {
    this.isDestroy_ = isDestroy;
    if (this.isDestroy_) {
      this.rootNode = null;
    }
  }

  postEvent(event: TouchEvent | undefined): boolean {
    return this.rootNode?.postTouchEvent(event) as boolean;
  }
}

@Component
struct TextInputComponent1 {
  @Prop params: Params;
  @State bkColor: Color = Color.White;

  build() {
    Column() {
      Text('display:overlay-infinity')
      TextInput({text: '', placeholder: 'please input your word...'})
        .placeholderColor(Color.Gray)
        .id(this.params?.elementId)
        .placeholderFont({size: 13, weight: 400})
        .caretColor(Color.Gray)
        .fontSize(14)
        .fontColor(Color.Black)
    }
    // The width and height of the outermost container component in the custom component should be the width and height of the same-layer tag.
    .width(this.params.width)
    .height(this.params.height)
  }
}

// The specific component content of the dynamic component in @Builder.
@Builder
function textInputBuilder1(params:Params) {
  TextInputComponent1({params: params})
    .backgroundColor(Color.Pink)
}

@Component
struct TextInputComponent2 {
  @Prop params: Params;
  @State bkColor: Color = Color.White;

  build() {
    Column() {
      Text('display:overlay')
      TextInput({text: '', placeholder: 'please input your word...'})
        .placeholderColor(Color.Gray)
        .id(this.params?.elementId)
        .placeholderFont({size: 13, weight: 400})
        .caretColor(Color.Gray)
        .fontSize(14)
        .fontColor(Color.Black)
    }
    // The width and height of the outermost container component in the custom component should be the width and height of the same-layer tag.
    .width(this.params.width)
    .height(this.params.height)
  }
}


// The specific component content of the dynamic component in @Builder.
@Builder
function textInputBuilder2(params:Params) {
  TextInputComponent2({params: params})
    .backgroundColor(Color.Gray)
}

@Entry
@Component
struct Page{
  browserTabController: webview.WebviewController = new webview.WebviewController();
  private nodeControllerMap: Map<string, MyNodeController> = new Map();
  @State componentIdArr: Array<string> = [];
  @State widthMap: Map<string, number> = new Map();
  @State heightMap: Map<string, number> = new Map();
  @State positionMap: Map<string, Edges> = new Map();
  @State edges: Edges = {};
  uiContext: UIContext = this.getUIContext();

  build() {
    Row() {
      Column() {
        Stack() {
          ForEach(this.componentIdArr, (componentId: string) => {
            NodeContainer(this.nodeControllerMap.get(componentId))
              .position(this.positionMap.get(componentId))
              .width(this.widthMap.get(componentId))
              .height(this.heightMap.get(componentId))
          }, (embedId: string) => embedId)
          // The Web component loads the local test4.html page.
          Web({src: $rawfile('test4.html'), controller: this.browserTabController})
          // Enable the same-layer rendering switch.
            .enableNativeEmbedMode(true)
            // Obtain the lifecycle change data of the <embed> tag.
            .onNativeEmbedLifecycleChange((embed) => {
              console.info('NativeEmbed surfaceId' + embed.surfaceId);
              // If embed.info.id is used as the key for mapping nodeController, explicitly specify the id in the H5 page.
              const componentId = embed.info?.id?.toString() as string
              if (embed.status === NativeEmbedStatus.CREATE) {
                console.info('NativeEmbed create' + JSON.stringify(embed.info));
                // Create the node controller and set parameters.
                let nodeController = new MyNodeController();
                // The units of embed.info.width and embed.info.height are px, which need to be converted to vp, the default unit on the ets side.
                nodeController.setRenderOption({surfaceId : embed.surfaceId as string,
                  type : embed.info?.type as string,
                  renderType : NodeRenderType.RENDER_TYPE_TEXTURE,
                  embedId : embed.embedId as string,
                  width : this.uiContext.px2vp(embed.info?.width),
                  height : this.uiContext.px2vp(embed.info?.height)});
                this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`};
                nodeController.setDestroy(false);
                // Use the id attribute of the embed passed in by Web as the key to store nodeController in the Map.
                this.nodeControllerMap.set(componentId, nodeController);
                this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
                this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
                this.positionMap.set(componentId, this.edges);
                // Store the id attribute of the embed passed in by Web in the @State state array variable for dynamically creating the nodeContainer node container. The push action must be placed after set.
                this.componentIdArr.push(componentId);
              } else if (embed.status === NativeEmbedStatus.UPDATE) {
                let nodeController = this.nodeControllerMap.get(componentId);
                console.info('NativeEmbed update' + JSON.stringify(embed));
                this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`};
                this.positionMap.set(componentId, this.edges);
                this.widthMap.set(componentId, this.uiContext.px2vp(embed.info?.width));
                this.heightMap.set(componentId, this.uiContext.px2vp(embed.info?.height));
                interface UpdateNodeParams {
                  textOne: string;
                  width: number;
                  height: number;
                }
                const updateParams: UpdateNodeParams = {
                  textOne: 'update',
                  width: this.uiContext.px2vp(embed.info?.width),
                  height: this.uiContext.px2vp(embed.info?.height)
                }
                nodeController?.updateNode(updateParams);
              } else if (embed.status === NativeEmbedStatus.DESTROY) {
                console.info('NativeEmbed destroy' + JSON.stringify(embed));
                let nodeController = this.nodeControllerMap.get(componentId);
                nodeController?.setDestroy(true);
                this.nodeControllerMap.delete(componentId);
                this.positionMap.delete(componentId);
                this.widthMap.delete(componentId);
                this.heightMap.delete(componentId);
                this.componentIdArr = this.componentIdArr.filter((value: string) => value != componentId);
              } else {
                console.info('NativeEmbed status' + embed.status);
              }
            })// Obtain the touch event information of the same-layer rendering component.
            .onNativeEmbedGestureEvent((touch) => {
              console.info('NativeEmbed onNativeEmbedGestureEvent' + JSON.stringify(touch.touchEvent));
              this.componentIdArr.forEach((componentId: string) => {
                let nodeController = this.nodeControllerMap.get(componentId);
                // Send the obtained event of the same-layer area to the nodeController corresponding to the embedId of that area.
                if (nodeController?.getEmbedId() === touch.embedId) {
                  let ret = nodeController?.postEvent(touch.touchEvent);
                  if (ret) {
                    console.info('onNativeEmbedGestureEvent success ' + componentId);
                  } else {
                    console.info('onNativeEmbedGestureEvent fail ' + componentId);
                  }
                  if (touch.result) {
                    // Notify the Web component of the gesture event consumption result.
                    touch.result.setGestureEventResult(ret);
                  }
                }
              })
            })
            .border({width: 2, color: Color.Gray})
            .height('50%')
        }
      }
    }
  }
}

Aligning the Same-layer Rendered Texture Map

The private attribute** arkwebnativestyle** for ArkWeb same-layer rendering scenarios only takes effect on the and tags after same-layer rendering is enabled. Since API version 23, two configurations are added: object-fit: stretch (the default value, where the texture is stretched and aligned according to the bounds size of the same-layer tag) and object-fit: none (where the texture is not stretched and aligned with the top-left corner). These configurations are used to control the texture alignment mode of a single same-layer tag for flexible display effects. They can solve the texture stretching issue that occurs in scenarios where the width and height of the same-layer tag change dynamically.

The values of the attribute are listed in the following table.

Value Description
object-fit:stretch Default value. The texture is stretched and aligned based on the bounds of the same-layer tag.
object-fit:none The texture is not stretched and aligned with the left top corner.

FAQs

[h2]What should I do if the same-layer rendered components are stretched?

[h2]How to Pass Events Captured by Same-Layer Rendering Components to the Web Frontend?

For gesture events of the same-layer rendered tag, you can set the gesture event consumption result through setGestureEventResult(). The gesture events can be consumed on the system component side or ArkWeb side. To consume gesture events on the system component side and ArkWeb side at the same time, set stopPropagation to false in setGestureEventResult(). In this way, gesture events are propagated to ArkWeb when being consumed on the system component side.

[h2]What should I do if plug-ins cannot be displayed on the same-layer rendered page?

[h2]What should I do if the cursor and text box of the ArkUI component (such as TextInput) that involves UI interaction are misplaced?

You need to use a Stack to wrap the same-layer component container and BuilderNode. Then you need to bind NodeContainer to the position of the same-layer tag. The following is an example:

ForEach(this.componentIdArr, (componentId: string) => {
  NodeContainer(this.nodeControllerMap.get(componentId))
    // The same-layer component container should be bound to the width, height, and position of the same-layer tag.
    .position(this.positionMap.get(componentId))
    .width(this.widthMap.get(componentId))
    .height(this.heightMap.get(componentId))
}, (embedId: string) => embedId)