之前在『「MOJiKana」中模仿实体按键的动效是如何实现的?』一文中介绍了 Android 平台 View 体系以及 Compose 体系关于 KeyCap 的开发实践。文中我顺嘴提了一句 ArkUI 的实现方式,但并未给出实际代码,今天单独来讲下 ArkUI 中的实现方式。

KeyCap 效果

因为 Android 平台无论是 View 还是 Compose 默认情况下都不支持单独设置某一方向上的边框,所以我们采用两个图层叠加的方式来模拟。采用两个图层有个小缺陷,就是当两个图层之间的间隔过高时,视觉效果就不再像是一个实体按键了。

KeyCap 间隔过高

当然,这既可以归咎为使用不当,也可以说是我设计的时候并没有兼容,其实只需要将底部图层的高度撑满整个父布局就可以解决这个问题。

但是像 ArkUI 就可以使用单边 border 来规避这种问题。

@Component
export struct KeyCap {
  keyFlatColor: ResourceColor = Color.Transparent
  keyShadowColor: ResourceColor = Color.Transparent
  keyStroke: Length = 0
  keyCapRadius: Length = 0
  @BuilderParam child: () => void = this.customBuilder
  @State private pressed: boolean = false

  @Builder
  customBuilder() {
  }

  build() {
    RelativeContainer() {
      Stack({ alignContent: Alignment.Center }) {
        this.child()
      }
      .width('100%')
      .backgroundColor(this.keyFlatColor)
      .margin({ top: this.pressed ? this.keyStroke : 0 })
      .borderRadius(this.keyCapRadius)
      .borderColor(this.keyShadowColor)
      .borderWidth({ bottom: this.pressed ? 0 : this.keyStroke })
      .alignRules({
        top: { anchor: '__container__', align: VerticalAlign.Top },
        bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
      })
    }.onTouch((event: TouchEvent) => {
      if (event.type == TouchType.Down) {
        this.pressed = true
      } else if (event.type == TouchType.Up || event.type == TouchType.Cancel) {
        this.pressed = false
      }
    })
  }
}

属性还是跟之前一样,可以看到这里少了一层组件,通过判断是否按压来调节底部边框宽度以及顶部边距,整体代码量又简洁了不少。

虽说这种方式值的一夸,但众所周知,ArkUI 从不经夸。在自定义组件中,如果需要添加子组件,我们无法像 Compose 一样采用高阶函数,甚至由于系统组件无法查看具体实现,我们没办法模拟出这种闭包的写法,只能将子组件通过属性传递。

子组件需要通过 @BuilderParam 传递,它用来承接 @Builder 函数,这意味着我们还需要单独写一个 @Builder 函数来实现子组件:

@Entry
@Component
struct MainPage {
  Stack({ alignContent: Alignment.Center }) {
    KeyCap({
      keyFlatColor: Color.Brown,
      keyShadowColor: Color.Orange,
      keyStroke: 6,
      keyCapRadius: 60,
      child: this.keyCapChildBuilder
    })
      .width(120)
      .height(54)
      .onClick(() => {})
  }
  @Builder
  keyCapChildBuilder() {
    Text()  // 子组件实现
  }
}

这种设计非常蠢,仿佛 View Tree 层次缺失,不利于阅读。

又能怎么样呢,甩甩手上的屎继续写呗。