鸿蒙-自界说布局-实现一个可限定行数的-Flex

[复制链接]
发表于 2025-10-27 01:22:44 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
千呼万唤始出来的自界说布局功能终于可以用了,这就给了我们更多自由发挥创造的空间,不再范围于使用已有组件做组合。固然,用 NAPI 和 C|C++页可以实现本身绘制全部内容,更别提尚有雷同XComponent这种东西了。但如果我们只是必要简朴的本身控制子组件地点的位置,不必要接受绘制等逻辑,比如实现一个扇形菜单、实现一个可以控制行数的标签列表等,怎么搞嘞?如今鸿蒙提供了onPlaceChildren和onMeasureSize这两个回调方法,使得我们可以按照本身的意愿来摆放组件。
老样子,先放效果图

条件

我们先来相识一下这两个回调方法:
  1. onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions) {}
  2. onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {}
复制代码
onMeasureSize

   ArkUI框架会在自界说组件确定尺寸时,将该自界说组件的节点信息和尺寸范围通过onMeasureSize通报给该开辟者。不允许在onMeasureSize函数中改变状态变量。
  在build方法调用之后,就会调用onMeasureSize方法。在该方法中,我们可以获取到组件本身和子组件的巨细,通过盘算确认组件本身巨细后返回一个SizeResult对象,告知体系该组件终极巨细。
selfLayoutInfo

在该方法的的参数中,有一个GeometryInfo对象实例selfLayoutInfo.通过这个对象,我们可以拿到父组件的宽高、padding、margin、borderWidth等信息。
文档中对selfLayoutInfo的表明为父组件布局信息,这里的父组件是指的自界说组件本身,而不是包罗该自界说组件的组件。举个简朴的例子:
自界说组件名字为CustomLayout,有如下布局
  1. @Entry
  2. @Component
  3. struct LineLimitFlexPage {
  4.   build() {
  5.     Column() {
  6.       CustomLayout().width('90%').border({ width: 2, color: Color.Yellow, radius: 2 }).padding(2).margin(2)
  7.     }.width('90%').border({ width: 6, color: Color.Red, radius: 6 }).padding(6).margin(6)
  8.   }
  9. }
复制代码
我们在CustomLayout组建内重写onMeasureSize方法,将相干信息打印出来
  1. onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions):SizeResult {
  2.     hilog.error(0x01, "LineLimitFlexPage", `onMeasureSize selfLayoutInfo: ${JSON.stringify(selfLayoutInfo)}`)
  3. }
复制代码
日记信息是如许的
  1. onMeasureSize selfLayoutInfo: {"borderWidth":{"top":2,"right":2,"bottom":2,"left":2},"margin":{"top":2,"right":2,"bottom":2,"left":2},"padding":{"top":2,"right":2,"bottom":2,"left":2},"width":292.9846003605769,"height":731.3846153846154}
复制代码
可以看到,打印出来的信息是自界说组件本身的属性。
constraint

别的,尚有一个constraint的ConstraintSizeOptions对象,文档中对其表明是设置束缚尺寸,组件布局时,举行尺寸范围限定。,同样也打印一下,
  1. onMeasureSize constraint: {"minWidth":0,"minHeight":0,"maxWidth":285.59998497596155,"maxHeight":724}
复制代码
可以看到有四个属性:最小宽度,最小高度,最大宽度,最大高度。这也是我们组件巨细的下限和上限。
children

一个关键的参数:children: Array<Measurable>,子组件的布局信息。这里并没有直接把子组件通报下来,而是抽象成了Measurable对象。该对象有四个方法
  1. measure(constraint: ConstraintSizeOptions): MeasureResult;
  2. getMargin(): DirectionalEdgesT<number>;
  3. getPadding(): DirectionalEdgesT<number>;
  4. getBorderWidth(): DirectionalEdgesT<number>;
复制代码
见名知义,没有什么好说的,我们通过measure方法可以获取到子组件的巨细,之后通过盘算,综合子组件巨细、selfLayoutInfo、constraint三者的信息来盘算该组件必要的巨细。而且返回SizeResult对象,来告知体系该组件的终极巨细。
onPlaceChildren

在来看onPlaceChildren方法,在该方法中的selfLayoutInfo和constraint这两个参数,和onMeasureSize方法中的参数寄义是雷同的,这里不再赘述。
来看一下children: Array<Layoutable>参数。这里也是把子组件抽象成了Layoutable对象,它有一个measureResult: MeasureResult;属性和四个方法:
  1. layout(position: Position): void;
  2. getMargin(): DirectionalEdgesT<number>;
  3. getPadding(): DirectionalEdgesT<number>;
  4. getBorderWidth(): DirectionalEdgesT<number>;
