《HarmonyOS技术精讲-UI开发》第2篇:常用UI组件详解

页面结构背后的组件选择

HarmonyOS NEXT 的 ArkUI 框架提供了声明式 UI 开发范式,在实际项目中,最常用的组件就是那五个:Button、Text、Image、TextField 和 Progress。很多刚接触 ArkTS 的开发者容易陷入一个误区------试图用一个组件完成所有需求,比如用 Text 模拟按钮,或者用 Image 加载超清图片导致内存溢出。

这篇文章直接拆开这五个组件,把每个常用属性都跑一遍。不会讲设计模式,也不扯架构,只聚焦于:怎么用、用在哪、容易在哪出问题。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

核心实现:逐个击破

下面这段代码是把五个组件组合在一个页面里,演示各自的核心能力。代码直接在 @Entry 装饰的组件里运行即可。

Button - 不止是"点击"

很多人以为 Button 就是 <button>Click</button> 的翻版,但 ArkUI 的 Button 支持更细粒度的样式控制。

typescript 复制代码
@Entry
@Component
struct ButtonDemo {
  @State count: number = 0

  build() {
    Column() {
      // 默认圆角按钮
      Button('默认样式')
        .onClick(() => {
          this.count++
          console.log('点击次数:' + this.count)
        })

      // 自定义背景色和圆角
      Button('自定义样式')
        .backgroundColor('#007AFF')
        .borderRadius(20)
        .width(200)
        .height(48)
        .fontColor(Color.White)
        .fontSize(16)
        .onClick(() => {
          console.log('自定义按钮被点击')
        })

      // 图标+文字按钮,通过 Image 组件内嵌
      Button() {
        Image($r('app.media.icon'))
          .width(20)
          .height(20)
        Text('保存')
          .fontSize(14)
      }
      .backgroundColor(Color.Green)
      .borderRadius(8)
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    }
    .justifyContent(FlexAlign.SpaceAround)
    .height('100%')
    .width('100%')
  }
}

这里需要注意Button() 的构造参数如果传字符串或资源引用,其实是《常规按钮》模式。如果想在按钮里放自定义组件(如 Image+Text),必须使用空白构造函数 Button(),然后在代码块里直接写子组件。

实际项目里,很多人直接在字符串参数里拼接文字和图标占位符,这是行不通的。必须用 Button(() => { ... }) 这种闭包写法

Text - 不仅仅是文本显示

Text 最容易被忽略的属性是 textOverflowmaxLines。很多新人在显示长文本时不加限制,导致布局撑爆。

typescript 复制代码
@Entry
@Component
struct TextDemo {
  build() {
    Column() {
      // 基础文本
      Text('这是一段普通文本').fontSize(16)

      // 带省略号的多行文本
      Text('这是一段很长的文本,超出部分会显示为省略号。实际开发中如果不对长度做限制,布局会变得不可控。')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .fontSize(14)
        .width(200)

      // 富文本:不同颜色和字体
      Text() {
        Span('红色').fontColor(Color.Red).fontSize(20)
        Span('蓝色').fontColor(Color.Blue).fontSize(16)
        Span('加粗').fontWeight(FontWeight.Bold)
      }

      // 可点击文本
      Text('点击我')
        .fontColor(Color.Blue)
        .decoration({ type: TextDecorationType.Underline })
        .onClick(() => {
          console.log('点击了可点击文本')
        })
    }
    .justifyContent(FlexAlign.SpaceAround)
    .height('100%')
    .width('100%')
  }
}

实际开发中,textOverflow 必须和 maxLines 同时使用才能生效 。只写 textOverflow 但不设置行数限制,在表现上非常诡异------它会截断文本但不会展示省略号。

Image - 图片加载的坑很多

Image 组件最常被问的问题就是:为什么图片显示不出来?

typescript 复制代码
@Entry
@Component
struct ImageDemo {
  @State imageWidth: number = 100
  @State imageHeight: number = 100

  build() {
    Column() {
      // 加载本地资源
      Image($r('app.media.logo'))
        .width(100)
        .height(100)
        .objectFit(ImageFit.Contain)

      // 加载网络图片(需要网络权限)
      Image('https://example.com/icon.png')
        .width(200)
        .height(200)
        .objectFit(ImageFit.Cover)
        .onError((err) => {
          console.log('图片加载失败:' + JSON.stringify(err))
        })

      // 圆形裁剪
      Image($r('app.media.avatar'))
        .width(80)
        .height(80)
        .borderRadius(40)
        .objectFit(ImageFit.Cover)
    }
    .justifyContent(FlexAlign.SpaceAround)
    .height('100%')
    .width('100%')
  }
}

三个容易翻车的地方:

  1. 网络图片需要 ohos.permission.INTERNET 权限 ,在 module.json5 里配置。不配置会报网络错误,但控制台日志非常隐晦。
  2. objectFit 默认是 ImageFit.Cover,如果图片比例和组件尺寸不一致,会被裁剪掉一部分。很多人想要"完整显示"却得到"部分显示",就是因为不知道默认值是什么。
  3. onError 回调里拿到的错误信息比较简陋 ,只有 error 字段,没有 HTTP 状态码。需要判断是网络问题还是图片资源不存在。

