引子:看似简单的交互,背后有多少门道

一个完整的应用不能只有一个功能模块。硬币页面和随机数页面虽然逻辑更简单,却涉及了 ArkUI 中几个关键的交互机制:事件绑定、输入组件处理、条件渲染和动画控制。

完整效果
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

硬币页面:事件绑定与动画控制

硬币页面的代码很短,但包含的信息量不小:

@Builder CoinTab() {
  Column() {
    Text('硬币').fontSize(FontSize.title).fontColor(this.gc().text)
      .fontWeight(FontWeight.Bold).margin({ top: Spacing.lg })
    Column() {
      if (this.coinResult) {
        Text(this.coinResult === '正面' ? '🪙' : '🪙').fontSize(96)
          .rotate({ angle: this.coinResult === '正面' ? 0 : 180 })
        Text(this.coinResult).fontSize(FontSize.xxl).fontColor(this.gc().accent)
          .fontWeight(FontWeight.Bold).margin({ top: Spacing.md })
      } else {
        Text('🪙').fontSize(96)
        Text('点击下方抛硬币').fontSize(FontSize.md).fontColor(this.gc().text2)
          .margin({ top: Spacing.md })
      }
    }.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    Row() {
      Text('🪙 抛硬币').fontSize(FontSize.xl).fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }.width('80%').height(56).justifyContent(FlexAlign.Center)
      .backgroundColor(this.gc().primary).borderRadius(BorderRadius.xl)
      .onClick(() => this.flipCoin()).margin({ bottom: Spacing.xl })
  }.width('100%').height('100%')
}

在这里插入图片描述

条件渲染:if-else 的 ArkTS 写法

硬币页面最明显的特征是"有结果"和"没结果"两种状态。代码用 if-else 来控制显示内容:

if (this.coinResult) {
  // 有结果:显示硬币和结果文字
} else {
  // 无结果:显示默认状态
}

这和 React 的条件渲染很像,但 ArkTS 的 if-else 是在 build() 方法中直接使用的,不需要 JSX 的三元运算符或 && 短路。这种写法更直观,但也更容易写出性能问题——如果 if-else 的条件频繁变化,会导致整个 Column 重新渲染。

在这个应用中,coinResult 只在点击按钮时变化,所以性能没问题。但如果条件变化很频繁(比如实时搜索),建议用 if 控制显示/隐藏,而不是切换整个内容块。

旋转动画:.rotate() 的使用

硬币翻转的视觉效果是通过 .rotate() 实现的:

Text(this.coinResult === '正面' ? '🪙' : '🪙').fontSize(96)
  .rotate({ angle: this.coinResult === '正面' ? 0 : 180 })

这里有个有趣的细节:硬币的 emoji 都是 🪙,但通过旋转角度区分正反面。正面是 0 度,反面是 180 度(上下翻转)。

.rotate() 是 ArkUI 的属性动画,它会在属性值变化时自动添加过渡效果。但这里有个问题:代码中没有显式设置动画参数(比如持续时间、缓动函数),所以动画效果可能很生硬。

如果要更流畅的翻转效果,可以用 animateTo()

private flipCoin(): void {
  animateTo({
    duration: 500,
    curve: Curve.EaseInOut
  }, () => {
    this.coinResult = Math.random() > 0.5 ? '正面' : '反面';
  });
}

这样硬币会用 500 毫秒的时间平滑地从 0 度旋转到 180 度,视觉效果更好。

事件绑定:.onClick() 的执行时机

按钮的点击事件通过 .onClick(() => this.flipCoin()) 绑定。这里用箭头函数而不是直接引用方法,是有原因的:

  • 如果写成 .onClick(this.flipCoin)this 的指向会丢失
  • 箭头函数确保 this 指向当前组件实例

.onClick() 是 ArkUI 最常用的事件绑定方式,它等同于原生的 onclick。在 ArkUI 中,几乎所有的用户交互都可以通过类似的事件绑定来处理:

  • .onClick():点击事件
  • .onLongPress():长按事件
  • .onSwipe():滑动事件
  • .onHover():悬停事件

flipCoin 方法:极简的随机逻辑

private flipCoin(): void {
  this.coinResult = Math.random() > 0.5 ? '正面' : '反面';
}

这个方法只有一行代码,但包含了一个典型的设计决策:随机结果的表示方式

代码用字符串 '正面''反面' 来表示结果,而不是布尔值 true/false。这样做的好处是:

  1. 可读性:看到 this.coinResult === '正面'this.coinResult === true 更直观
  2. 可扩展性:如果将来要加"侧面"(虽然是硬币不太可能),只需改字符串
  3. 调试友好:在日志中看到 "coinResult: 正面""coinResult: true" 更容易理解