复制代码
同样的见名知义,没有什么好说的。我们不必要相识子组件的具体信息,只必要关心子组件巨细和摆放的位置就好。这里我们通过layout方法来确认子组件摆放位置。
实现

前置的条件我们都已经相识了,那么怎样实现一个浅显版可指定展示行数的 Flex 也就有思绪了。这里为了简朴,我们只思量横向从左向右分列的情况,没有思量 padding 和 margin属性。其他的属性各人有爱好可以本身实现
思绪

在onMeasureSize方法中丈量并获取每个子组件的巨细,长度累加大于便是束缚的最大宽度则换行,高度累加。直到凌驾指定行数大概遍历完子组件竣事。返回组件巨细。
在onPlaceChildren方法中遍历子组件,通过子组件的宽高确认摆放位置,长度累加大于便是束缚的最大宽度则换行,高度累加,直到凌驾指定行数大概遍历完子组件竣事。
属性

这里我们只思量水平隔断和垂直隔断以及指定行数
  1. @Component
  2. struct CustomLayout {
  3.   hSpace: number = 0
  4.   vSpace: number = 0
  5.   @Prop maxLine: number
  6. }
复制代码
这里尚有一些必要特别注意的细节点:


  • 自界说布局暂不支持LazyForEach写法。
  • 使用builder情势的自界说布局创建,自界说组件的build()方法内只允许存在this.builder(),即示例的保举用法。
  • 父容器(自界说组件)上设置的尺寸信息,除aspectRatio之外,优先级小于onMeasureSize设置的尺寸信息。
  • 子组件设置的位置信息,offset、position、markAnchor优先级大于onPlaceChildren设置的位置信息,其他位置设置属性不见效。
  • 使用自界说布局方法时,必要同时调用onMeasureSize和onPlaceChildren方法,否则大概出现布局非常。
预备

既然如许,我们先预备好大抵框架
我们的自界说布局
  1. @Component
  2. struct CustomLayout {
  3.   hSpace: number = 0
  4.   vSpace: number = 0
  5.   @Prop maxLine: number
  6.   @Prop showAll: boolean
  7.   @Builder
  8.   doNothingBuilder() {};
  9.   @BuilderParam builder: () => void = this.doNothingBuilder;
  10.   onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions): SizeResult{}
  11.   onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {}
  12. }
复制代码
一个全局的@Builder修饰的布局,也就是我们的子控件
  1. const colors: string[] = ["#ff6134", "#1b91e0", "#39d167"]
  2. @Builder
  3. function ColumnChildren() {
  4.   ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], (index: number) => { //暂不支持lazyForEach的写法
  5.     Text('标签' + index)
  6.       .fontSize(30)
  7.       .borderWidth(2).backgroundColor(colors[index%3])
  8.   })
  9. }
复制代码
一个页面
  1. @Entry
  2. @Component
  3. struct LineLimitFlexPage {
  4.   build() {
  5.     Column() {
  6.       CustomLayout({
  7.         builder: ColumnChildren,
  8.         hSpace: vp2px(10),
  9.         vSpace: vp2px(6),
  10.         maxLine: 3
  11.       })
  12.     }}
  13. }
复制代码
如许我们就预备好了框架内容,接下来就是处置惩罚丈量组件巨细及布局了
丈量组件

我们按照上面的思绪在onMeasureSize方法中对子组件举行丈量,并确认组件巨细。
  1.   onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions): SizeResult {
  2.     hilog.error(0x01, "LineLimitFlexPage", `onMeasureSize selfLayoutInfo: ${JSON.stringify(selfLayoutInfo)}`)
  3.     hilog.error(0x01, "LineLimitFlexPage", `onMeasureSize constraint: ${JSON.stringify(constraint)}`)
  4.     let totalWidth = 0
  5.     let totalHeight = 0
  6.     let lineHeight = 0;
  7.     let firstLineHeight = 0
  8.     let lineCount = 1
  9.     for (let i = 0; i < children.length; i++) {
  10.       let child = children[i]
  11.       //测量当前控件的宽高
  12.       let result: MeasureResult = child.measure({
  13.         minHeight: 0,
  14.         minWidth: 0,
  15.         maxWidth: selfLayoutInfo.width,
  16.         maxHeight: selfLayoutInfo.height
  17.       })
  18.       //累计当前行宽度
  19.       totalWidth += result.width
  20.       //记录当前行的最大高度
  21.       lineHeight = Math.max(lineHeight, result.height)
  22.       if (totalWidth > selfLayoutInfo.width) {
  23.         //记录一下第一行高度
  24.         if (firstLineHeight == 0) {
  25.           firstLineHeight = lineHeight;
  26.         }
  27.         //如果加上当前控件超过了父控件宽度,则换行
  28.         lineCount++
  29.         if (lineCount > this.maxLine) {
  30.           break;
  31.         }
  32.         totalHeight += lineHeight + this.vSpace
  33.         totalWidth = result.width + this.vSpace
  34.         lineHeight = 0
  35.       } else {
  36.         //如果加上当前控件没有超过父控件宽度,加上水平间距
  37.         totalWidth += this.hSpace
  38.       }
  39.     }
  40.     let result: SizeResult = {
  41.       width: lineCount > 1 ? selfLayoutInfo.width : totalWidth,
  42.       height: totalHeight + firstLineHeight
  43.     };
  44.     return result
  45.   }
