鸿蒙Next如何自定义标签页

发布于:2025-03-01 ⋅ 阅读:(12) ⋅ 点赞:(0)

前言

项目需求是展示标签,标签的个数不定,一行展示不行就自行换行。但是,使用鸿蒙原生的 Grid 后发现特别的难看。然后就想着自定义控件。找了官方文档,发现2个重要的实现方法,但是,官方的demo中讲的很少,需要自己去看去思考。

效果图如下:
在这里插入图片描述
注意点:

  1. 需要计算整体布局的宽高,这和 Android 差不多。
  2. 注意 margin 的计算

具体的代码如下:

/**
 * 自定义标签页面
 */
@Component struct CustomTagView {
  screenWidth: number = 0

  aboutToAppear(): void {
    let dis = display.getDefaultDisplaySync();
    let width = dis.width
    let height = dis.height

    // width 是单位是 px, 转换为 vp ,因为 onMeasureSize 和 onPlaceChildren 得到的 width margin 以及 padding 都是 vp
    let w = px2vp(width)
    let h = px2vp(height)

    this.screenWidth = w
    console.log("TagView aboutToAppear width = " + width + " , height = " + height + ", w = " + w + ", h = " + h)
  }

  @Builder childBuilder() {}

  @BuilderParam buildTagView: () => void = this.childBuilder

  result: SizeResult = {
    width: 0,
    height: 0
  }

  // 第一步:计算各子组件的实际大小以及设置布局本身的大小
  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>, constraint: ConstraintSizeOptions) {
    let parentWidth = selfLayoutInfo.width
    let parentHeight = selfLayoutInfo.height

    console.log("TagView onMeasureSize parentWidth = " + parentWidth + " , parentHeight = " + parentHeight)
    let startPosX = 0
    let columNumber = 1
    let childHeight = 0
    children.forEach((child) => {

      // 得到子控件的实际大小
      let result: MeasureResult = child.measure({
        minHeight: constraint.minHeight,
        minWidth: constraint.minWidth,
        maxWidth: constraint.maxWidth,
        maxHeight: constraint.maxHeight
      })

      let padding = child.getPadding()
      let border = child.getBorderWidth()
      console.log("TagView onMeasureSize = child width = " + result.width + "  ,  height = " + result.height
        + " , padding = [" + padding.start + ", " + padding.end + ", " + padding.top + ", " + padding.bottom +
        "], border = ["
        + border.start + ", " + border.end + ", " + border.top + ", " + border.bottom + "]")


      /// 计算 布局所需的高度, 宽度默认为屏幕的宽度
      childHeight = result.height + child.getMargin().top
      startPosX += result.width + child.getMargin().start
      if (startPosX > parentWidth) {
        columNumber++
        startPosX = result.width + child.getMargin().start
      }
    })
    // 父布局的宽和高,即承载 child 布局的宽和高,这里指的就是 TagView 的宽和高
    this.result.width = this.screenWidth;
    this.result.height = childHeight * columNumber + 10 // 加10是为了底部多点空间
    return this.result;
  }

  // 第二步:放置各子组件的位置
  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>, constraint: ConstraintSizeOptions) {
    let startPosX = 0
    let startPosY = 0
    let posY = 0
    let parentWidth = selfLayoutInfo.width

    console.log("TagView onPlaceChildren parentWidth = " + selfLayoutInfo.width + " , parentHeight = " + selfLayoutInfo.height)

    children.forEach((child) => {
      startPosX += child.getMargin().start
      // 如果一行的控件的长度大于屏幕宽度则换行
      if (startPosX + child.measureResult.width > parentWidth) {
        startPosY += child.measureResult.height + child.getMargin().top
        startPosX = child.getMargin().start
      }
      posY = startPosY
      console.log("TagView child width = " + child.measureResult.width + "  ,  height = " + child.measureResult.height +
        " , margin left = " + child.getMargin().start)
      child.layout({ x: startPosX, y: posY })
      startPosX += child.measureResult.width
    })
  }

  build() {
    this.buildTagView()
  }
}
调用的方法也很简单,下面是个调用的demo
@Entry
@Component
struct TestPage {
  @State name: string = 'hello'
  // @Provide 参数 key 必须是 string
  @Provide('provideName') pName: string = "哈哈"
  @Provide('count') count: number = 4

  textWidth: number = 0

  aboutToAppear(): void {

    let width = MeasureText.measureText({ textContent: '返厂无忧券1', fontSize: '13vp' });
    let w = px2vp(width)
    console.log("TestPage aboutToAppear >>>> width = " + width + " , w = " + w)

    // 如果有换行,那么长度等于最长的一行
    let textSize = MeasureText.measureTextSize({ textContent: '返厂无忧券1\n返厂无忧券1234', fontSize: '13vp' })
    let w2 = textSize.width
    let h2 = textSize.height
    this.textWidth = px2vp(w2 as number)
    console.log("TestPage aboutToAppear >>>> w2 = " + w2 + " , h2 = " + h2)
  }


  // @Build 参数按值传递,状态变量改变不会引起 @Build 方法内的 UI 刷新
  // 但是,Text(" ----- " + this.name) 中的 UI 会刷新
  @Builder
  nameView(name: string) {
    Text(name)
  }

  //  @Build 参数按引用传递的话,状态变量(@State name) 改变,@Build 方法内的 UI 会刷新
  @Builder
  nameView2(tmp: Tmp) {
    Text('V2 : name = ' + tmp.params)
  }

  // 通过builder的方式传递多个组件,作为自定义组件的一级子组件(即不包含容器组件,如Column)
  @Builder
  TagViewChildren() {
    ForEach(['你好,哪吒2', '大圣归来啊', '我是名字超长的但是很厉害', 'hello world', '你真的好厉害啊哈哈', '没空看', '非凡',
      '再来一个很长的名字啊', 'OK'],
      (data: string, index: number) => { // 暂不支持lazyForEach的写法
        Text(data)
          .fontSize(13)
          .fontColor(Color.Red)
          .margin({ left: 3, top: 4 })// .width(100)
            // .height(100)
          .borderWidth(1)
          .border({ radius: 4 })
          .padding(5)
          .textAlign(TextAlign.Center)
        // .offset({ x: 10, y: 20 })
      })
  }


  build() {
    Column() {
      
      Column() {
        CustomTagView({ buildTagView: this.TagViewChildren })

      }.backgroundColor(Color.Pink)
      .margin({ top: 2 })

    }.width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Start)
  }
}

好了,具体的可以参考下 demo 啦,有疑问的可以一起交流。