harmony 鸿蒙Using Same-Layer Rendering
Using Same-Layer Rendering
In the system, applications can use the Web component to load web pages. If the capability or performance of non-native UI components (same-layer components) is inferior to that of native components, you can use the ArkUI component to render these components.
When to Use
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. - On the web page, you can render the UI components (same-layer tags) such as <embed> and <object> at the same layer based on certain rules. For details, see Specifications and Constraints.
On the application, you can use the same-layer rendering event reporting API of the Web component to detect the lifecycle and input event of the HTML5 same-layer tags, and process the service logic of the same-layer rendering components.
In addition, you can use ArkUI APIs such as NodeContainer to construct same-layer rendering components corresponding to HTML5 same-layer tags. Common ArkUI components that support same-layer rendering: TextInput, XComponent, Canvas, Video, Web. For details, see Specifications and Constraints.
On the Third-Party UI Framework
Flutter provides the PlatformView and Texture abstract components that can be rendered using native components, which complete the functions of the Flutter components. Weex2.0 framework supports the Camera, Video, and Canvas components.
Since third-party frameworks such as Flutter and Weex are not operated in the OS, the available third-party framework UI components that can be rendered at the same layer are not listed in the following.
On the application, you can use ArkUI APIs such as NodeContainer to construct same-layer rendering components corresponding to third-party framework same-layer tags. Common ArkUI components that support same-layer rendering: TextInput, XComponent, Canvas, Video, Web. For details, see Specifications and Constraints.
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
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:
Basic components: AlphabetIndexer, Blank, Button, CalendarPicker, Checkbox, CheckboxGroup, ContainerSpan, DataPanel, DatePicker, Divider, Gauge, Hyperlink, Image, ImageAnimator, ImageSpan, LoadingProgress, Marquee, PatternLock, Progress, QRCode, Radio, Rating, Refresh, ScrollBar, Search, Span, Select, Slider, Text, TextArea, TextClock, TextInput, TextPicker, TextTimer, TimePicker, Toggle
Container components: Badge, Column, ColumnSplit, Counter, Flex, GridCol, GridRow, Grid, GridItem and List, ListItem, ListItemGroup, RelativeContainer, Row, RowSplit, Scroll, Stack, Swiper, Tabs, TabContent, NodeContainer, SideBarContainer, Stepper, StepperItem, WaterFlow, FlowItem
Self-drawing components: XComponent, Canvas, Video, Web
Command-based custom drawing nodes: BuilderNode, ComponentContent, ContentSlot, FrameNode, Graphics, NodeController, RenderNode, XComponentNode, AttributeUpdater and CAPI (The components that support same-layer rendering are the same as that of ArkTS.)
Supported Common Component Attributes and Events:
Common attributes that are not supported: restoreId and Special Effect Drawing Combination.
Other attributes, events, and component capabilities that are not clearly marked as not supported are supported by default.
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 the 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 rendering components based on the following specifications.
Supported Devices: Currently, only mobile phones and tablets are supported.
Supported HTML5 Tags: - <embed>: After same-layer rendering is enabled, only tags whose type is prefixed with native can be identified as same-layer components. Attributes cannot be customized.
<object>: After the same-layer rendering is enabled, the <object> tag of the non-standard MIME type can be identified as a same-layer component and parsed based on the custom param/value attribute.
W3C standard tags (such as <input> and <video>) cannot be defined as same-layer tags.
The <object> and <embed> tags cannot be configured as the same-layer tags at the same time.
The tag types contain only English characters and are case insensitive.
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 shew in the transform attribute, may not meet the expectation.
Lifecycle Management: The onNativeEmbedLifecycleChange() callback is triggered when the lifecycle of the Embed tag changes.
Creation, destruction, and position width and height change are supported. The visibility status change is not supported.
Web pages containing same-layer components support back-forward cache.
Distributing and Processing the Input Events: - The DOWN, UP, MOVE, and CANCEL touch events are supported. The onnativeembedgestureevent11 can be configured. By default, the touch event is consumed on the application side.
The application page containing same-layer components cannot be scaled, and the scaling APIs such as initialScale, zoom, zoomIn and zoomOut are not supported.
Mouse, keyboard, and touchpad events are not supported.
Constraints:
Configure a maximum of five same-layer tags on a web page. Otherwise, the rendering performance deteriorates.
Due to GPU restrictions, the maximum height and texture size of a same-layer tag are 8000 px.
When same-layer rendering is enabled, web pages opened by the Web component do not support RenderMode.
When the non-full-screen mode is changed to the full-screen mode, the Video component is exported through non-texture mode and the video playback status remains unchanged. When the non-full-screen mode is restored, the Video component is exported through texture mode and the video playback status remains unchanged.
The Web component supports only same-layer rendering nesting at one layer. The input events such as swipe, tap, zoom, and long-press are supported, while drag and rotate events are not supported.
In the page layout of ArkUI components (such as TextInput), you are advised to use a Stack component to wrap the same-layer NodeContainer and BuilderNode and ensure that they are in the same position. In addition, the NodeContainer must be aligned with the <embed>/<object> tag to ensure proper component interaction. If the positions of the two components are different, the following problems may occur: The position of the text selection box attached to the TextInput/TextArea component is incorrect (as shown in the following figure). The animation start and stop of the LoadingProgress/Marquee component do not match the visibility status of the component.
Figure 2 Misplaced TextInput without Stack
Figure 3 Proper TextInput with Stack
Rendering Text Boxes at the Same Layer on Web Pages
On web pages, you can render the native 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
- 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 this API is not used or receives an invalid string (empty string), the ArkWeb kernel uses the default setting, that is, “embed” + “native/” prefix. If the specified type is the same as the W3C standard object or embed type, for example, registerNativeEmbedRule (”object,” “application/pdf”), ArkWeb will comply with the W3C standard behavior and will not identify 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 Test 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("text.html"), controller: this.browserTabController}) // Register the same-layer tag of "object" and type of "test." .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/text.html--> <!DOCTYPE html> <html> <head> <title>Same-Layer Rendering Test 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>
Use enableNativeEmbedMode to enable the same-layer rendering on the application,
which is disabled by default.
// xxx.ets
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Web({ src: 'www.example.com', controller: this.controller })
// Enable same-layer rendering.
.enableNativeEmbedMode(true)
}
}
}
- Create a custom component,
which is displayed as a native component in the corresponding area after the 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)
.width(this.params?.width)
.height(this.params?.height)
.fontSize(14)
.fontColor(Color.Black)
}
// The width and height of the outermost custom container component must be the same as those of the tag at the same layer.
.width(this.params.width)
.height(this.params.height)
}
}
@Builder
function TextInputBuilder(params:Params) {
TextInputComponent({params: params})
.width(params.width)
.height(params.height)
.backgroundColor(Color.White)
}
- Create a node controller,
which is used to control and report node behaviors of the corresponding 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;
}
// Method that must be overridden. It is used to build the number of nodes and return the number of nodes that will be mounted to 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 set to 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();
}
setBuilderNode(rootNode: BuilderNode<Params[]>|null): void {
this.rootNode = rootNode;
}
getBuilderNode(): BuilderNode<[Params]>|undefined|null {
return this.rootNode;
}
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
}
}
- Call onNativeEmbedLifecycleChange to listen for the lifecycle changes of the same-layer rendering tags.
After this function is enabled, the ArkWeb kernel triggers the callback registered by onNativeEmbedLifecycleChange each time a same-layer rendering tag is used on a web page.
```ts
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)
// Load the local text.html page.
Web({src: $rawfile("text.html"), controller: this.browserTabController})
// Enable same-layer rendering.
.enableNativeEmbedMode(true)
// Register the same-layer tag of "object" and type of "test."
.registerNativeEmbedRule("object", "test")
// Obtain the lifecycle change data of the embed tag.
.onNativeEmbedLifecycleChange((embed) => {
console.log("NativeEmbed surfaceId" + embed.surfaceId);
// If embed.info.id is used as the key for mapping nodeController, explicitly specify the ID on the HTML5 page.
const componentId = embed.info?.id?.toString() as string
if (embed.status == NativeEmbedStatus.CREATE) {
console.log("NativeEmbed create" + JSON.stringify(embed.info));
// Create a NodeController instance, set parameters, and rebuild.
let nodeController = new MyNodeController()
// The unit of embed.info.width and embed.info.height is px, which needs to be converted to the default unit vp 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 : px2vp(embed.info?.width),
height : 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);
// Save the nodeController instance to the Map, with the Id attribute of the embed tag passed in by the Web component as the key.
this.nodeControllerMap.set(componentId, nodeController);
this.widthMap.set(componentId, px2vp(embed.info?.width));
this.heightMap.set(componentId, px2vp(embed.info?.height));
this.positionMap.set(componentId, this.edges);
// Save the Id attribute of the embed tag passed in by the Web component to the @State decorated variable for dynamically creating a nodeContainer. The push action must be executed after the set action.
this.componentIdArr.push(componentId)
} else if (embed.status == NativeEmbedStatus.UPDATE) {
let nodeController = this.nodeControllerMap.get(componentId);
console.log("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, px2vp(embed.info?.width));
this.heightMap.set(componentId, px2vp(embed.info?.height));
nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject)
} else if (embed.status == NativeEmbedStatus.DESTROY) {
console.log("NativeEmbed destroy" + JSON.stringify(embed));
let nodeController = this.nodeControllerMap.get(componentId);
nodeController?.setDestroy(true)
this.nodeControllerMap.clear();
this.positionMap.delete(componentId);
this.widthMap.delete(componentId);
this.heightMap.delete(componentId);
this.componentIdArr.filter((value: string) => value != componentId)
} else {
console.log("NativeEmbed status" + embed.status);
}
})
}.height("80%")
}
}
}
```
- Call onNativeEmbedGestureEvent to listen for gesture events that are rendered at the same layer.
When gesture events are listened, the ArkWeb kernel triggers the callback registered by onNativeEmbedGestureEvent each time a touch operation is performed in the same-layer rendering region.
```ts
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)
// Load the local text.html page.
Web({src: $rawfile("text.html"), controller: this.browserTabController})
// Enable same-layer rendering.
.enableNativeEmbedMode(true)
// Obtain the lifecycle change data of the embed tag.
.onNativeEmbedLifecycleChange((embed) => {
// Implement lifecycle changes.
})
.onNativeEmbedGestureEvent((touch) => {
console.log("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.log("onNativeEmbedGestureEvent success " + componentId);
} else {
console.log("onNativeEmbedGestureEvent fail " + componentId);
}
if(touch.result) {
// Notify the Web component of the gesture event consumption result.
touch.result.setGestureEventResult(ret);
}
}
})
})
}
}
}
}
```
Sample Code
To start with, add the Internet permission to the module.json5 file. For details, see Declaring Permissions.
"requestPermissions":[
{
"name" : "ohos.permission.INTERNET"
}
]
Code on the application side:
// Create a NodeController instance.
import webview from '@ohos.web.webview';
import { UIContext } from '@ohos.arkui.UIContext';
import { NodeController, BuilderNode, NodeRenderType, FrameNode } from "@ohos.arkui.node";
@Observed
declare class Params{
elementId: string
textOne: string
textTwo: string
width: number
height: number
}
declare class NodeControllerParams {
surfaceId: string
type: string
renderType: NodeRenderType
embedId: string
width: number
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;
}
// Method that must be overridden. It is used to build the number of nodes and return the number of nodes that will be mounted to 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 set to 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();
}
setBuilderNode(rootNode: BuilderNode<Params[]>|null): void {
this.rootNode = rootNode;
}
getBuilderNode(): BuilderNode<[Params]>|undefined|null {
return this.rootNode;
}
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 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 custom container component must be the same as those of the tag at the same layer.
.width(this.params.width)
.height(this.params.height)
}
}
// In @Builder, add the specific dynamic component content.
@Builder
function TextInputBuilder(params:Params) {
TextInputComponent({params: params})
.width(params.width)
.height(params.height)
.backgroundColor(Color.White)
}
@Entry
@Component
struct Page{
browserTabController: WebviewController = new webview.WebviewController()
private nodeControllerMap: Map<string, MyNodeController> = new Map();
@State componentIdArr: Array<string> = [];
@State posMap: Map<string, Position|undefined> = new Map();
@State widthMap: Map<string, number> = new Map();
@State heightMap: Map<string, number> = new Map();
@State positionMap: Map<string, Edges> = new Map();
@State edges: Edges = {};
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)
// Load the local text.html page.
Web({src: $rawfile("text.html"), controller: this.browserTabController})
// Enable same-layer rendering.
.enableNativeEmbedMode(true)
// Obtain the lifecycle change data of the embed tag.
.onNativeEmbedLifecycleChange((embed) => {
console.log("NativeEmbed surfaceId" + embed.surfaceId);
// If embed.info.id is used as the key for mapping nodeController, explicitly specify the ID on the HTML5 page.
const componentId = embed.info?.id?.toString() as string
if (embed.status == NativeEmbedStatus.CREATE) {
console.log("NativeEmbed create" + JSON.stringify(embed.info));
// Create a NodeController instance, set parameters, and rebuild.
let nodeController = new MyNodeController()
// The unit of embed.info.width and embed.info.height is px, which needs to be converted to the default unit vp 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 : px2vp(embed.info?.width),
height : 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);
// Save the nodeController instance to the Map, with the Id attribute of the embed tag passed in by the Web component as the key.
this.nodeControllerMap.set(componentId, nodeController);
this.widthMap.set(componentId, px2vp(embed.info?.width));
this.heightMap.set(componentId, px2vp(embed.info?.height));
this.positionMap.set(componentId, this.edges);
// Save the Id attribute of the embed tag passed in by the Web component to the @State decorated variable for dynamically creating a nodeContainer. The push action must be executed after the set action.
this.componentIdArr.push(componentId)
} else if (embed.status == NativeEmbedStatus.UPDATE) {
let nodeController = this.nodeControllerMap.get(componentId);
console.log("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, px2vp(embed.info?.width));
this.heightMap.set(componentId, px2vp(embed.info?.height));
nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject)
} else if (embed.status == NativeEmbedStatus.DESTROY) {
console.log("NativeEmbed destroy" + JSON.stringify(embed));
let nodeController = this.nodeControllerMap.get(componentId);
nodeController?.setDestroy(true)
this.nodeControllerMap.clear();
this.positionMap.delete(componentId);
this.widthMap.delete(componentId);
this.heightMap.delete(componentId);
this.componentIdArr.filter((value: string) => value != componentId)
} else {
console.log("NativeEmbed status" + embed.status);
}
})// Obtain the touch event information of components for same-layer rendering.
.onNativeEmbedGestureEvent((touch) => {
console.log("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.log("onNativeEmbedGestureEvent success " + componentId);
} else {
console.log("onNativeEmbedGestureEvent fail " + componentId);
}
if(touch.result) {
// Notify the Web component of the gesture event consumption result.
touch.result.setGestureEventResult(ret);
}
}
})
})
}
}
}
}
}
Drawing the XComponent+AVPlayer and Button Components
You can enable or disable same-layer rendering through enableNativeEmbedMode(). To use same-layer rendering, the <embed> element must be explicitly used in the HTML file, and the type attribute of the element must start with native/. The background of the elements corresponding to the tags at the same layer is transparent.
- Example of using same-layer rendering on the application side:
// HAP's src/main/ets/pages/Index.ets
// Create a NodeController instance.
import { webview } from '@kit.ArkWeb';
import { UIContext, NodeController, BuilderNode, NodeRenderType, FrameNode } from "@kit.ArkUI";
import { AVPlayerDemo } from './PlayerDemo';
@Observed
declare class Params {
textOne : string
textTwo : string
width : number
height : number
}
declare class NodeControllerParams {
surfaceId : string
type : string
renderType : NodeRenderType
embedId : string
width : number
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;
}
// Method that must be overridden. It is used to build the number of nodes and return the number of nodes that will be mounted to 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 set to undefined
this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_});
if (this.type_ === 'native/video') {
this.rootNode.build(wrapBuilder(VideoBuilder), {textOne: "myButton", width : this.width_, height : this.height_});
} else {
// other
}
}
// Return the FrameNode object.
return this.rootNode.getFrameNode();
}
setBuilderNode(rootNode: BuilderNode<Params[]>|null): void{
this.rootNode = rootNode;
}
getBuilderNode(): BuilderNode<[Params]>|undefined|null{
return this.rootNode;
}
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 VideoComponent {
@ObjectLink params: Params
@State bkColor: Color = Color.Red
mXComponentController: XComponentController = new XComponentController();
@State player_changed: boolean = false;
player?: AVPlayerDemo;
build() {
Column() {
Button(this.params.textOne)
XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController})
.border({width: 1, color: Color.Red})
.onLoad(() => {
this.player = new AVPlayerDemo();
this.player.setSurfaceID(this.mXComponentController.getXComponentSurfaceId());
this.player_changed = !this.player_changed;
this.player.avPlayerLiveDemo()
})
.width(300)
.height(200)
}
// The width and height of the outermost custom container component must be the same as those of the tag at the same layer.
.width(this.params.width)
.height(this.params.height)
}
}
// In @Builder, add the specific dynamic component content.
@Builder
function VideoBuilder(params: Params) {
VideoComponent({ params: params })
.backgroundColor(Color.Gray)
}
@Entry
@Component
struct WebIndex {
browserTabController: WebviewController = new webview.WebviewController()
private nodeControllerMap: Map<string, MyNodeController> = new Map();
@State componentIdArr: Array<string> = [];
aboutToAppear() {
// Enable web frontend page debugging.
webview.WebviewController.setWebDebuggingAccess(true);
}
build(){
Row() {
Column() {
Stack() {
ForEach(this.componentIdArr, (componentId: string) => {
NodeContainer(this.nodeControllerMap.get(componentId))
}, (embedId: string) => embedId)
// Load the local test.html page.
Web({ src: $rawfile("test.html"), controller: this.browserTabController })
// Enable same-layer rendering.
.enableNativeEmbedMode(true)
// Obtain the lifecycle change data of the embed tag.
.onNativeEmbedLifecycleChange((embed) => {
console.log("NativeEmbed surfaceId" + embed.surfaceId);
// 1. If embed.info.id is used as the key for mapping nodeController, explicitly specify the ID on the HTML5 page.
const componentId = embed.info?.id?.toString() as string
if (embed.status == NativeEmbedStatus.CREATE) {
console.log("NativeEmbed create" + JSON.stringify(embed.info))
// Create a NodeController instance, set parameters, and rebuild.
let nodeController = new MyNodeController()
// 1. The unit of embed.info.width and embed.info.height is px, which needs to be converted to the default unit vp 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 : px2vp(embed.info?.width), height : px2vp(embed.info?.height)})
nodeController.setDestroy(false);
// Save the nodeController instance to the Map, with the Id attribute of the embed tag passed in by the Web component as the key.
this.nodeControllerMap.set(componentId, nodeController)
// Save the Id attribute of the embed tag passed in by the Web component to the @State decorated variable for dynamically creating a nodeContainer. The push action must be executed after the set action.
this.componentIdArr.push(componentId)
} else if (embed.status == NativeEmbedStatus.UPDATE) {
let nodeController = this.nodeControllerMap.get(componentId)
nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject)
} else {
let nodeController = this.nodeControllerMap.get(componentId);
nodeController?.setDestroy(true)
this.nodeControllerMap.clear();
this.componentIdArr.length = 0;
}
})// Obtain the touch event information of components for same-layer rendering.
.onNativeEmbedGestureEvent((touch) => {
console.log("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.log("onNativeEmbedGestureEvent success " + componentId)
} else {
console.log("onNativeEmbedGestureEvent fail " + componentId)
}
if (touch.result) {
// Notify the Web component of the gesture event consumption result.
touch.result.setGestureEventResult(ret);
}
}
})
})
}
}
}
}
}
- Example of video playback code on the application side. Replace the video URL with the correct one in practice.
// HAP's src/main/ets/pages/PlayerDemo.ets
import { media } from '@kit.MediaKit';
import { BusinessError } from '@ohos.base';
export class AVPlayerDemo {
private count: number = 0;
private surfaceID: string = ''; // The surfaceID parameter specifies the window used to display the video. Its value is obtained through XComponent.
private isSeek: boolean = true; // Specify whether the seek operation is supported.
setSurfaceID(surface_id: string){
console.log('setSurfaceID : ' + surface_id);
this.surfaceID = surface_id;
}
// Set AVPlayer callback functions.
setAVPlayerCallback(avPlayer: media.AVPlayer) {
// Callback function for the seek operation.
avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// Callback function for errors. If an error occurs during the operation on the AVPlayer, reset() is called to reset the AVPlayer.
avPlayer.on('error', (err: BusinessError) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
avPlayer.reset();
})
// Callback for state changes.
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case 'idle': // This state is reported upon a successful callback of reset().
console.info('AVPlayer state idle called.');
avPlayer.release(); // Call release() to release the instance.
break;
case 'initialized': // This state is reported when the AVPlayer sets the playback source.
console.info('AVPlayer state initialized called.');
avPlayer.surfaceId = this.surfaceID; // Set the window to display the video. This setting is not required when a pure audio asset is to be played.
avPlayer.prepare();
break;
case 'prepared': // This state is reported upon a successful callback of prepare().
console.info('AVPlayer state prepared called.');
avPlayer.play(); // Call play() to start playback.
break;
case 'playing': // This state is reported upon a successful callback of play().
console.info('AVPlayer state prepared called.');
if(this.count !== 0) {
if (this.isSeek) {
console.info('AVPlayer start to seek.');
avPlayer.seek(avPlayer.duration); // Call seek() to seek to the end of the video clip.
} else {
// When the seek operation is not supported, the playback continues until it reaches the end.
console.info('AVPlayer wait to play end.');
}
} else {
avPlayer.pause(); // Call pause() to pause the playback.
}
this.count++;
break;
case 'paused': // This state is reported upon a successful callback of pause().
console.info('AVPlayer state paused called.');
avPlayer.play(); // Call play() again to start playback.
break;
case 'completed': // This state is reported upon the completion of the playback.
console.info('AVPlayer state paused called.');
avPlayer.stop(); // Call stop() to stop the playback.
break;
case 'stopped': // This state is reported upon a successful callback of stop().
console.info('AVPlayer state stopped called.');
avPlayer.reset(); // Call reset() to reset the AVPlayer.
break;
case 'released': // This state is reported upon the release of the AVPlayer.
console.info('AVPlayer state released called.');
break;
default:
break;
}
})
}
// Set the live stream source through the URL.
async avPlayerLiveDemo(){
// Create an AVPlayer instance.
let avPlayer: media.AVPlayer = await media.createAVPlayer();
// Create a callback for state changes.
this.setAVPlayerCallback(avPlayer);
this.isSeek = false; // The seek operation is not supported.
// Replace the URL with the actual URL of the video source.
avPlayer.url = 'https://xxx.xxx/demo.mp4';
}
}
- Example of the frontend page:
<!--HAP's src/main/resources/rawfile/test.html-->
<!DOCTYPE html>
<html>
<head>
<title>Same-Layer Rendering Test HTML</title>
<meta name="viewport">
</head>
<body>
<div>
<div id="bodyId">
<embed id="nativeVideo" type = "native/video" width="1000" height="1500" src="test" style = "background-color:red"/>
</div>
</div>
</body>
</html>
你可能感兴趣的鸿蒙文章
harmony 鸿蒙Taking Over the Media Playback on Web Pages
harmony 鸿蒙Mutual Invoking Between the Application and the Frontend Page (C/C++)
harmony 鸿蒙Establishing a Data Channel Between the Application and the Frontend Page (C/C++)
harmony 鸿蒙Enabling Ads Blocking
harmony 鸿蒙Establishing a Data Channel Between the Application and the Frontend Page
harmony 鸿蒙Migrating Web Components Between Different Windows
harmony 鸿蒙Introduction to ArkWeb
- 所属分类: 后端技术
- 本文标签:
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