复制代码
布局

在onPlaceChildren方法中确认每个组件的位置
  1.   onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
  2.     hilog.error(0x01, "LineLimitFlexPage", `onPlaceChildren: selfLayoutInfo: ${JSON.stringify(selfLayoutInfo)}`)
  3.     hilog.error(0x01, "LineLimitFlexPage", `onPlaceChildren: constraint: ${JSON.stringify(constraint)}`)
  4.     let startX = 0;
  5.     let startY = 0;
  6.     let lineCount = 1
  7.     for (let i = 0; i < children.length; i++) {
  8.       let child = children[i]
  9.       let childWidth = child.measureResult.width;
  10.       let childHeight = child.measureResult.height
  11.       if (startX + childWidth > selfLayoutInfo.width) {
  12.         startX = 0
  13.         startY += childHeight + this.vSpace
  14.         lineCount++
  15.         if (lineCount > this.maxLine) {
  16.           break
  17.         }
  18.       }
  19.       child.layout({ x: startX, y: startY })
  20.       startX += childWidth + this.hSpace
  21.     }
  22.   }
复制代码
小结

如许我们就完成了一个浅显版的可以指定行数的类 Flex 组件。和 Android 中的自界说布局对比一下,流程险些是同等的,只不外方法署名不一样而已。对于初学者来讲照旧挺友好的。
革新

这里扩展一下,我们怎样革新自界说组件?
很天然的想到了父子组件通报参数并举行同步的修饰符:在父组件中使用@State修饰变量,在子组件中使用@Prop修饰变量,如许就能实现父子组件单向数据同步,父组件改变变量值时子组件同步革新。那我们也这么写一下:
在父组件中
  1. @Entry@Componentstruct LineLimitFlexPage {  @State maxLine: number = 2  build() {    Column() {      CustomLayout({        builder: ColumnChildren,        hSpace: vp2px(10),        vSpace: vp2px(6),        maxLine: this.maxLine      }).onClick((_) => {        if(this.maxLine == 2){          this.maxLine = Number.MAX_VALUE        }else{          this.maxLine =2        }        })    }  }}@Component
  2. struct CustomLayout {
  3.   hSpace: number = 0
  4.   vSpace: number = 0
  5.   @Prop maxLine: number
  6. }
复制代码
如许写完了,但是点击之后发现控件并没有革新,这是啥缘故原由? 咨询之后相识到由于子控件中的maxLine变量没有直接在子控件的build方法中使用,因此改变它的值不会触发build函数,更不会触发onMeasureSize和onPlaceChildren方法。
ArkUI 也没有提供雷同invalidate()方法也革新页面。咨询之后给了个比力魔幻的利用:额外界说一个变量,让这个变量加入build就好了,因此有了下面的代码:
  1. @Entry
  2. @Component
  3. struct LineLimitFlexPage {
  4.   @State maxLine: number = 2
  5.   @State showAll: boolean = false
  6.   build() {
  7.     Column() {
  8.       CustomLayout({
  9.         builder: ColumnChildren,
  10.         hSpace: vp2px(10),
  11.         vSpace: vp2px(6),
  12.         maxLine: this.maxLine,
  13.         showAll: this.showAll
  14.       }).onClick((_) => {
  15.         if (this.showAll) {
  16.           this.maxLine = 2
  17.         } else {
  18.           this.maxLine = Number.MAX_VALUE
  19.         }
  20.         this.showAll = !this.showAll
  21.       }
  22.       )}
  23.   }
  24. }
  25. @Component
  26. struct CustomLayout {
  27.   hSpace: number = 0
  28.   vSpace: number = 0
  29.   @Prop maxLine: number
  30.   @Prop showAll: boolean
  31.     build() {
  32.     if (this.showAll ) {
  33.       this.builder()
  34.     }else{
  35.       this.builder()
  36.     }
  37.   }
  38. }
复制代码
嗯,如许点击控件的时间就能革新了。。。。。
哈哈哈哈,我先笑一会。
嗯,大概把这个if判定放在父组件中
  1. Column() {
  2.    
  3.     if(this.showAll){
  4.             CustomLayout({})
  5.     }else{
  6.             CustomLayout({})
  7.     }
  8. }
复制代码
哈哈哈哈哈哈哈, 就先如许吧。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表