Skip to content

hushenghao/ArkTS-RelativeContainerExtend

Repository files navigation

让 HarmonyOS ArkTS 相对布局更好用

前段时间 Harmony Next 不再兼容 Android 的消息满天飞,各个大厂也开始对纯血 HarmonyOS 进行适配了,普通开发者也可以下载 DevEco Studio 进行尝鲜,相信大部分同学已经尝过鲜了,甚至有一部分已经着手开始了适配计划了。

现在 HarmonyOS 4.0 的一些 API 还不太成熟,第三方SDK也在开发中,想复刻成熟的原生应用可能为时尚早,不过我们用 ArkTS 来画页面和实现一些简单的业务逻辑还绰绰有余的 。

最近公司也开始了复刻 HarmonyOS 版本的尝鲜计划,Android iOS 客户端开发全员上阵。我们的 iOS 小伙伴说,除了 SwiftUI,鸿蒙这套 ArkTS 画页面也太快了吧,根本停不下来;不过对于Android 开发来说,特别是之前写过RN、Flutter 或者Compose的来说,写起来无外乎换一套API,成本相对是比较小的。

不过我想吐槽一下 Harmony 的开发文档写的是真不怎么样

开始

话不多说,直接开始。相信大家对 ArkTS 已经有一定了解,画布局已经不在话下。例如,用 RelativeContainer 画个 Hello World:

RelativeContainer() {
    Text('Hello World')
        .id("text")
        .alignRules({
            center: { anchor: "__container__", align: VerticalAlign.Center },
            middle: { anchor: "__container__", align: HorizontalAlign.Center },
        })
}
.width('100%')
.height('100%')

