前言

各位开发者,大家好!我是若城。

在鸿蒙应用开发过程中,我发现许多组件样式和工具方法具有高度的复用性,但每次新项目都需要重复编写,这极大地降低了开发效率。因此,我决定投入半年时间,打造一款专为鸿蒙生态设计的 UI 组件库 —— rchoui

项目简介

rchoui 是一个面向 HarmonyOS6 的企业级 UI 组件库,旨在提供开箱即用的高质量组件,让开发者告别"重复造轮子"。

核心特性

  • 丰富组件:涵盖基础组件、表单组件、弹窗组件、布局组件等
  • 设计规范:遵循统一的色彩体系和设计语言
  • 工具集成:内置常用工具方法,提升开发效率
  • 完善文档:每个模块都配有详细的设计思路和使用说明

开源计划

项目预计于 2026 年 7 月中旬正式开源,届时可通过三方库直接下载使用。在此期间,我会通过系列文章逐一介绍每个模块的设计思路与实现细节。

一、概述

交互是UI组件的灵魂,RcButton通过精心设计的事件处理机制,为用户提供清晰的反馈和流畅的操作体验。本文将深入解析组件的交互逻辑、事件处理、状态联动以及性能优化策略。
效果展示:

二、事件系统架构

2.1 事件定义

RcButton使用@Event装饰器定义点击事件:

/**
 * 点击事件
 */
@Event onBtnClick: (event: ClickEvent) => void = () => {}

装饰器特性:

  • @Event表示这是一个事件回调属性
  • 类型为(event: ClickEvent) => void,接收点击事件对象
  • 默认值为空函数() => {},避免未定义错误

事件对象 ClickEvent:

  • 包含点击位置坐标
  • 包含时间戳
  • 包含触摸点信息
  • 继承自HarmonyOS6的原生事件对象

2.2 事件注册

build方法中,通过.onClick()注册事件处理器:

Button({ type: ButtonType.Normal }) {
  // 内容构建
}
.onClick(this.handleClick)

特点:

  • 使用方法引用而非内联函数,提高性能
  • handleClick是组件的私有方法,封装了事件处理逻辑

2.3 事件处理流程

private handleClick = (event: ClickEvent): void => {
  // 第一步: 状态检查
  if (this.disabled || this.loading) {
    return
  }

  // 第二步: 节流控制
  const currentTime = Date.now()
  if (this.throttleTime && this.throttleTime > 0) {
    if (currentTime - this.lastClickTime < this.throttleTime) {
      return
    }
    this.lastClickTime = currentTime
  }

  // 第三步: 触发回调
  this.onBtnClick(event)
}

处理流程图:

用户点击
   ↓
handleClick被调用
   ↓
检查disabled/loading ─→ 是 ─→ 忽略点击,返回
   ↓ 否
节流时间检查 ─→ 在节流期内 ─→ 忽略点击,返回
   ↓ 否或无节流
触发onBtnClick回调
   ↓
用户代码执行

三、状态控制机制

3.1 禁用状态(disabled)

状态定义
@Param disabled?: boolean = false
影响范围

1. 点击事件拦截

if (this.disabled || this.loading) {
  return
}

禁用时所有点击被拦截,不会触发onBtnClick回调。

2. 交互能力控制

.enabled(!this.disabled && !this.loading)

使用HarmonyOS6的.enabled()属性控制组件是否可交互。当enabled(false)时:

  • 组件不响应任何触摸事件
  • 无法获得焦点
  • 不触发按压态样式

3. 视觉样式变化

背景色:

.backgroundColor(this.disabled ?
  this.getDisabledColor() :
  (this.plain || this.textButton ? Color.Transparent : this.getColorConfig().bg))

透明度:

.opacity(this.disabled ? 0.6 : 1)

文字色:

private getTextColor(): ResourceColor {
  if (this.textColor !== undefined) {
    return this.textColor
  }

  if (this.disabled) {
    return this.getDisabledColor()
  }
  // ...其他逻辑
}

视觉效果总结:

  • 实体按钮: 背景变为禁用色,透明度0.6
  • 镂空按钮: 边框和文字变为灰色,透明度0.6
  • 文本按钮: 文字变为灰色,透明度0.6
