本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

在鸿蒙系统中,应用可以使用Web组件加载网页。当非系统框架的UI组件功能或性能不如系统组件时,可使用同层渲染技术,通过ArkUI组件渲染这些组件(简称"同层组件")。

简单来说,就是让Web页面中的某些UI元素(如输入框、视频播放器等)使用鸿蒙原生组件来渲染,从而获得更好的性能和体验。

使用场景

1. Web网页场景

  • 地图组件:使用ArkUI的XComponent组件渲染,提升性能

  • 输入框组件:使用ArkUI的TextInput组件渲染,获得系统级输入体验

  • 网页侧:开发者可将<embed><object>标签按规则进行同层渲染

2. 三方UI框架场景

  • Flutter:PlatformView与Texture抽象组件可使用系统组件渲染

  • Weex2.0:Camera、Video和Canvas组件可使用系统组件渲染

整体架构

ArkWeb同层渲染特性提供两种核心能力:

  1. 同层标签生命周期管理:关联前端标签(<embed>/<object>

  2. 事件命中转发处理:同层标签的事件上报到开发者侧,由开发者分发到对应组件树

架构流程图

Web页面(embed/object标签) 
    ↓ 识别同层标签
ArkWeb内核 
    ↓ 生命周期回调/事件上报
应用开发者 
    ↓ 创建NodeController/BuilderNode
ArkUI原生组件
    ↓ 渲染
界面展示

四、支持的ArkUI组件

1. 基础组件(部分列举)

  • 文本类:Text、TextInput、TextArea、TextClock、TextPicker、TextTimer

  • 按钮类:Button、Toggle、Radio、Checkbox、CheckboxGroup

  • 选择器:DatePicker、TimePicker、CalendarPicker、Select

  • 进度类:Progress、LoadingProgress、Slider、Rating

  • 图片类:Image、ImageAnimator、ImageSpan

  • 其他:Divider、Marquee、QRCode、PatternLock、Search

2. 容器类组件

  • 布局类:Column、Row、Flex、Grid、List、Stack、RelativeContainer

  • 滚动类:Scroll、Swiper、WaterFlow

  • 分割类:ColumnSplit、RowSplit

  • 特殊容器:Badge、Counter、Tabs、TabContent、SideBarContainer

3. 自绘制类组件

  • XComponent:支持Native绘制

  • Canvas:画布组件

  • Video:视频播放

  • Web:Web组件(仅支持一层嵌套)

4. 命令式自定义绘制节点

  • BuilderNode、ComponentContent、FrameNode、NodeController

  • RenderNode、XComponentNode、AttributeUpdater等

5. 不支持的通用属性

  • 分布式迁移标识

  • 特效绘制合并

注意:其他未明确标注不支持的属性与事件均默认支持

五、Web网页同层渲染规格

1. 支持的H5标签

标签类型 使用规则
<embed> type类型为"native/"前缀才识别为同层组件
<object> 非标准MIME type,支持通过param/value自定义属性

不支持:W3C规范标准标签(如<input><video>)定义为同层标签

2. 支持的CSS属性

完整支持

  • 布局类:display、position、z-index、visibility、opacity

  • 背景类:background-color、background-image

  • 尺寸类:width、height

  • 内边距:padding及四个方向

  • 外边距:margin及四个方向

  • 边框类:border系列(width/style/color/radius)

  • 动画类:transition

  • 变换:transform(仅支持translate/scale,scale参数≥0)

不支持:rotate、skew等变换属性

3. 生命周期管理

通过onNativeEmbedLifecycleChange()回调监听:

  • 创建(CREATE)

  • 销毁(DESTROY)

  • 位置宽高变化(UPDATE)

  • Web页面前进后退缓存支持

4. 事件处理

触摸事件

  • 支持:TouchEvent的DOWN/UP/MOVE/CANCEL

  • 可通过setGestureEventResult()配置消费结果

鼠标事件

  • 通过onNativeEmbedMouseEvent()回调

  • 支持鼠标左键、中键、右键点击/长按

  • 触摸板对应操作转换

暂不支持

  • 鼠标、键盘、触摸板事件直接上报

  • 支持将鼠标/触摸板左键事件转换为触摸事件上报

5. 可见状态变化

  • 通过onNativeEmbedVisibilityChange()回调

  • 支持同层标签相对于视口的可见状态上报

6. 约束限制

限制项 说明
同层标签数量 ≤5个,超过性能下降
最大高度 ≤8000px
最大纹理大小 ≤8000px
渲染模式 不支持同步渲染模式
Web嵌套 仅支持一层同层渲染嵌套
页面缩放 不支持缩放接口

六、开发实现

1. 前端HTML代码

方式一:使用<embed>标签

<!DOCTYPE html>
<html>
<head>
    <title>同层渲染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>

方式二:使用<object>标签(需注册)

<object id="input1" type="test/input" 
        style="width: 100%; height: 100px; margin: 30px; margin-top: 600px">
</object>

2. 应用侧配置

开启同层渲染权限

// module.json5
"requestPermissions": [
    {
        "name": "ohos.permission.INTERNET"
    }
]

开启同层渲染开关

// 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 })
                .enableNativeEmbedMode(true)  // 开启同层渲染
                .registerNativeEmbedRule("object", "test") // 注册object标签规则
        }
    }
}