查看 RelativeContainer 的写法和文档,发现它就是 Android RelativeLayout 的 ArkTS 版本,与 RelativeLayout 的功能相同,同时又有一些不同:

  • 增强了居中对齐功能,支持基于某个控件的某个方向的中间进行对齐
  • 不支持基线对齐 (baseline
  • 所有子组件必须强制声明 id,不支持 id 有效性的检查(重复或者不存在的情况),Parent id 的关键字是 __container__
  • 所有对齐的锚点 anchor 不支持代码补全,只能靠手敲
  • 对齐规则写法繁琐
  • 不支持相互约束(Parent <-- A <---> B --> Parent)

属实是一点优点没学到,iOS同学写Xib的看了都直摇头。

那有没有方案来优化 RelativeContainer 对齐规则写法呢,经过对TS语法一顿研究,发现可以通过工具方法来优化 alignRules 的写法:

优化 AlignRuleOption 属性结构体声明

alignRules(AlignRuleOption) 的入参 AlignRuleOption 有如下属性:

属性名 功能 对应 Android RelativeLayout 属性(基于Parent对齐的属性没有写)
left 当前控件左边基于anchor的(左边、右边和水平方向中间)对齐 layout_alignLeft or layout_toRightOf
top 当前控件上方基于anchor的(上方、下方和垂直方向中间)对齐 layout_alignTop or layout_below
right 当前控件右边基于anchor的(左边、右边和水平方向中间)对齐 layout_alignRight or layout_toLeftOf
bottom 当前控件下方基于anchor的(上方、下方和垂直方向中间)对齐 layout_alignBottom or layout_above
center 当前控件垂直方向中间基于anchor的(上方、下方和垂直方向中间)对齐 layout_centerVertical,只支持基于父容器居中对齐
middle 当前控件水平方向中间基于anchor的(左边、右边和水平方向中间)对齐 layout_centerHorizontal,只支持基于父容器居中对齐

它们每个属性都是 { anchor: "id", align: (VerticalAlign|HorizontalAlign).* } 的结构体,可以对这些结构体声明进行优化:

// 把 `__container__` 提取成常量
export const Parent = "__container__"

// 声明垂直方向对齐属性结果类型
declare interface VerticalRule {
  anchor: string,
  align: VerticalAlign
}

// 声明水平方向对齐属性结果类型
declare interface HorizontalRule {
  anchor: string,
  align: HorizontalAlign
}

// 这两个接口主要是为了显示声明结构体属性类型
// API 9是可以不声明的
// API 10增强了代码检查需要声明返回值类型方法才可能return数据

export function toLeftOf(id: string): HorizontalRule {
  return { anchor: id, align: HorizontalAlign.Start }
}

export function toRightOf(id: string): HorizontalRule {
  return { anchor: id, align: HorizontalAlign.End }
}

export function centerHorizontalOf(id: string): HorizontalRule {
  return { anchor: id, align: HorizontalAlign.Center }
}

export function toTopOf(id: string): VerticalRule {
  return { anchor: id, align: VerticalAlign.Top }
}

export function toBottomOf(id: string): VerticalRule {
  return { anchor: id, align: VerticalAlign.Bottom }
}

export function centerVerticalOf(id: string): VerticalRule {
  return { anchor: id, align: VerticalAlign.Center }
}

修改上面Demo的代码

// 别忘了导包
import { centerHorizontalOf, centerVerticalOf, Parent, toBottomOf } from '../utils/RelativeContainerExtend';

RelativeContainer() {
    Text('Hello World')
        .id("text")
        .alignRules({
            center: centerVerticalOf(Parent),
            middle: centerHorizontalOf(Parent),
        })
        .backgroundColor(Color.Red)

    Text("Test")
        .id("test")
        .alignRules({
            left: centerHorizontalOf("text"),
            top: toBottomOf("text")
        })
        .backgroundColor(Color.Green)
}
.width('100%')
.height('100%')

是不是以为到这里就结束了,No No No,我们还可以再进一步

优化 AlignRuleOption 整体声明

我们可以让它更有 Android 特色,同时让API更明了。

通过上面的 和 RelativeLayout xml 属性进行对比表格,发现它不太适合 RelativeLayout xml 属性的那套命名,反而更像 ConstraintLayout 的位置约束属性,索性直接套 ConstraintLayout xml 的属性:leftToLeftOf,leftToRightOf 等等这些来扩展。首先声明支持的属性:

declare interface AlignRules {
  leftToLeftOf?: string,
  leftToRightOf?: string,
  rightToLeftOf?: string,
  rightToRightOf?: string,
  topToTopOf?: string,
  topToBottomOf?: string,
  bottomToTopOf?: string,
  bottomToBottomOf?: string,

  // 居中属性
  centerOf?: string,
  centerHorizontalOf?: string,
  centerVerticalOf?: string,
}

然后添加一个方法将 AlignRules 转换为 AlignRuleOption,同时对一些居中情况进行特殊处理

/**
 * 构建相对布局规则
 * @param rules
 * @returns
 */
export function buildRules(rules: AlignRules): AlignRuleOption {
  let _left: HorizontalRule | undefined = undefined
  if (rules.leftToLeftOf != null && rules.leftToRightOf != null) {
    throw Error("leftToLeftOf 和 leftToRightOf 不能同时约束")
  } else if (rules.leftToLeftOf != null) {
    _left = toLeftOf(rules.leftToLeftOf!)
  } else {
    _left = toRightOf(rules.leftToRightOf!)
  }

  let _right: HorizontalRule | undefined = undefined
  if (rules.rightToLeftOf != null && rules.rightToRightOf != null) {
    throw Error("rightToLeftOf 和 rightToRightOf 不能同时约束")
  } else if (rules.rightToLeftOf != null) {
    _right = toLeftOf(rules.rightToLeftOf!)
  } else {
    _right = toRightOf(rules.rightToRightOf!)
  }

  let _middle: HorizontalRule | undefined = undefined
  if (
    rules.centerHorizontalOf != null ||
      (_left != null && _right != null &&
        _left.anchor == _right.anchor &&
        _left.align == HorizontalAlign.Start &&
        _right.align == HorizontalAlign.End)
  ) {
    _middle = rules.centerHorizontalOf != null ? centerHorizontalOf(rules.centerHorizontalOf!) : centerHorizontalOf(_left.anchor!)
    _left = undefined
    _right = undefined
  }

  let _top: VerticalRule | undefined = undefined
  if (rules.topToTopOf != null && rules.topToBottomOf != null) {
    throw Error("topToTopOf 和 topToBottomOf 不能同时约束")
  } else if (rules.topToTopOf != null) {
    _top = toTopOf(rules.topToTopOf!)
  } else {
    _top = toBottomOf(rules.topToBottomOf!)
  }

  let _bottom: VerticalRule | undefined = undefined
  if (rules.bottomToTopOf != null && rules.bottomToBottomOf != null) {
    throw Error("bottomToTopOf 和 bottomToBottomOf 不能同时约束")
  } else if (rules.bottomToTopOf != null) {
    _bottom = toTopOf(rules.bottomToTopOf!)
  } else {
    _bottom = toBottomOf(rules.bottomToBottomOf!)
  }

  let _center: VerticalRule | undefined = undefined
  if (
    rules.centerVerticalOf != null ||
      (_top != null && _bottom != null &&
        _top.anchor == _bottom.anchor &&
        _top.align == VerticalAlign.Top &&
        _bottom.align == VerticalAlign.Bottom)
  ) {
    _center = rules.centerVerticalOf != null ? centerVerticalOf(rules.centerVerticalOf!) : centerVerticalOf(_top.anchor!)
    _top = undefined
    _bottom = undefined
  }

  if (rules.centerOf != null) {
    _middle = centerHorizontalOf(rules.centerOf)
    _center = centerVerticalOf(rules.centerOf)
    _left = undefined
    _right = undefined
    _top = undefined
    _bottom = undefined
  }

  return {
    left: _left,
    right: _right,
    middle: _middle,
    top: _top,
    bottom: _bottom,
    center: _center,
  }
}

再看看上面的Demo代码怎么写:

import { buildRules, Parent } from '../utils/RelativeContainerExtend';

@Extend(Text)
function styling() {
  .fontSize(20)
  .fontWeight(FontWeight.Bold)
  .fontColor(Color.Black)
}

@Entry
@Component
struct Index {
  build() {
    Navigation() {
      RelativeContainer() {
        Text('Hello World')
          .margin(12)
          .styling()
          .id("text")
          .alignRules(buildRules({
            centerOf: Parent
          }))
          .backgroundColor(Color.Red)

        Text("Left")
          .id("left")
          .styling()
          .alignRules(buildRules({
            rightToLeftOf: "text",
            centerVerticalOf: "text"
          }))
          .backgroundColor(Color.Brown)

        Text("Right")
          .styling()
          .id("right")
          .alignRules(buildRules({
            leftToRightOf: "text",
            centerVerticalOf: "text"
          }))
          .backgroundColor(Color.Green)

        Text("Top")
          .styling()
          .id("top")
          .alignRules(buildRules({
            bottomToTopOf: "text",
            centerHorizontalOf: "text"
          }))
          .backgroundColor(Color.Yellow)

        Text("Bottom")
          .styling()
          .id("bottom")
          .alignRules(buildRules({
            topToBottomOf: "text",
            centerHorizontalOf: "text"
          }))
          .backgroundColor(Color.Orange)
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .title("Hello HarmonyOS")
    .titleMode(NavigationTitleMode.Mini)
  }
}

总结

到此对 RelativeContainer 的扩展也就结束了,期盼已久的完整代码马上就来

Demo仓库

直接将RelativeContainerExtend.ets复制到项目中即可使用

相关链接