使用场景
// 表单验证未通过
RcButton({ 
  text: '提交', 
  type: RcButtonType.PRIMARY,
  disabled: !isFormValid 
})

// 数据加载中
RcButton({ 
  text: '删除', 
  type: RcButtonType.ERROR,
  disabled: isDeleting 
})

3.2 加载状态(loading)

状态定义
@Param loading?: boolean = false
@Param loadingText?: string = ''
影响范围

1. 点击事件拦截

与disabled相同,loading状态也会拦截点击:

if (this.disabled || this.loading) {
  return
}

.enabled(!this.disabled && !this.loading)

2. 视觉内容替换

显示加载动画:

if (this.loading) {
  LoadingProgress()
    .width(this.iconSize || this.getSizeConfig().iconSize)
    .height(this.iconSize || this.getSizeConfig().iconSize)
    .color(this.getTextColor())
    .margin({ right: (this.loadingText || this.text) ? 6 : 0 })
}

隐藏普通图标:

if (!this.loading && this.icon) {
  RcIcon({
    name: this.icon,
    iconSize: this.iconSize || this.getSizeConfig().iconSize,
    color: this.getTextColor()
  })
}

文本切换:

if (this.loading && this.loadingText) {
  Text(this.loadingText)
    .fontSize(this.getTextSize())
    .fontColor(this.getTextColor())
} else if (this.text) {
  Text(this.text)
    .fontSize(this.getTextSize())
    .fontColor(this.getTextColor())
}

加载状态的内容优先级:

  1. loading=true且有loadingText: 显示loadingText
  2. loading=true但无loadingText: 只显示加载动画,保持原text
  3. loading=false: 显示正常内容
加载动画特性
LoadingProgress()

HarmonyOS6的LoadingProgress组件特点:

  • 自动旋转动画,无需额外代码
  • 颜色可自定义,跟随按钮文字色
  • 大小与图标大小保持一致
  • 性能优化,GPU加速
使用场景

异步操作反馈:

@State isLoading: boolean = false

const handleSubmit = async () => {
  this.isLoading = true
  try {
    await submitForm()
  } finally {
    this.isLoading = false
  }
}

RcButton({ 
  text: '提交', 
  loading: this.isLoading,
  loadingText: '提交中...',
  type: RcButtonType.PRIMARY,
  onBtnClick: handleSubmit
})

只显示动画:

RcButton({ 
  text: '加载', 
  loading: true, 
  type: RcButtonType.PRIMARY 
})
// 显示: [旋转动画] 加载

带加载文案:

RcButton({ 
  text: '加载', 
  loading: true, 
  loadingText: '加载中...',
  type: RcButtonType.PRIMARY 
})
// 显示: [旋转动画] 加载中...

3.3 状态组合

disabled和loading可以同时为true,逻辑关系:

.enabled(!this.disabled && !this.loading)

组合效果表:

disabled loading 可点击 视觉状态 内容显示
false false 正常 正常内容
true false 禁用样式 正常内容
false true 正常 加载动画+文本
true true 禁用样式 加载动画+文本

最佳实践:

  • 通常不需要同时设置disabled和loading
  • 如果同时设置,禁用样式会覆盖加载样式
  • 建议只使用loading,它自带禁用交互能力

四、节流机制深度解析

4.1 节流原理

节流(Throttle)是一种性能优化手段,限制函数在一定时间内只能执行一次。

无节流问题:

  • 用户可能快速多次点击
  • 每次点击都触发回调
  • 可能导致重复提交、性能问题

节流解决方案:

  • 记录上次执行时间
  • 在时间间隔内的点击被忽略
  • 间隔过后才允许下次点击

4.2 节流实现

状态定义
@Param throttleTime?: number = 0
@Local lastClickTime: number = 0
  • throttleTime: 节流间隔(毫秒),0表示不节流
  • lastClickTime: 上次点击时间戳
实现逻辑
private handleClick = (event: ClickEvent): void => {
  if (this.disabled || this.loading) {
    return
  }

  const currentTime = Date.now()
  if (this.throttleTime && this.throttleTime > 0) {
    if (currentTime - this.lastClickTime < this.throttleTime) {
      return
    }
    this.lastClickTime = currentTime
  }

  this.onBtnClick(event)
}