但字符串比较也有缺点:拼写错误会导致 bug。如果写成 '正而'(而不是 '正面'),编译器不会报错,但运行时逻辑会出问题。可以用枚举来避免这个问题:

enum CoinSide {
  HEADS = '正面',
  TAILS = '反面'
}

private flipCoin(): void {
  this.coinResult = Math.random() > 0.5 ? CoinSide.HEADS : CoinSide.TAILS;
}

随机数页面:输入组件与数据绑定

随机数页面比硬币页面复杂一点,因为它涉及用户输入:

@Builder RandTab() {
  Column() {
    Text('随机数').fontSize(FontSize.title).fontColor(this.gc().text)
      .fontWeight(FontWeight.Bold).margin({ top: Spacing.lg })
    Column() {
      if (this.randResult > 0) {
        Text(String(this.randResult)).fontSize(80).fontColor(this.gc().accent)
          .fontWeight(FontWeight.Bold)
        Text('范围: ' + String(Math.min(this.randMin, this.randMax)) + ' ~ ' 
          + String(Math.max(this.randMin, this.randMax)))
          .fontSize(FontSize.sm).fontColor(this.gc().text2).margin({ top: Spacing.md })
      } else {
        Text('🎯').fontSize(80)
        Text('设置范围后生成').fontSize(FontSize.md).fontColor(this.gc().text2)
          .margin({ top: Spacing.md })
      }
    }.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    Row() {
      Text('最小').fontSize(FontSize.sm).fontColor(this.gc().text2).width(40)
      TextInput({ text: String(this.randMin) }).width(70).height(40).fontSize(FontSize.md)
        .fontColor(this.gc().text).backgroundColor(this.gc().surface)
        .borderRadius(BorderRadius.sm).border({ width: 1, color: this.gc().border })
        .type(InputType.Number)
        .onChange((v: string) => this.randMin = parseInt(v) || 1)
      Blank()
      Text('最大').fontSize(FontSize.sm).fontColor(this.gc().text2).width(40)
        .margin({ left: Spacing.sm })
      TextInput({ text: String(this.randMax) }).width(70).height(40).fontSize(FontSize.md)
        .fontColor(this.gc().text).backgroundColor(this.gc().surface)
        .borderRadius(BorderRadius.sm).border({ width: 1, color: this.gc().border })
        .type(InputType.Number)
        .onChange((v: string) => this.randMax = parseInt(v) || 100)
    }.width('100%').padding({ left: Spacing.md, right: Spacing.md })
      .margin({ bottom: Spacing.lg })
    Row() {
      Text('🎯 生成').fontSize(FontSize.xl).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
    }.width('80%').height(56).justifyContent(FlexAlign.Center)
      .backgroundColor(this.gc().primary).borderRadius(BorderRadius.xl)
      .onClick(() => this.doRandom()).margin({ bottom: Spacing.xl })
  }.width('100%').height('100%')
}

在这里插入图片描述

TextInput 组件:用户输入的处理

TextInput 是 ArkUI 的输入组件,这里用了几个关键属性:

TextInput({ text: String(this.randMin) })
  .type(InputType.Number)  // 限制为数字输入
  .onChange((v: string) => this.randMin = parseInt(v) || 1)

.type(InputType.Number):限制输入类型为数字。这不仅会影响键盘类型(弹出数字键盘),还会进行输入校验——用户不能输入字母或特殊字符。

.onChange():输入内容变化时的回调。这里用 parseInt(v) || 1 来处理空输入和非法输入——如果输入为空或非数字,默认值为 1。

但这里有个潜在问题:parseInt(v) 在输入为空时返回 NaN,而 NaN || 1 的结果是 1。这看起来没问题,但如果用户输入 “0”,parseInt("0") 返回 0,而 0 || 1 的结果是 1——用户无法输入 0 作为最小值。

修复方案是用 isNaN 判断:

.onChange((v: string) => {
  const num = parseInt(v);
  this.randMin = isNaN(num) ? 1 : num;
})

双向绑定的陷阱

ArkTS 的 TextInput 不支持直接的双向绑定(像 Vue 的 v-model 那样)。代码用 .onChange() 手动同步状态,这是一种常见的模式,但有几个注意事项:

  1. 性能:每次输入都会触发 onChange,更新 @State,触发重新渲染。如果输入框很多,可能有性能问题
  2. 输入法兼容:某些输入法会在输入过程中多次触发 onChange,导致状态更新不一致
  3. 数字转换parseInt 会丢弃小数部分,如果要支持小数,需要用 parseFloat

