
本节目标理解 ArkUI 布局引擎“测量—布局—绘制”三阶段管线掌握自定义布局的触发机制掌握onMeasureSize与onPlaceChildren两个核心回调的用法与配合关系掌握Measurable与Layoutable接口的能力能够对子组件进行精确的尺寸测量与位置摆放掌握自定义布局的性能优化策略行级缓存、可视区剪裁、固定尺寸声明掌握Canvas自定义绘制的基本用法理解CanvasRenderingContext2D的生命周期与绘制时机掌握XComponent的 SURFACE 与 TEXTURE 两种渲染模式的区别与适用场景能够根据需求选择正确的自定义布局或绘制方案构建高性能的自定义 UI 组件一、ArkUI 布局引擎与自定义布局概述1.1 布局引擎的三阶段管线ArkUI 的布局引擎在渲染前会经历测量Measure—布局Layout—绘制Draw三轮管线。每引入一层 Column 或 Row子节点就需要重新参与约束传递和尺寸协商。框架的默认布局组件Row/Column/Stack/Flex/Grid已经覆盖了绝大多数排列需求但当内置布局无法满足特定排列逻辑时就需要接管布局控制权。1.2 自定义布局的定位自定义组件的自定义布局允许开发者通过onMeasureSize和onPlaceChildren接口以数据计算的方式精确控制子组件的位置和尺寸实现更灵活的布局效果。适用于需要实现复杂非标准布局、内置布局组件无法满足特定排列需求、需要根据动态数据计算子组件位置和尺寸等场景。关键规则在自定义组件内实现onMeasureSize、onPlaceChildren任一方法即视为实现自定义布局。为实现完整的自定义布局效果推荐同时实现两种方法onMeasureSize确定组件尺寸后onPlaceChildren才能根据该尺寸正确布局子组件。从 API version 20 开始自定义布局中若子组件设置了LayoutPolicy的fixAtIdealSize属性表示尺寸将不受父组件约束完全按照开发者自定义的尺寸范围布局。重要约束自定义布局内不支持使用懒加载包含Repeat和LazyForEach。如果需要配合长列表使用应采用LazyDynamicLayout等专用懒加载布局方案。二、onMeasureSize 与 onPlaceChildren2.1 onMeasureSize 测量接口onMeasureSize在 ArkUI 框架确定自定义组件尺寸时被调用将自定义组件的节点信息和尺寸范围传递给开发者。返回值为SizeResult表示自定义组件的最终尺寸。onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult参数说明selfLayoutInfo是父组件自定义组件的布局信息第一次布局时以自身设置的属性为准。children是子组件的测量信息数组每个Measurable对象提供measure()方法用于测量子组件尺寸。constraint是父组件传递的尺寸约束信息。关键规则不允许在onMeasureSize函数中改变状态变量。父容器上设置的尺寸信息除aspectRatio之外优先级小于onMeasureSize设置的尺寸信息。2.2 onPlaceChildren 布局接口onPlaceChildren在 ArkUI 框架布局自定义组件时被调用将子节点自身的尺寸范围传递给该自定义组件。返回值为 void通过调用子组件的layout()方法完成位置摆放。onPlaceChildren(selfLayoutInfo:GeometryInfo,children:ArrayLayoutable,constraint:ConstraintSizeOptions):voidchildren数组中的每个Layoutable对象提供layout()方法用于设置子组件的位置和尺寸。不允许在onPlaceChildren函数中改变状态变量。2.3 完整自定义布局示例以下实现一个简单的“对角线排列”自定义布局classDiagonalLayoutextendsArrayLayoutable{}Componentstruct DiagonalLayoutDemo{build(){Column(){DiagonalLayoutContainer({builder:this.childrenBuilder})}.width(100%).height(100%)}BuilderchildrenBuilder(){ForEach([1,2,3,4],(index:number){Text(Item${index}).fontSize(20).width(80).height(40).backgroundColor(#007DFF).fontColor(Color.White).textAlign(TextAlign.Center).borderRadius(8)})}}Componentstruct DiagonalLayoutContainer{BuilderParambuilder:()void;onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult{// 每个子组件固定 80x40依次沿对角线排列constitemWidth80;constitemHeight40;consttotalWidthchildren.length*itemWidth;consttotalHeightchildren.length*itemHeight;children.forEach((child:Measurable){child.measure({maxWidth:itemWidth,maxHeight:itemHeight,minWidth:itemWidth,minHeight:itemHeight});});return{width:Math.min(totalWidth,constraint.maxWidthasnumber),height:Math.min(totalHeight,constraint.maxHeightasnumber)};}onPlaceChildren(selfLayoutInfo:GeometryInfo,children:ArrayLayoutable,constraint:ConstraintSizeOptions):void{constitemWidth80;constitemHeight40;children.forEach((child:Layoutable,index:number){child.layout({x:index*itemWidth,y:index*itemHeight,width:itemWidth,height:itemHeight});});}build(){Column(){this.builder()}}}这个示例中onMeasureSize负责测量每个子组件并计算容器的总尺寸onPlaceChildren负责将每个子组件摆放到对角线的对应位置。三、自定义布局的性能优化3.1 自定义布局的性能陷阱自定义布局最常见的性能问题是“每次布局遍历全部子节点”。假设有一个包含 2000 个商品卡片的网格布局每次滚动时如果都重新测量全部 2000 个卡片即使屏幕上只显示 10 个也会造成严重的性能问题。核心解法分三步第一步用行级缓存隔离测量范围。在onMeasureSize中维护一个Mapnumber, RowLayoutkey 是起始索引value 存储该行的测量结果高度、子节点边界。滚动偏移变化时只计算新进入可视区的那几行已测量过的行直接从缓存取。第二步用LazyForEachReusableV2驱动节点复用。自定义布局只管“位置摆放”节点创建交给LazyForEach。配合ReusableV2滚动时组件实例会被回收复用不会反复创建销毁。LazyForEach的 key 必须用数据项的唯一 ID不能用 index。第三步在onPlaceChildren中只排布可视区的行。根据当前滚动偏移算出可视区的起止行号只对这几行调用child.layout()可视区外的节点不参与布局计算。3.2 可视区剪裁与缓存实战ComponentV2struct VirtualGrid{LocalscrollOffset:number0;privaterowCache:Mapnumber,RowLayoutnewMap();privateitemHeight:number200;privatecolumns:number2;onMeasureSize(self:LayoutSelf,children:LayoutChildren,constraint:LayoutConstraint){// 只测量可视区 缓冲区的新行constvisibleStartRowMath.floor(this.scrollOffset/this.itemHeight);constvisibleEndRowvisibleStartRowMath.ceil(constraint.maxHeight/this.itemHeight)2;for(letrowvisibleStartRow;rowvisibleEndRow;row){if(!this.rowCache.has(row)){this.measureRow(row,children,constraint);}}return{width:constraint.maxWidth,height:this.totalHeight};}onPlaceChildren(self:LayoutSelf,children:LayoutChildren,constraint:LayoutConstraint){constvisibleStartRowMath.floor(this.scrollOffset/this.itemHeight);constvisibleEndRowvisibleStartRowMath.ceil(constraint.maxHeight/this.itemHeight)2;for(letrowvisibleStartRow;rowvisibleEndRow;row){constlayoutthis.rowCache.get(row);if(layout){layout.children.forEach((child,col){child.layout({x:col*this.itemWidth,y:row*this.itemHeight});});}}}}核心原则测量结果缓存化、布局范围可视区化、节点创建复用化。3.3 固定尺寸声明如果子组件尺寸是固定的应在子组件上显式声明width和height避免onMeasureSize中的重复测量。如果尺寸在运行时才确定应在onMeasureSize中将测量结果缓存起来避免重复调用child.measure()。四、Canvas 自定义绘制4.1 Canvas 组件的基本用法Canvas组件用于自定义绘制最大允许尺寸不超过 10000px × 10000px超过最大面积则无法正常创建。CanvasRenderingContext2D是 Canvas 组件的 2D 绘图上下文对象用于在 Canvas 组件上进行自定义绘图。建议将CanvasRenderingContext2D对象与 Canvas 组件封装到同一个自定义组件中保证两者一一对应且生命周期保持一致。Componentstruct CustomChart{privatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);build(){Canvas(this.context).width(100%).height(300).backgroundColor(#F5F7FA).onReady((){this.drawChart();})}privatedrawChart():void{constctxthis.context;// 绘制坐标轴ctx.beginPath();ctx.moveTo(40,10);ctx.lineTo(40,260);ctx.lineTo(360,260);ctx.strokeStyle#CCCCCC;ctx.stroke();// 绘制折线constdata[80,140,100,200,160,220,180];ctx.beginPath();data.forEach((value:number,index:number){constx40index*(320/(data.length-1));consty260-value;if(index0){ctx.moveTo(x,y);}else{ctx.lineTo(x,y);}});ctx.strokeStyle#007DFF;ctx.lineWidth2;ctx.stroke();}}4.2 绘制时机与生命周期Canvas 的onReady回调触发后画布宽高才确定且可获取才能使用 Canvas 相关 API 进行绘制。绘制接口在调用时会存入被关联的 Canvas 组件的指令队列中仅当当前帧进入渲染阶段且关联的 Canvas 组件处于可见状态时这些指令才会从队列中被提取并执行。不可见场景Canvas 不响应绘制命令页面进入后台、组件滑到窗口外、visibility属性设置为隐藏等。4.3 重绘策略当数据变化需要重绘时应先调用context.clearRect()清除画布再重新绘制。避免在onReady之外频繁调用绘制方法应通过状态变量驱动条件判断后重绘。4.4 Canvas 的适用场景Canvas 适合以下场景金融软件的股票行情图、基金折线图自定义图表柱状图、饼图、雷达图手写板、签名板图片裁剪与滤镜处理游戏画面绘制。五、XComponent 原生渲染5.1 XComponent 的定位XComponent组件作为一种渲染组件可用于 EGL/OpenGLES 和媒体数据写入通过使用 XComponent 持有的“NativeWindow”来渲染画面通常用于满足开发者较为复杂的自定义渲染需求例如相机预览流的显示和游戏画面的渲染。5.2 SURFACE 与 TEXTURE 两种模式XComponent 通过type字段实现不同的渲染方式SURFACE 类型开发者将定制的绘制内容单独展示到屏幕上。独立 Surface不和组件树合成直接上屏会遮挡下方组件。系统合成层自动 Y 翻转修正显示正常。TEXTURE 类型将定制的绘制内容和 XComponent 组件的内容合成后展示到屏幕上。走 UI 纹理贴图无自动翻转OpenGL UV 与图像坐标系冲突可能导致显示倒置。选型建议游戏渲染、相机预览等需要独立高性能渲染的场景优先使用 SURFACE 类型。需要与 ArkUI 组件树混合渲染、需要圆角/裁剪/透明度等效果时使用 TEXTURE 类型。5.3 触摸事件回传XComponent 在 Native 侧通过OH_NativeXComponent拿到触摸事件后需要安全地回传到 ArkTS 并更新 UI 状态。推荐通过registerCallback注册事件回调在回调中使用napi_call_function调用 ArkTS 侧的 JS 函数注意处理好线程切换和对象生命周期。六、多元化习题习题 1判断题题目在 ArkUI 中自定义布局的onMeasureSize和onPlaceChildren两个回调中可以修改状态变量来驱动 UI 刷新。答案错误解读onMeasureSize和onPlaceChildren中均不允许改变状态变量。这两个回调是布局计算阶段的纯函数修改状态变量会导致布局循环或未定义行为。习题 2单选题题目以下关于自定义布局接口的说法正确的是 A. 只实现onMeasureSize即可完成完整的自定义布局B. 只实现onPlaceChildren即可完成完整的自定义布局C. 推荐同时实现onMeasureSize和onPlaceChildren两个方法D.onMeasureSize在onPlaceChildren之后执行答案C解读在自定义组件内实现任一方法即视为实现自定义布局但为实现完整的自定义布局效果推荐同时实现两种方法。onMeasureSize确定组件尺寸后onPlaceChildren才能根据该尺寸正确布局子组件。执行顺序是先onMeasureSize后onPlaceChildren。习题 3多选题题目关于自定义布局的性能优化以下说法正确的有多选A. 使用行级缓存隔离测量范围只测量可视区的新行B. 使用LazyForEachReusableV2驱动节点复用C. 在onPlaceChildren中只排布可视区的行D. 自定义布局中可以直接使用LazyForEach实现懒加载答案A、B、C解读行级缓存隔离测量范围、LazyForEachReusableV2驱动节点复用、只排布可视区的行都是自定义布局性能优化的核心策略。但自定义布局内不支持使用懒加载包含Repeat和LazyForEach选项 D 错误。节点创建应交给外层的LazyForEach自定义布局只负责位置摆放。习题 4代码填空题题目请补全以下自定义布局代码在onMeasureSize中测量每个子组件并返回容器尺寸。onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult{constitemWidth100;constitemHeight50;children.forEach((child:______________){child.______________({maxWidth:itemWidth,maxHeight:itemHeight,minWidth:itemWidth,minHeight:itemHeight});});return{width:children.length*itemWidth,height:itemHeight};}答案Measurable、measure解读onMeasureSize的children参数类型为ArrayMeasurable每个Measurable对象提供measure()方法用于测量子组件尺寸。传入的constraint对象包含maxWidth、maxHeight、minWidth、minHeight等约束信息。习题 5代码改错题题目以下 Canvas 绘制代码存在空白问题请指出问题并修正。Componentstruct MyChart{privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D();aboutToAppear():void{this.drawChart();// 在 aboutToAppear 中直接绘制}build(){Canvas(this.context).width(100%).height(300)}privatedrawChart():void{this.context.fillStyle#007DFF;this.context.fillRect(10,10,100,100);}}答案在aboutToAppear中调用绘制方法时Canvas 组件的宽高还未确定CanvasRenderingContext2D的绘图表面也未初始化导致绘制内容无法显示。修正方案是将绘制逻辑放在onReady回调中build(){Canvas(this.context).width(100%).height(300).onReady((){this.drawChart();// onReady 回调触发后画布宽高才确定})}解读Canvas 的onReady回调触发后画布宽高才确定且可获取才能使用 Canvas 相关 API 进行绘制。在aboutToAppear中调用绘制时Canvas 组件尚未完成初始化。习题 6简答题题目简述 XComponent 的 SURFACE 和 TEXTURE 两种渲染模式的区别及适用场景。答案SURFACE 类型将定制的绘制内容单独展示到屏幕上是独立的 Surface不和组件树合成直接上屏。系统合成层自动 Y 翻转修正显示正常。适用于游戏渲染、相机预览等需要独立高性能渲染的场景但会遮挡下方组件。TEXTURE 类型将定制的绘制内容和 XComponent 组件的内容合成后展示到屏幕上走 UI 纹理贴图无自动翻转OpenGL UV 与图像坐标系冲突可能导致显示倒置。适用于需要与 ArkUI 组件树混合渲染、需要圆角/裁剪/透明度等效果的场景。选型时需要独立高性能渲染用 SURFACE需要与 ArkUI 混合渲染用 TEXTURE。解读SURFACE 和 TEXTURE 的核心区别在于是否与 ArkUI 组件树合成。SURFACE 是“独立上屏”TEXTURE 是“合成上屏”。选择时需要综合考虑渲染性能、混合渲染需求、显示方向等因素。习题 7简答题题目简述自定义布局中行级缓存优化的核心思路以及为什么自定义布局内不支持使用LazyForEach。答案行级缓存优化的核心思路是在onMeasureSize中维护一个Mapnumber, RowLayoutkey 是起始索引value 存储该行的测量结果高度、子节点边界。滚动偏移变化时只计算新进入可视区的那几行已测量过的行直接从缓存取。在onPlaceChildren中根据当前滚动偏移算出可视区的起止行号只对这几行调用child.layout()可视区外的节点不参与布局计算。自定义布局内不支持使用LazyForEach是因为自定义布局接管了子组件的测量与布局逻辑而LazyForEach的按需创建机制需要由框架的布局引擎驱动。如果自定义布局内使用LazyForEach框架无法在自定义布局的测量/布局回调中正确感知子组件的创建时机。解决方案是将LazyForEach放在自定义布局的外层由外层驱动节点创建自定义布局只负责位置摆放。解读自定义布局与懒加载的职责边界需要明确LazyForEach负责“创建哪些节点”自定义布局负责“把已创建的节点摆在哪里”。两者协作而非嵌套才能实现最佳性能。七、本节知识点总结布局引擎三阶段ArkUI 布局引擎经历测量—布局—绘制三轮管线。自定义布局通过onMeasureSize和onPlaceChildren两个回调接管布局控制权。onMeasureSize在组件确定尺寸时调用接收自身布局信息、子组件测量信息数组和父组件约束。通过child.measure()测量子组件返回SizeResult设定自身尺寸。不允许改变状态变量。onPlaceChildren在组件布局时调用接收子组件布局信息数组。通过child.layout()设置子组件位置和尺寸。不允许改变状态变量。性能优化三原则测量结果缓存化行级缓存隔离测量范围、布局范围可视区化只排布可视区的行、节点创建复用化LazyForEachReusableV2驱动节点复用。Canvas 自定义绘制CanvasRenderingContext2D与 Canvas 组件一一对应绘制逻辑放在onReady回调中。不可见场景下 Canvas 不响应绘制命令。适用于图表、手写板等自绘场景。XComponent 原生渲染SURFACE 类型独立上屏适用于游戏渲染、相机预览TEXTURE 类型合成上屏适用于与 ArkUI 混合渲染。通过 NativeWindow 对接 EGL/OpenGLES。下节预告第3课将进入自定义组件与节点操作的学习涵盖FrameNode自定义节点、RenderNode渲染节点、组件动态创建与预创建以及节点树操作与性能优化。