再谈ConstraintLayout

ConstraintLayout 是我们Android开发中常见的以“锚点”为布局的容器,某位UI大湿曾经有句名言:我能用ConstraintLayout实现所有布局!的确,ConstraintLayout特有的layout_constraintxx定义,其实更加符合UI开发的直觉,描述约束本身的成本其实是很少的,同时后期可维护性非常强。因为后续想要添加其他子内容的时候,我们只需要处理好约束本身即可。因此,在AndroidStudio的默认工程中,新建一个xml默认都是以ConstraintLayout为根布局,这也看出来官方对其的重视程度。

不仅如此,ConstraintLayout还被Android团队运用在声明式UI框架Compose当中,成为最重要的组件之一

@Composable
fun ConstraintLayoutContent() {
  ConstraintLayout {
    // Create references for the composables to constrain
    val (button, text) = createRefs()
    Button(
      onClick = { /* Do something */ },
      // Assign reference "button" to the Button composable
      // and constrain it to the top of the ConstraintLayout
      modifier = Modifier.constrainAs(button) {
        top.linkTo(parent.top, margin = 16.dp)
      }
    ) {
      Text("Button")
    }
    // Assign reference "text" to the Text composable
    // and constrain it to the bottom of the Button composable
    Text("Text", Modifier.constrainAs(text) {
      top.linkTo(button.bottom, margin = 16.dp)
    })
  }
}

同时Android团队把ConstraintLayout作为基础,扩展到了更加复杂的MotionLayout中,作为简化开发动画的利器。

RelativeContainer

既然描述约束这么好用,那么Harmony OS中,有没有类似的组件呢?当然有,毕竟谁也不想让开发们遇上多嵌套的Row跟Column(太难受了)

RelativeContainer就是在鸿蒙开发者,承担着解决多重嵌套的布局利器。

默认情况下,RelativeContainer跟普通组件没有什么区别

RelativeContainer() {
  Text("text1")
    .fontSize(25)
    .width(200)
    .id("text1")  // 必须添加 id,否则不显示
    .textAlign(TextAlign.Center)
    .backgroundColor("#ccaabb")
}
.width('100%')
.height(200)
.backgroundColor("#111111")

子View text1会默认处于RelativeContainer的左上角

RelativeContainer (鸿蒙版本的ConstraintLayout)

我们可以通过alignRules 方法,声明需要的约束,下面我们来详细认识一下这些锚点

锚点

alignRules 接受一个AlignRuleOption 实现,里面提供了几个具体的锚点

declare interface AlignRuleOption {
  /**
   * The param of left align.
   * @form
   * @since 9
   */
  left?: { anchor: string, align: HorizontalAlign };
  /**
   * The param of right align.
   * @form
   * @since 9
   */
  right?: { anchor: string, align: HorizontalAlign };
  /**
   * The param of middle align.
   * @form
   * @since 9
   */
  middle?: { anchor: string, align: HorizontalAlign };
  /**
   * The param of top align.
   * @form
   * @since 9
   */
  top?: { anchor: string, align: VerticalAlign };
  /**
   * The param of bottom align.
   * @form
   * @since 9
   */
  bottom?: { anchor: string, align: VerticalAlign };
  /**
   * The param of center align.
   * @form
   * @since 9
   */
  center?: { anchor: string, align: VerticalAlign };
}

锚点 举例

我们拿right 举例子

Text("text1")
  .fontSize(25)
  .width(200)
  .id("text1")  // 必须添加 id,否则不显示
  .textAlign(TextAlign.Center)
  .backgroundColor("#ccaabb")
  .alignRules({
      right: {
        anchor: "__container__",
        align: HorizontalAlign.End
      },
    })

这里alignRules 可以填入锚点方向

right?: { anchor: string, align: HorizontalAlign };

right 代表着当前组件需要定位的方向,anchor代表着锚点,以anchor为参考系,align表示对齐方式

这里针对anchor有一个特殊值,”__container__” 代表着以父布局为参考系。

right: { anchor: “container“, align: HorizontalAlign.End }, 这个含义是指,当前子组件Text的right方向以父布局为基准,然后按照右侧对齐

RelativeContainer (鸿蒙版本的ConstraintLayout)

针对水平方向align为HorizontalAlign

  • HorizontalAlign.Start: 设置子组件右边框相对锚点组件的左边框位置对齐。
  • HorizontalAlign.Center: 设置子组件右边框相对锚点组件的中间点位置对齐。
  • HorizontalAlign.End: 设置子组件右边框相对锚点组件的右边框位置对齐。