TextField - 输入框的正确用法

TextField 在 ArkUI 里支持输入类型、占位符、监听变化等。

typescript 复制代码
@Entry
@Component
struct TextFieldDemo {
  @State inputText: string = ''

  build() {
    Column() {
      TextField({ placeholder: '请输入用户名' })
        .width('80%')
        .height(48)
        .onChange((value: string) => {
          this.inputText = value
          console.log('输入内容:' + value)
        })

      TextField({ placeholder: '请输入密码', type: InputType.Password })
        .width('80%')
        .height(48)
        .onChange((value: string) => {
          console.log('密码输入长度:' + value.length)
        })

      Text('已输入:' + this.inputText)
        .fontSize(14)
        .margin({ top: 20 })
    }
    .justifyContent(FlexAlign.SpaceAround)
    .height('100%')
    .width('100%')
  }
}

一个反直觉的设计TextFieldonChange 回调在每次字符变化时触发,但在初始化赋值(比如用 @State 绑定初始值)时不会触发。如果需要在页面加载时做校验,必须在 onAppear 钩子里手动调用一次。

另外,type: InputType.Password 并不能让输入框完全变成密文,它在某些设备上默认会有一个"眼睛"图标用于切换明文显示。如果业务要求强制隐藏,需要额外处理。

Progress - 进度条的样式切换

Progress 组件在加载场景里非常常用,但很多人不知道它支持多种样式。

typescript 复制代码
@Entry
@Component
struct ProgressDemo {
  @State progressValue: number = 30

  build() {
    Column() {
      // 线性进度条
      Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear })
        .width(200)
        .color(Color.Blue)

      // 环形进度条
      Progress({ value: this.progressValue, total: 100, type: ProgressType.Ring })
        .width(80)
        .height(80)
        .color(Color.Green)

      // 胶囊型进度条
      Progress({ value: this.progressValue, total: 100, type: ProgressType.Capsule })
        .width(200)
        .height(40)
        .color(Color.Orange)

      Button('增加进度')
        .onClick(() => {
          if (this.progressValue < 100) {
            this.progressValue += 10
          }
        })
    }
    .justifyContent(FlexAlign.SpaceAround)
    .height('100%')
    .width('100%')
  }
}

需要注意Progresstype 属性在创建时设置后不能动态修改。如果需要在环形和线性之间切换,需要条件渲染两个不同的 Progress 组件。这个问题在官方文档里没有明确说明,属于实际踩坑经验。

常见问题 1:Button 的点击事件在组件频繁重建时失效

现象 :当页面的 @State 状态频繁变化时,Button 的 onClick 回调有时不触发。

原因 :ArkUI 的声明式渲染机制决定,每次状态变化会触发组件重建。如果 Button 的 onClick 闭包里引用了外部变量,且该变量在重建时被覆盖,可能导致回调函数被重新创建,旧的回调引用丢失。

解决方案 :确保 onClick 回调里不依赖组件的临时状态,直接修改 @State 变量是最稳妥的方式。如果确实需要复杂逻辑,建议使用 @Builder 构建按钮。

常见问题 2:Image 加载网络图片时出现白屏

现象:网络图片一直显示白色区域,无报错无日志。

原因 :最常见的原因是 ohos.permission.INTERNET 权限没有配置。其次可能是图片 URL 不支持 HTTPS,或者图片资源跨域。

解决方案

  1. 检查 module.json5 是否添加了 ohos.permission.INTERNET
  2. onError 回调里打印错误信息,判断是网络问题还是资源问题。
  3. 如果是 HTTP 协议,在 entry/src/main/resources/base/profile/network_config.json 中配置允许明文流量。

最佳实践

  1. 不要在 build() 中频繁创建对象 :比如 new Text('xxx') 这种写法,每次状态变化都会创建新的 Text 实例,导致 ArkUI 频繁触发组件重建。推荐直接使用字面量或 @Builder
  2. 推荐把状态集中到 @State@Observed 管理 :多个组件同步同一个数据时,如果各自维护本地 @State,很容易出现状态不一致。用 @Observed 装饰的数据模型可以统一管理。
  3. 异步回调里不要直接修改 UI 状态 :比如网络请求的回调里直接赋值给 @State 变量,如果在页面销毁后触发,会导致警告日志。建议在回调前判断 isActive() 状态。

FAQ

Q:为什么真机正常,模拟器不生效?

A:模拟器不支持某些硬件特性,比如摄像头、传感器权限。同时模拟器的网络请求权限配置和真机有差异,建议遇到奇怪问题先尝试真机测试。

Q:为什么页面返回后状态丢失?

A:当页面使用 @State 管理数据时,页面销毁后状态会丢失。如果需要保持状态,建议使用 @LocalStorage 或 AppStorage 持久化存储。

Q:为什么 Image 的 onError 回调里拿不到 HTTP 状态码?

A:这是 ArkUI 当前版本的限制。Image.onError 的回调参数只包含一个 error 字段,没有详细的状态信息。如果需要精确判断,建议封装一层网络请求先下载图片到本地,再用 Image 加载本地文件。