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

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

当然,这既可以归咎为使用不当,也可以说是我设计的时候并没有兼容,其实只需要将底部图层的高度撑满整个父布局就可以解决这个问题。
但是像 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 层次缺失,不利于阅读。
又能怎么样呢,甩甩手上的屎继续写呗。