3. 创建自定义组件

@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)
        }
        .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)
}

4. 创建NodeController

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;
    }
    
    // 必须重写的方法
    makeNode(uiContext: UIContext): FrameNode | null {
        if (this.isDestroy_) {
            return null;
        }
        
        if (!this.rootNode) {
            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 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;
    }
}

5. 生命周期监听与事件处理

build() {
    Row() {
        Column() {
            Stack() {
                // 动态创建NodeContainer
                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)
                
                Web({ src: $rawfile("text.html"), controller: this.browserTabController })
                    .enableNativeEmbedMode(true)
                    .registerNativeEmbedRule("object", "test")
                    
                    // 生命周期监听
                    .onNativeEmbedLifecycleChange((embed) => {
                        console.info("NativeEmbed surfaceId" + embed.surfaceId);
                        const componentId = embed.info?.id?.toString() as string;
                        
                        if (embed.status == NativeEmbedStatus.CREATE) {
                            // 创建节点控制器
                            let nodeController = new MyNodeController();
                            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.nodeControllerMap.set(componentId, nodeController);
                            this.componentIdArr.push(componentId);
                            
                        } else if (embed.status == NativeEmbedStatus.UPDATE) {
                            // 更新节点
                            let nodeController = this.nodeControllerMap.get(componentId);
                            nodeController?.updateNode({
                                textOne: 'update', 
                                width: this.uiContext.px2vp(embed.info?.width), 
                                height: this.uiContext.px2vp(embed.info?.height)
                            } as ESObject);
                            
                        } else if (embed.status == NativeEmbedStatus.DESTROY) {
                            // 销毁节点
                            let nodeController = this.nodeControllerMap.get(componentId);
                            nodeController?.setDestroy(true);
                            this.nodeControllerMap.delete(componentId);
                            this.componentIdArr = this.componentIdArr.filter(
                                (value: string) => value !== componentId
                            );
                        }
                    })
                    
                    // 触摸事件监听
                    .onNativeEmbedGestureEvent((touch) => {
                        this.componentIdArr.forEach((componentId: string) => {
                            let nodeController = this.nodeControllerMap.get(componentId);
                            if (nodeController?.getEmbedId() == touch.embedId) {
                                let ret = nodeController?.postEvent(touch.touchEvent);
                                if (touch.result) {
                                    touch.result.setGestureEventResult(ret);
                                }
                            }
                        });
                    })
                    
                    // 鼠标事件监听
                    .onNativeEmbedMouseEvent((mouse) => {
                        this.componentIdArr.forEach((componentId: string) => {
                            let nodeController = this.nodeControllerMap.get(componentId);
                            if (nodeController?.getEmbedId() == mouse.embedId) {
                                let ret = nodeController?.postInputEvent(mouse.mouseEvent);
                                if (mouse.result) {
                                    mouse.result.setMouseEventResult(ret);
                                }
                            }
                        });
                    })
            }
        }
    }
}

七、控制同层标签层级

私有属性 arkwebnativestyle

该属性仅在开启同层渲染后的<embed><object>中生效:

display取值 说明
overlay 设置同层标签层级高于其他Web元素
overlay-infinity 设置同层标签层级高于其他Web元素和设置overlay的同层标签

前端代码示例

<!DOCTYPE html>
<html>
<body>
    <div id="test" style="position: absolute; z-index: 9999; ...">
        z-index: 9999
    </div>
    <!-- 最高层级 -->
    <embed id="input1" type="native/view1" 
           arkwebnativestyle="display:overlay-infinity"
           style="position: absolute; top: 60px; left: 50px; width: 300px; height: 100px">
    <!-- 次高层级 -->
    <embed id="input2" type="native/view2" 
           arkwebnativestyle="display:overlay"
           style="position: absolute; top: 150px; left: 40px; width: 300px; height: 100px">
</body>
</html>

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