执行流程:

  1. 获取当前时间戳currentTime
  2. 检查是否设置了节流时间
  3. 计算距离上次点击的时间间隔
  4. 如果间隔小于节流时间,忽略点击
  5. 否则,更新lastClickTime并触发回调

时间轴示例:

假设节流时间为1000ms:

时刻    操作       lastClickTime    currentTime    间隔    结果
0ms    首次点击      0              0           0      执行,更新为0
300ms  第2次点击     0              300         300    忽略
600ms  第3次点击     0              600         600    忽略
1100ms 第4次点击     0              1100        1100   执行,更新为1100
1200ms 第5次点击     1100           1200        100    忽略
2200ms 第6次点击     1100           2200        1100   执行,更新为2200

4.3 节流vs防抖

节流(Throttle):

  • 固定时间间隔内只执行一次
  • 首次点击立即执行
  • 适合: 按钮点击、滚动事件

防抖(Debounce):

  • 连续触发时只执行最后一次
  • 等待触发停止后才执行
  • 适合: 输入框搜索、窗口调整

RcButton使用节流而非防抖的原因:

  • 按钮点击需要立即反馈
  • 防抖会延迟用户感知
  • 节流可以立即执行首次点击

4.4 节流配置建议

场景推荐:

场景 推荐节流时间 原因
普通按钮 0(不节流) 允许快速连续操作
表单提交 1000-2000ms 防止重复提交
网络请求 1000-3000ms 避免频繁请求
付款按钮 2000-5000ms 防止误操作和重复扣款
点赞/收藏 500ms 轻量操作,短节流

使用示例:

// 表单提交按钮
RcButton({ 
  text: '提交', 
  type: RcButtonType.PRIMARY,
  throttleTime: 2000, // 2秒内只能点击一次
  onBtnClick: () => {
    submitForm()
  }
})

// 付款按钮
RcButton({ 
  text: '确认支付', 
  type: RcButtonType.WARNING,
  throttleTime: 5000, // 5秒内只能点击一次
  onBtnClick: () => {
    processPay()
  }
})

4.5 节流与loading的配合

实际应用中,通常将节流和loading结合使用:

@State isSubmitting: boolean = false

const handleSubmit = async () => {
  this.isSubmitting = true
  try {
    await api.submit()
  } finally {
    this.isSubmitting = false
  }
}

RcButton({ 
  text: '提交', 
  type: RcButtonType.PRIMARY,
  loading: this.isSubmitting,
  throttleTime: 1000,
  onBtnClick: handleSubmit
})

双重保护:

  1. 节流: 限制点击频率,防止短时间内多次触发
  2. loading: 在处理期间禁用按钮,防止重复触发

这种组合提供了最佳的用户体验和安全性。

五、按压反馈机制

5.1 状态样式系统

HarmonyOS6提供了stateStylesAPI用于定义不同状态的样式:

.stateStyles({
  normal: {
    .opacity(1)
  },
  pressed: {
    .backgroundColor(this.disabled || this.plain || this.textButton ?
      undefined :
      this.getColorConfig().activeBg)
    .opacity(this.disabled ? 0.6 : (this.plain || this.textButton ? 0.7 : 1))
  },
  disabled: {
    .opacity(0.6)
  }
})

5.2 三种状态

Normal(正常态)
normal: {
  .opacity(1)
}
  • 按钮未被按下时的状态
  • 透明度为1,完全不透明
  • 使用正常的背景色和文字色
Pressed(按压态)
pressed: {
  .backgroundColor(this.disabled || this.plain || this.textButton ?
    undefined :
    this.getColorConfig().activeBg)
  .opacity(this.disabled ? 0.6 : (this.plain || this.textButton ? 0.7 : 1))
}

背景色逻辑:

  • 禁用/镂空/文本按钮: 不改变背景色(undefined)
  • 实体按钮: 使用activeBg(深色背景)

透明度逻辑:

  • 禁用: 保持0.6
  • 镂空/文本: 降低到0.7
  • 实体: 保持1.0

反馈策略对比:

按钮类型 按压反馈方式 视觉效果
实体按钮 背景色加深 PRIMARY蓝色→深蓝色
镂空按钮 降低透明度 整体略微变暗
文本按钮 降低透明度 文字略微变暗
禁用按钮 无变化 保持禁用样式
Disabled(禁用态)
disabled: {
  .opacity(0.6)
}
  • 透明度固定0.6
  • 配合.enabled()使用
  • 与disabled属性联动

5.3 按压反馈的时机

触发时机:

  1. 用户手指按下按钮 → 进入pressed状态
  2. 用户手指离开按钮 → 返回normal状态
  3. 按钮点击事件在手指离开时触发

时序图:

用户操作      按钮状态      事件触发
   ↓
手指按下  →   pressed
   ↓            ↓
          背景变深/透明度降低
   ↓            ↓
手指离开  →   normal   →  onClick触发
   ↓            ↓            ↓
          恢复原样      handleClick执行

5.4 触觉反馈(可选)

虽然当前代码未实现,但可以扩展触觉反馈:

// 扩展示例
private handleClick = (event: ClickEvent): void => {
  if (this.disabled || this.loading) {
    return
  }

  // 触发触觉反馈
  vibrator.vibrate({ duration: 10, mode: 'short' })

  // 节流逻辑
  // ...

  this.onBtnClick(event)
}

触觉反馈增强用户体验,特别适合:

  • 重要操作按钮
  • 游戏场景
  • 移动端应用

六、图标与内容布局

6.1 内容布局结构

按钮内容使用Row布局,水平排列:

Button({ type: ButtonType.Normal }) {
  Row() {
    // 1. 加载动画(loading时显示)
    // 2. 图标(非loading时显示)
    // 3. 文本
  }
  .justifyContent(FlexAlign.Center)
  .alignItems(VerticalAlign.Center)
}

布局特点:

  • 水平居中对齐(FlexAlign.Center)
  • 垂直居中对齐(VerticalAlign.Center)
  • 图标在左,文本在右
  • 图标与文本间距6px

6.2 加载动画布局

if (this.loading) {
  LoadingProgress()
    .width(this.iconSize || this.getSizeConfig().iconSize)
    .height(this.iconSize || this.getSizeConfig().iconSize)
    .color(this.getTextColor())
    .margin({ right: (this.loadingText || this.text) ? 6 : 0 })
}

布局逻辑:

  • loading=true时显示
  • 尺寸与图标保持一致
  • 颜色跟随文字色
  • 如果有文本,右边距6px;如果没有文本,无边距

边距判断:

.margin({ right: (this.loadingText || this.text) ? 6 : 0 })

这个判断确保:

  • 有文本时: [动画] [6px] [文本]
  • 无文本时: [动画] (无多余空间)

6.3 图标布局

if (!this.loading && this.icon) {
  RcIcon({
    name: this.icon,
    iconSize: this.iconSize || this.getSizeConfig().iconSize,
    color: this.getTextColor()
  })
    .margin({ right: this.text ? 6 : 0 })
}

显示条件:

  • 非loading状态
  • 设置了icon属性

图标尺寸:

  1. 优先使用iconSize属性
  2. 否则根据btnSize自动计算

图标颜色:

  • 跟随文字颜色
  • 确保与按钮类型和状态匹配

边距逻辑:

  • 有文本: 右边距6px
  • 无文本: 无边距(纯图标按钮)

6.4 文本布局

if (this.loading && this.loadingText) {
  Text(this.loadingText)
    .fontSize(this.getTextSize())
    .fontColor(this.getTextColor())
} else if (this.text) {
  Text(this.text)
    .fontSize(this.getTextSize())
    .fontColor(this.getTextColor())
}

显示逻辑:

  1. loading且有loadingText: 显示loadingText
  2. 否则有text: 显示text
  3. 都没有: 不显示文本

优先级:

loadingText(loading时) > text

6.5 内容组合模式

模式1: 纯文本

RcButton({ text: '按钮' })

布局: [文本]

模式2: 图标+文本

RcButton({ text: '搜索', icon: 'search' })

布局: [图标] [6px] [文本]

模式3: 纯图标

RcButton({ icon: 'plus' })

布局: [图标]

模式4: 加载+文本

