1. Text组件的富文本与自定义样式组合

Text组件作为HarmonyOS ArkTS中最基础的文字展示组件,其实隐藏着不少高级玩法。在实际项目中,我们经常需要实现图文混排、局部文字高亮、点击事件绑定等复杂效果。下面我就分享几个实战中总结的技巧。

Span组件的嵌套使用 是Text组件的核心能力之一。通过Span,我们可以实现同一段文字中不同样式的混合展示。比如在电商App的商品详情页,经常需要展示原价和现价:

Text() {
  Span('原价:')
    .fontColor('#999')
    .fontSize(14)
  Span('¥299')
    .fontColor('#999')
    .fontSize(14)
    .decoration({type: TextDecorationType.LineThrough})
  Span(' 现价:')
    .fontColor('#f30')
    .fontSize(16)
  Span('¥199')
    .fontColor('#f30')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
}

这段代码会生成带删除线的原价和红色加粗的现价效果。我在实际开发中发现,Span的链式调用最多支持10层嵌套,超过这个限制会导致编译错误。解决方法是将长文本拆分成多个Text组件组合使用。

自定义文本装饰线 是另一个实用技巧。除了系统提供的下划线、删除线,我们还可以通过border属性模拟更多效果:

Text('限时特惠')
  .border({
    width: {bottom: 2},
    color: 0xFFFF0000,
    radius: 0,
    style: BorderStyle.Dotted
  })
  .padding({bottom: 5})

这个例子实现了红色虚线底纹效果,比单纯的实线下划线更显眼。在促销活动场景特别实用。

文本点击事件绑定 需要配合Span的onClick方法。我在开发新闻App时,需要实现话题标签的点击跳转:

Text() {
  Span('最新科技动态:')
  Span('#HarmonyOS4.0#')
    .fontColor('#1a73e8')
    .onClick(() => {
      router.pushUrl({
        url: 'pages/topic/detail',
        params: {tag: 'HarmonyOS4.0'}
      })
    })
}

这里有个坑要注意:Span的点击区域默认没有视觉反馈,最好加上背景色变化效果:

@State isPressed: boolean = false

Span('#HarmonyOS4.0#')
  .backgroundColor(this.isPressed ? '#f0f5ff' : '#00000000')
  .onClick(() => {...})
  .onTouch((event: TouchEvent) => {
    if(event.type === TouchType.Down) {
      this.isPressed = true
    } else if(event.type === TouchType.Up) {
      this.isPressed = false
    }
  })

2. Image组件的高效加载与错误处理

图片加载是移动应用的性能瓶颈之一,我在金融类App开发中就遇到过大量图片导致页面卡顿的问题。经过多次优化,总结出以下实战经验。

渐进式加载策略 能显著提升用户体验。ArkTS的Image组件支持placeholder属性,可以在图片完全加载前显示占位图:

Image('https://example.com/large-image.jpg')
  .width(300)
  .height(200)
  .placeholder($r('app.media.loading'))
  .transition({duration: 300, curve: Curve.EaseIn})

更高级的做法是结合Blur效果,先加载小图再过渡到大图:

@State isLoaded: boolean = false

Image(this.isLoaded ? 
  'https://example.com/high-res.jpg' : 
  'https://example.com/low-res.jpg')
  .blur(this.isLoaded ? 0 : 5)
  .onComplete(() => {
    this.isLoaded = true
  })

错误处理机制 必不可少。网络图片加载失败时,我们需要优雅的降级方案:

@State loadError: boolean = false

Image(loadError ? 
  $r('app.media.error') : 
  'https://example.com/unstable-image.jpg')
  .onError(() => {
    this.loadError = true
  })

对于重要图片,还可以加入重试逻辑:

@State retryCount: number = 0

Image(this.loadError && this.retryCount < 3 ?
  $r('app.media.retry') :
  'https://example.com/unstable-image.jpg')
  .onError(() => {
    if(this.retryCount < 3) {
      setTimeout(() => {
        this.retryCount++
        this.loadError = false
      }, 1000)
    } else {
      this.loadError = true
    }
  })

内存优化技巧 也很关键。对于长列表中的图片,建议:

  1. 使用合适的图片格式:WebP通常比PNG小30%
  2. 设置准确的宽高避免布局重排
  3. 对于离开可视区域的图片使用loading="lazy"
  4. 列表滑动时暂停图片加载
List() {
  ForEach(this.items, (item) => {
    ListItem() {
      Image(item.url)
        .width(120)
        .height(120)
        .loadingStrategy(LoadingStrategy.Lazy)
    }
  })
}
.onScrollStop(() => {
  // 恢复图片加载
})

