HarmonyOS7 自定义布局别只会写 Row/Column:Layout 和 customLayout 实战
文章目录前言ArkUI 内置布局的局限customLayout 的原理LayoutChild 与 LayoutItem实战流式标签布局完整的页面组件measure 和 layout 的性能注意事项扩展思路我的使用感受前言ArkUI 自带的Row、Column、Stack、Flex能搞定大部分布局需求。但你总会遇到它们无能为力的场景——比如一个流式标签布局标签大小不一自动换行Flex的wrap虽然能换行但控制力不够间距、对齐、动画都不好调。这时候就该customLayout出场了。今天用一个流式标签布局的例子把自定义布局的measure和layout流程讲透。ArkUI 内置布局的局限Flex组件的wrap模式能实现基本的换行但有几个限制子元素之间的间距控制不灵活没法实现行间距和列间距不同没办法根据容器宽度动态计算每行放多少个不支持子元素动画过渡比如标签添加/删除时的流动效果无法实现复杂的对齐方式比如最后一行左对齐 vs 两端对齐碰到这些需求customLayout就是正确答案。customLayout 的原理customLayout分两步measure测量告诉框架每个子元素需要多大的空间layout布局告诉框架每个子元素放在哪个位置你拿到所有子元素的LayoutItem遍历它们做测量然后根据测量结果计算位置最后把位置信息提交回去。LayoutChild 与 LayoutItemLayoutChild是customLayout里子元素的包装通过它来调用measure获取子元素的尺寸。LayoutItem是测量后的结果包含宽高等信息。在layout阶段你调用LayoutChild的layout方法传入 x、y 坐标把子元素放到指定位置。实战流式标签布局目标效果一堆标签自动换行排列标签大小自适应内容支持添加/删除时的动画过渡。先定义数据模型和标签数据ObservedclassTagItem{id:numbertext:stringcolor:stringconstructor(id:number,text:string,color:string){this.ididthis.texttextthis.colorcolor}}// 预置颜色constTAG_COLORS[#e74c3c,#3498db,#2ecc71,#f39c12,#9b59b6,#1abc9c]标签子组件Componentstruct TagView{ObjectLinktag:TagItemonRemove:()void(){}build(){Row({space:6}){Text(this.tag.text).fontSize(14).fontColor(Color.White)Text(×).fontSize(16).fontColor(rgba(255,255,255,0.8)).onClick((){this.onRemove()})}.padding({left:12,right:10,top:6,bottom:6}).backgroundColor(this.tag.color).borderRadius(20).shadow({radius:2,color:rgba(0,0,0,0.1),offsetX:0,offsetY:1})}}现在写核心的自定义布局组件Componentstruct TagFlowLayout{Propitems:TagItem[][]// 行间距ProprowSpacing:number10// 列间距PropcolumnSpacing:number8// 水平内边距ProphorizontalPadding:number16onItemRemove:(index:number)void(){}build(){customLayout(this.items.map((item,index)({tag:item,index:index}))){TagView({tag:$item.tag,onRemove:(){this.onItemRemove(item.index)}})}.measure((children:LayoutChild[],constraint:BoxConstraints){constmaxWidthconstraint.maxWidth-this.horizontalPadding*2letcurrentX0letcurrentY0letlineHeight0lettotalHeight0for(leti0;ichildren.length;i){constchildchildren[i]// 测量子元素给它足够的空间constchildSizechild.measure({minWidth:0,maxWidth:maxWidth,minHeight:0,maxHeight:constraint.maxHeight})// 检查当前行还放不放得下if(currentX0currentXchildSize.widthmaxWidth){// 换行currentX0currentYlineHeightthis.rowSpacing lineHeight0}// 更新行高lineHeightMath.max(lineHeight,childSize.height)// 累加 X 坐标currentXchildSize.widththis.columnSpacing}// 总高度 最后一行的 Y 行高totalHeightcurrentYlineHeight// 返回容器的期望尺寸return{width:constraint.maxWidth,height:totalHeight}}.layout((children:LayoutChild[],constraint:BoxConstraints){constmaxWidthconstraint.maxWidth-this.horizontalPadding*2letcurrentXthis.horizontalPaddingletcurrentY0letlineHeight0for(leti0;ichildren.length;i){constchildchildren[i]constchildSizechild.measure({minWidth:0,maxWidth:maxWidth,minHeight:0,maxHeight:constraint.maxHeight})// 换行判断constitemRightcurrentX-this.horizontalPaddingchildSize.widthif(currentXthis.horizontalPaddingitemRightmaxWidth){currentXthis.horizontalPadding currentYlineHeightthis.rowSpacing lineHeight0}// 放置子元素child.layout(currentX,currentY)// 更新行高lineHeightMath.max(lineHeight,childSize.height)// 移到下一个位置currentXchildSize.widththis.columnSpacing}}).width(100%).clip(true)}}measure和layout的核心逻辑是类似的遍历子元素逐个测量累计 X 坐标放不下就换行。区别在于measure返回容器尺寸layout执行实际的定位。完整的页面组件EntryComponentstruct TagFlowPage{Statetags:TagItem[][newTagItem(1,HarmonyOS,TAG_COLORS[0]),newTagItem(2,ArkTS,TAG_COLORS[1]),newTagItem(3,ArkUI,TAG_COLORS[2]),newTagItem(4,DevEco Studio,TAG_COLORS[3]),newTagItem(5,分布式,TAG_COLORS[4]),newTagItem(6,原子化服务,TAG_COLORS[5]),newTagItem(7,ArkCompiler,TAG_COLORS[0]),newTagItem(8,Stage 模型,TAG_COLORS[1]),newTagItem(9,AbilityKit,TAG_COLORS[2]),newTagItem(10,HAP,TAG_COLORS[3]),]StateinputText:stringprivatenextId:number11build(){Column({space:20}){Text(流式标签布局).fontSize(22).fontWeight(FontWeight.Bold)// 添加标签的输入框Row({space:8}){TextInput({placeholder:输入标签名,text:this.inputText}).onChange((value:string){this.inputTextvalue}).layoutWeight(1)Button(添加).onClick((){if(this.inputText.trim()!){constcolorTAG_COLORS[this.nextId%TAG_COLORS.length]this.tags[...this.tags,newTagItem(this.nextId,this.inputText.trim(),color)]this.inputText}})}.width(100%)// 标签数量Text(共${this.tags.length}个标签).fontSize(13).fontColor(#999)// 流式标签区域TagFlowLayout({items:this.tags,onItemRemove:(index:number){constnewTags[...this.tags]newTags.splice(index,1)animateTo({duration:300,curve:Curve.EaseOut},(){this.tagsnewTags})}})// 一键清空if(this.tags.length0){Button(清空全部).type(ButtonType.Normal).fontColor(#e74c3c).backgroundColor(transparent).onClick((){animateTo({duration:300,curve:Curve.EaseOut},(){this.tags[]})})}}.padding(20).width(100%)}}删除标签时套了一层animateTo标签消失的时候会有个平滑过渡效果。customLayout支持animateTo驱动的布局变化动画子元素位置变了框架会自动做插值过渡。measure 和 layout 的性能注意事项customLayout里measure和layout可能被频繁调用要注意两点避免在 measure/layout 里做重计算。这两个回调可能在每帧都执行比如有动画的时候在里面跑复杂逻辑会掉帧。能缓存的提前缓存能简化的尽量简化。measure 里可以多次调用子元素的 measure。框架允许你在measure和layout里多次调用child.measure()返回的结果是一致的。但别在循环里反复 measure 同一个 child 十几次——虽然不会报错但浪费时间。扩展思路流式标签只是customLayout的入门用法。掌握了measurelayout这套机制你还能做瀑布流布局多列排列每列高度不同新元素放到最短的那列环形布局子元素沿着圆形排列用三角函数算坐标网格布局固定列数自动计算行数支持跨行跨列这些布局的核心流程都一样measure 阶段拿尺寸layout 阶段算位置放下去。区别只在坐标计算逻辑上。我的使用感受customLayout上手门槛不算高但要理解measure和layout两阶段分离的设计思想。很多人一上来就在layout里不调measure直接拿尺寸结果发现子元素尺寸全是 0——因为measure是拿尺寸的唯一途径。日常开发中简单的布局需求Row、Column、Flex足够了。customLayout留给那些真正需要精细控制位置的复杂场景。别为了秀技术把简单布局也搞成customLayout维护起来成本更高。最后一个建议写完customLayout一定要在不同屏幕宽度下测一测。折叠屏展开、横竖屏切换容器宽度一变布局逻辑就可能出 bug。用 DevEco 的预览器多跑几种配置比上线后发现问题靠谱多了。