RcButton({ text: '提交', loading: true })

布局: [加载动画] [6px] [文本]

模式5: 加载+自定义文本

RcButton({ text: '提交', loading: true, loadingText: '提交中...' })

布局: [加载动画] [6px] [提交中…]

模式6: 纯加载动画

RcButton({ loading: true })

布局: [加载动画]

七、交互状态转换

7.1 状态机模型

按钮可以看作一个状态机,具有以下状态:

正常(Normal) ←→ 按压(Pressed)
     ↓
  禁用(Disabled)
     ↓
  加载(Loading)

状态转换规则:

当前状态 用户操作 下一状态 事件触发
Normal 按下 Pressed -
Pressed 释放 Normal onClick
Normal 设置disabled Disabled -
Disabled 取消disabled Normal -
Normal 设置loading Loading -
Loading 取消loading Normal -

7.2 交互阻断机制

某些状态会阻断交互:

.enabled(!this.disabled && !this.loading)

阻断条件:

  • disabled=true: 完全阻断
  • loading=true: 完全阻断

阻断效果:

  • 不响应触摸事件
  • 不进入pressed状态
  • 不触发onClick回调
  • 不获得焦点

7.3 状态恢复

通常状态转换由外部控制(通过属性变化):

@State submitDisabled: boolean = false
@State submitLoading: boolean = false

const handleSubmit = async () => {
  this.submitLoading = true
  try {
    await api.submit()
    // 成功后可能禁用按钮
    this.submitDisabled = true
  } catch (error) {
    // 失败后恢复可点击状态
  } finally {
    this.submitLoading = false
  }
}

RcButton({ 
  text: '提交', 
  disabled: this.submitDisabled,
  loading: this.submitLoading,
  onBtnClick: handleSubmit
})

八、性能优化策略

8.1 事件处理优化

使用箭头函数绑定:

private handleClick = (event: ClickEvent): void => {
  // ...
}

箭头函数自动绑定this,避免:

  • 使用.bind(this)
  • 在render中创建新函数

方法引用:

.onClick(this.handleClick)

而非:

.onClick((e) => this.handleClick(e)) // 每次render创建新函数

8.2 条件渲染优化

使用条件判断而非隐藏:

if (this.loading) {
  LoadingProgress()
}

if (!this.loading && this.icon) {
  RcIcon()
}

优势:

  • 不渲染不需要的组件
  • 减少组件树节点数
  • 降低内存占用

8.3 状态管理优化

使用@Local管理内部状态:

@Local lastClickTime: number = 0

优势:

  • 不触发外部re-render
  • 只在组件内部使用
  • 性能开销最小

8.4 样式计算缓存

虽然当前实现在每次render时都调用getter方法,但这些方法:

  • 计算简单(switch语句)
  • 返回静态对象
  • 执行速度极快

如果需要进一步优化,可以使用@Computed(如果框架支持):

// 优化示例
@Computed
get sizeConfig(): RcButtonSizeConfig {
  return this.getSizeConfig()
}

九、可访问性支持

9.1 焦点管理

通过.enabled()控制可获得焦点:

.enabled(!this.disabled && !this.loading)

禁用或加载时不可获得焦点,符合无障碍标准。

9.2 语义化

使用HarmonyOS6的Button组件:

Button({ type: ButtonType.Normal })

Button组件自带无障碍支持:

  • 屏幕阅读器识别为按钮
  • 可以通过键盘操作
  • 支持辅助触摸

9.3 状态反馈

视觉状态变化为辅助功能用户提供反馈:

  • 禁用: 降低透明度,改变颜色
  • 加载: 显示动画,可切换文本
  • 按压: 背景色或透明度变化

十、总结

RcButton的交互系统具有以下特点:

  1. 完善的事件处理: 节流、状态检查、回调触发
  2. 丰富的状态控制: disabled、loading及其组合
  3. 清晰的视觉反馈: 按压态、禁用态、加载态
  4. 灵活的内容布局: 图标、文本、加载动画自由组合
  5. 优秀的性能: 事件优化、条件渲染、状态管理
  6. 良好的可访问性: 焦点管理、语义化、状态反馈
    好了下课~~~~
Logo

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

更多推荐