3. TextInput组件在复杂表单中的联动与验证

表单开发是App中最常见的场景之一,TextInput作为核心输入组件,其高级用法值得深入探讨。

动态输入验证 能即时反馈用户输入状态。比如手机号输入框:

@State mobile: string = ''
@State mobileValid: boolean = false

TextInput({placeholder: '请输入手机号'})
  .width('100%')
  .onChange((value: string) => {
    this.mobile = value
    this.mobileValid = /^1[3-9]\d{9}$/.test(value)
  })
  .border({
    color: this.mobileValid ? '#67C23A' : 
          (this.mobile ? '#F56C6C' : '#DCDFE6'),
    width: 1
  })

表单联动校验 在注册流程中很常见。比如密码和确认密码的一致性检查:

@State password: string = ''
@State confirm: string = ''
@State pwdMatch: boolean = true

Column() {
  TextInput({placeholder: '设置密码'})
    .type(InputType.Password)
    .onChange((value) => {
      this.password = value
      this.checkMatch()
    })
  
  TextInput({placeholder: '确认密码'})
    .type(InputType.Password)
    .onChange((value) => {
      this.confirm = value
      this.checkMatch()
    })
  
  if(!this.pwdMatch) {
    Text('两次输入密码不一致')
      .fontColor('#F56C6C')
      .fontSize(12)
  }
}

checkMatch() {
  this.pwdMatch = this.password === this.confirm
}

输入法适配 是容易被忽视的细节。在开发多语言App时,需要特别关注:

TextInput()
  .enterKeyType(EnterKeyType.Search) // 键盘回车键样式
  .inputMethod(InputMethod.Number) // 限制数字键盘
  .onAccept((value) => {
    // 处理回车键事件
  })

对于搜索框,还可以加入防抖优化:

@State searchText: string = ''
timer: number = 0

TextInput({placeholder: '搜索'})
  .onChange((value) => {
    this.searchText = value
    clearTimeout(this.timer)
    this.timer = setTimeout(() => {
      this.doSearch()
    }, 500)
  })

4. Button组件的状态管理与交互反馈

按钮作为最重要的交互组件,其状态管理和动效设计直接影响用户体验。

多状态样式管理 可以通过@Styles实现统一维护:

@Styles function primaryButton() {
  .width('80%')
  .height(40)
  .borderRadius(20)
  .stateEffect(true)
}

@Styles function disabledButton() {
  .opacity(0.6)
  .stateEffect(false)
}

Button('提交')
  .primaryButton()
  .enabled(!this.isLoading)
  .disabledButton(!this.isLoading)
  .onClick(() => {
    this.submitForm()
  })

加载状态反馈 能有效防止重复提交:

@State isLoading: boolean = false

Button(this.isLoading ? '' : '提交')
  .width(120)
  .onClick(() => {
    this.isLoading = true
    this.submit().finally(() => {
      this.isLoading = false
    })
  })
  .enabled(!this.isLoading)
  .backgroundColor(this.isLoading ? '#ccc' : '#007DFF')
  
if(this.isLoading) {
  LoadingProgress()
    .color(Color.White)
    .width(20)
    .height(20)
}

高级点击效果 可以提升交互体验。比如水波纹效果:

@State rippleX: number = 0
@State rippleY: number = 0
@State rippleShow: boolean = false

Button('点击我')
  .overlay(
    this.rippleShow ? 
      Circle()
        .width(200)
        .height(200)
        .position({x: this.rippleX, y: this.rippleY})
        .backgroundColor('#ffffff40')
        .transition({duration: 600, curve: Curve.Friction})
    : null
  )
  .onTouch((event) => {
    if(event.type === TouchType.Down) {
      this.rippleX = event.touches[0].x
      this.rippleY = event.touches[0].y
      this.rippleShow = true
    } else if(event.type === TouchType.Up) {
      setTimeout(() => {
        this.rippleShow = false
      }, 600)
    }
  })

按钮组合技巧 在工具栏场景很实用。比如通过RowSplit实现可调整大小的按钮组:

RowSplit() {
  Button('撤销')
    .layoutWeight(1)
  
  Divider()
    .vertical(true)
    .height('80%')
  
  Button('重做')
    .layoutWeight(1)
}
.height(40)
.borderRadius(4)
.border({width:1, color:'#ddd'})

在实际项目中,我还发现Button的点击热区经常需要调整,可以通过padding扩大点击区域:

Button('小按钮')
  .width(60)
  .height(30)
  .padding(10) // 扩大点击区域
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