当然,单个left跟right对齐其实跟大部分的声明式UI框架没有太多区别,这里我特别举出一个例子,比如我们想要实现一个组件居中对齐,习惯了ConstraintLayout,我们可能会直接对left跟right进行对齐,比如错误示例

 right: {
   anchor: "__container__",
   align: HorizontalAlign.End
 },
left: {
 anchor: "__container__",
 align: HorizontalAlign.Start
},

这样的话,其实只会把子布局width撑大到父布局,比如

RelativeContainer (鸿蒙版本的ConstraintLayout)

这里也说明了,当多个方向的锚点被设置,其实效果是满足锚点,同时会把子组件自身的width属性的数据丢失(本来设置的width为200)

想要实现水平居中,RelativeContainer 采取middle对齐的方式实现,

middle:{
  anchor: "__container__",
  align: HorizontalAlign.Center
},

RelativeContainer (鸿蒙版本的ConstraintLayout)

垂直居中的话,就要采取center对齐

center:{
    anchor: "__container__",
    align: VerticalAlign.Center
  },

RelativeContainer (鸿蒙版本的ConstraintLayout)

其他的几个,欢迎读者们自行尝试一下效果。

除了以父组件为参考系以外,我们还可以以其他子组件为参考系,比如我们放置两个子Text

RelativeContainer() {
  Text("text1")
    .fontSize(25)
    .id("text1")  // 必须添加 id,否则不显示
    .textAlign(TextAlign.Center)
    .backgroundColor("#ccaabb")
    .alignRules({
      left:{
        anchor:"__container__",
        align: HorizontalAlign.Start
      },
      right:{
        anchor:"__container__",
        align: HorizontalAlign.End
      },
    })
  Text("text2")
    .fontSize(25)
    .id("text2")  // 必须添加 id,否则不显示
    .textAlign(TextAlign.Center)
    .backgroundColor("#bbccaa")
    .alignRules({
      left:{
        anchor:"__container__",
        align: HorizontalAlign.Start
      },
      right:{
        anchor:"__container__",
        align: HorizontalAlign.End
      },
      // 以text1 为参考系,顶部对齐
      top:{
        anchor:"text1",
        align:VerticalAlign.Bottom
      }
    })
}
.width('100%')
.height(200)
.backgroundColor("#111111")

对应的效果图如下

RelativeContainer (鸿蒙版本的ConstraintLayout)

注意点

这里总结几个开发过程中会遇到的几个需要注意的点

width等自身属性会被锚点覆盖

这里我们上面例子也给出来了,当我们设置组件自身固有属性,比如width的时候,如果设置了相同方向的多个锚点,比如同时设置left 跟right,那么组件的宽度会以锚点的布局为最终宽度

子组件需要设置id

如果子组件没有设置id,那么组件会不被绘制展示,比如我们把text2的id拿掉后

没有id
Text("text2")
  .fontSize(25)
  .textAlign(TextAlign.Center)
  .backgroundColor("#bbccaa")
  .alignRules({
    left:{
      anchor:"__container__",
      align: HorizontalAlign.Start
    },
    right:{
      anchor:"__container__",
      align: HorizontalAlign.End
    },
    // 以text1 为参考系,顶部对齐
    top:{
      anchor:"text1",
      align:VerticalAlign.Bottom
    }
  })

效果如下,text2将不被绘制

RelativeContainer (鸿蒙版本的ConstraintLayout)

避免align 错误影响

align参数也会影响到布局展示,比如

  RelativeContainer() {
    Text("text1")
      .fontSize(25)
      .id("text1")
      .textAlign(TextAlign.Center)
      .backgroundColor("#ccaabb")
      .alignRules({
        left:{
          anchor:"__container__",
          align: HorizontalAlign.End
        },
      })
  }
  .width('100%')
  .height(200)
  .backgroundColor("#111111")
}
.width('100%')

如果left 对齐的位置是父view的end,那么其实就是没有展示空间从而不被展示

RelativeContainer (鸿蒙版本的ConstraintLayout)

总结

本章中我们学习到了(鸿蒙版本ConstraintLayout) RelativeContainer 的用法,同时官方的资料中其实对RelativeContainer没有太多补充,因此我在这里提一下,当然,RelativeContainer在每个api版本中某些行为还是不太一致的,比如环形依赖的问题,这里大家可以实际操作体验一下。