对于这个应用,因为只有两个输入框且都是整数,性能不是问题。但如果要做更复杂的表单,建议用 @State 管理整个表单状态,而不是每个字段单独绑定。

范围处理:Math.min 和 Math.max

Text('范围: ' + String(Math.min(this.randMin, this.randMax)) + ' ~ ' 
  + String(Math.max(this.randMin, this.randMax)))

代码用 Math.minMath.max 来确保范围显示正确,即使用户把最小值设得比最大值还大。这是一个好的防御性编程实践——不假设用户输入是正确的。

doRandom 方法也有同样的处理:

private doRandom(): void {
  const min = Math.min(this.randMin, this.randMax);
  const max = Math.max(this.randMin, this.randMax);
  this.randResult = Math.floor(Math.random() * (max - min + 1)) + min;
}

这保证了无论用户怎么输入,生成的随机数都在合理范围内。

随机数生成算法

Math.floor(Math.random() * (max - min + 1)) + min 是标准的均匀分布随机数生成公式。它生成 [min, max] 范围内的整数,包括两端。

这个公式的推导过程:

  1. Math.random() 生成 [0, 1) 的浮点数
  2. 乘以 (max - min + 1) 得到 [0, max - min + 1) 的浮点数
  3. Math.floor() 向下取整得到 [0, max - min] 的整数
  4. 加上 min 得到 [min, max] 的整数

为什么是 max - min + 1 而不是 max - min

因为 Math.random() 不包含 1,所以乘以 max - min 只能得到 [0, max - min),取整后是 [0, max - min - 1],少了一个值。加上 1 就能覆盖整个范围。

两个页面的共同点:状态驱动的 UI

硬币页面和随机数页面虽然功能不同,但它们的 UI 逻辑有一个共同点:都是由状态驱动的

  • 硬币页面:coinResult 决定显示什么
  • 随机数页面:randResult 决定显示什么

这种"状态驱动"的模式是声明式 UI 的核心思想。开发者不需要手动操作 DOM(或者 ArkUI 中的 UI 元素),只需要修改状态,框架会自动更新 UI。

和命令式 UI 相比,声明式 UI 的好处是:

  1. 代码更简洁:不需要写 document.getElementById('xxx').innerHTML = 'yyy'
  2. 更容易维护:UI 和状态的对应关系一目了然
  3. 性能更好:框架会自动优化渲染,只更新变化的部分

但声明式 UI 也有缺点:当 UI 逻辑很复杂时,状态管理会变得困难。这就是为什么 ArkUI 提供了 @State@Prop@Link 等多种状态管理方案。

踩坑记录

在开发这两个页面时,遇到了几个值得注意的问题:

坑 1:TextInput 的焦点管理

ArkTS 的 TextInput 在获取焦点后会弹出键盘,但没有提供直接的方法来控制焦点。如果需要在某个条件下自动聚焦(比如页面加载后),需要用 @State 和条件渲染来实现。

坑 2:数字输入的边界值

代码中用 parseInt(v) || 1 来处理空输入,但这会把 0 当作非法值。如果要支持 0,需要用 isNaN 判断。这是一个典型的"边界值"问题——开发者容易忽略 0、负数、小数等特殊情况。

坑 3:动画的触发时机

硬币翻转的 .rotate() 动画在 coinResult 变化时触发,但如果用户快速连续点击,动画可能会被中断。需要添加防抖逻辑,确保一次动画完成后再响应下一次点击。

总结

硬币页面和随机数页面虽然逻辑简单,但涉及了 ArkUI 开发的多个核心知识点:事件绑定、输入组件、条件渲染、动画控制、数据绑定。在实际开发中,这些知识点会反复出现,只是复杂度不同。

适用边界:这两个页面适合用作 ArkUI 交互开发的入门案例,涵盖了事件处理、用户输入、动画控制等核心知识点。但如果要上架应用商店,还需要补充输入校验、焦点管理、动画防抖、无障碍支持等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

对于 ArkTS 新手,建议从类似的小项目入手,逐步理解框架的设计哲学。ArkTS 的声明式 UI 和 React 有相似之处,但状态管理和生命周期有明显区别,需要花时间适应。

对于有经验的开发者,重点是理解 ArkTS 的约束——它不是 TypeScript 的简单扩展,而是一个有自己规则的框架。遵循框架的最佳实践,才能写出可维护、高性能的代码。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