点一下,变个色——我用 HarmonyOS 的 Text 和 onClick 做了个轻巧的选项卡

前言

你有没有注意过,很多 App 的顶部都有一排选项卡?比如新闻 App 的"推荐""热点""科技",电商 App 的"首页""分类""购物车"。这些选项卡有一个共同的特点:点一下某个标签,它就会高亮变色,同时其他标签恢复默认状态。这个交互叫"选项卡切换",是所有 UI 设计师工具箱里最基础的模式之一。

通常,实现选项卡需要用到复杂的容器或专门的控件。但在 HarmonyOS 里,有一个更轻巧的方式:就用普通的 Text 组件,加上 onClick 事件,配合 @State 状态管理,几行代码就能搭出一个漂亮的选项卡。不用 TabBar,不用额外的依赖,干净利落。上周我在 Pura X Max 模拟器上花了半小时做这件事,这篇文章就把整个过程完整记录下来------不止是代码,更想聊聊状态管理、条件渲染和"互斥选择"这个经典交互模式的实现原理。

一、选项卡的本质------一个整数状态和一组条件渲染

别看选项卡外表复杂,它在代码层面其实只做了一件事:维护一个整数变量,用来记录当前选中的是第几个标签。 这个整数可以是 0、1、2,分别代表第一个、第二个、第三个标签。当用户点击某个标签时,这个整数就更新为对应的索引值。界面上的所有样式变化,都是根据这个整数来判断的:如果当前选中的索引等于某个标签的索引,这个标签就显示"选中"样式(比如蓝色背景、白色文字);否则显示"未选中"样式(比如灰色背景、黑色文字)。

这个过程用 ArkUI 实现起来非常直观。我们定义一个 @State selectedIndex: number = 0,初始为 0,表示默认选中第一个标签。然后在模板里用 ForEach 遍历标签数组,根据 selectedIndex === index 来决定每个 Text 组件的样式。当用户点击某个 Text 时,在 onClick 回调里更新 selectedIndex 为这个标签的索引。

由于 @State 变量的变化会自动触发 UI 刷新,所以当 selectedIndex 更新后,所有 Text 的样式都会重新计算。原本高亮的旧标签会变回默认样式,被点击的新标签会获得高亮样式。这个切换是瞬间完成的,用户看到的就是"点了哪个,哪个就亮起来"。

这种"一个状态变量驱动多个子组件的条件样式"的模式,是声明式 UI 最核心的思维方式。你不需要写"把旧标签恢复原样,把新标签点亮"这种命令式代码,只需要描述"当选中索引等于 X 时,样式应该是 Y",框架会自动处理新旧状态的切换。

二、让标签看起来像标签------样式设计的细节

选项卡的视觉设计有几个原则:

  1. 选中状态要醒目:和未选中状态有足够对比度,通常用深色背景、亮色文字,或者加下划线、加粗。
  2. 未选中状态要低调:但也不能太暗淡,否则用户会以为它不可用。灰色文字、浅背景或透明背景即可。
  3. 标签之间要有界限感:要么用间距隔开,要么用分割线,要么每个标签是一个独立的圆角胶囊。
  4. 整体要紧凑:选项卡通常只有一行,标签不宜太多(3~6 个为宜),文字要简短。

在我们的 Demo 里,我选择了"胶囊"风格的选项卡:每个标签是一个独立的圆角矩形,未选中时灰色背景黑色文字,选中后变成蓝色背景白色文字,同时字体变粗。标签之间保持 8vp 的间距,整体放在一个 Row 里,用 padding 左右留白,看起来干净整齐。

这种样式可以用非常简洁的 ArkUI 属性实现:backgroundColorfontColor 分别控制背景和文字颜色,borderRadius 控制圆角,padding 给标签内部留出呼吸空间,margin 控制标签之间的间距。所有属性都根据 selectedIndex === index 的条件来动态赋值。比如背景色:

复制代码
backgroundColor: selectedIndex === index ? '#1976D2' : '#EEEEEE'

这种三元表达式在声明式 UI 里大量使用,简洁明了。

为了让交互更丰富,我还给每个标签添加了一个 onClick 回调,点击时除了更新 selectedIndex,还可以加入微小的动效。但在 ArkUI 中,要加动画需要用 animateTo 函数,稍微复杂一些。这个 Demo 里我们保持纯粹的点击切换,不加动画,体验已经很好。如果需要动画,可以用 animateTo({ duration: 200 }, () => { this.selectedIndex = index; }) 来实现背景色的平滑过渡。

三、选项内容也可以跟着变------不只是标签样式

选项卡不只是标签本身在变,通常它的下方内容区域也会跟着切换。比如新闻 App 里,点"热点"标签,下面展示热点新闻列表;点"科技",下面换成科技新闻列表。这就是"标签页"的完整形态。

实现内容区切换同样非常简单:在标签行下方,根据 selectedIndex 的值,用条件渲染显示不同的内容组件。比如:

复制代码
if (this.selectedIndex === 0) {
  Text('这是推荐内容').fontSize(16)
} else if (this.selectedIndex === 1) {
  Text('这是热点内容').fontSize(16)
} else {
  Text('这是科技内容').fontSize(16)
}

这样,用户点击标签时,不仅标签样式变了,下方内容也跟着刷新,形成一个完整的选项卡交互闭环。在 Demo 里,我加了一个简单的内容区,展示一个带背景色的卡片,上面写着当前选中的标签名称。这让整个 Demo 看起来更完整,不只是一排孤零零的文字。

四、完整代码------一个轻量级的选项卡组件

以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,把 entry/src/main/ets/pages/Index.ets 全部替换即可。无需任何权限,纯 UI 交互。

复制代码
/*
 * 点击文字变色选中 --- Text + onClick 选项卡效果
 * 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
 */

@Entry
@Component
struct Index {
  @State selectedIndex: number = 0; // 当前选中的标签索引
  private tabs: string[] = ['推荐', '热点', '科技', '财经', '娱乐'];

  build() {
    Column() {
      // 标题
      Text('选项卡切换')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 10 })

      Text('点击标签切换内容与样式')
        .fontSize(15)
        .fontColor('#888')
        .margin({ bottom: 20 })

      // 标签行
      Row() {
        ForEach(this.tabs, (tab: string, index: number) => {
          Text(tab)
            .fontSize(16)
            .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.selectedIndex === index ? '#FFFFFF' : '#555555')
            .backgroundColor(this.selectedIndex === index ? '#1976D2' : '#EEEEEE')
            .borderRadius(20)
            .padding({ left: 16, right: 16, top: 8, bottom: 8 })
            .margin({ right: index === this.tabs.length - 1 ? 0 : 8 })
            .onClick(() => {
              this.selectedIndex = index;
            })
        })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 25 })

      // 内容区域
      Column() {
        Text(`📌 ${this.tabs[this.selectedIndex]} 内容区`)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333')
          .margin({ bottom: 8 })

        Text(this.getContent(this.selectedIndex))
          .fontSize(16)
          .fontColor('#666')
          .lineHeight(24)
          .textAlign(TextAlign.Center)
      }
      .width('88%')
      .height(200)
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .shadow({ radius: 8, color: '#20000000', offsetY: 4 })
      .justifyContent(FlexAlign.Center)

      Text('💡 使用 @State 管理选中索引,点击切换标签样式和内容')
        .fontSize(12)
        .fontColor('#AAA')
        .width('90%')
        .textAlign(TextAlign.Center)
        .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 根据选中索引返回模拟内容
  private getContent(index: number): string {
    let contents = [
      '这是推荐频道的内容,为你精选优质文章。',
      '热点频道,实时追踪最新话题。',
      '科技频道,探索前沿技术动态。',
      '财经频道,把握市场脉搏。',
      '娱乐频道,放松你的心情。'
    ];
    return contents[index] || '';
  }
}

代码分为三部分:标签数据定义、标签行渲染、内容区渲染。标签行用 ForEach 遍历 tabs 数组,每个 Text 根据 selectedIndex === index 切换样式。内容区用 getContent 方法返回不同文字,展示在不同标签下的内容变化。整个交互只依赖一个 @State 变量。

五、运行效果

把代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上方是一行五个胶囊形状的标签,默认"推荐"标签蓝色高亮,其余灰色。点击"科技",蓝色高亮立刻转移到"科技"标签上,下方内容区同步变成"科技频道,探索前沿技术动态"。再点"娱乐",标签和内容再次切换。整个切换过程瞬间完成,标签样式变化干净利落,内容区卡片保持圆角阴影。手指依次点过五个标签,像在调收音机的频道,手感清脆,视觉反馈明确。

总结

这个小小的选项卡,虽然只有几十行代码,却把声明式 UI 开发的精髓展现得淋漓尽致:

  • @State 状态驱动 :一个整数变量 selectedIndex 是整个交互的核心,所有样式变化和内容切换都由它驱动。
  • 条件渲染与动态样式:通过三元表达式和条件判断,让 UI 自动响应状态变化,无需手动操作 DOM。
  • ForEach 简化重复组件:用数据数组遍历生成标签行,便于扩展。如果以后要加新标签,只需在数组里加一行即可,不用改模板。
  • 组件复用思维 :同样是 Text 组件,通过不同的属性配置,实现了完全不同的视觉效果和交互行为。

选项卡是移动端界面里最常见的模式之一,但理解它背后的原理后,你会发现它不过是一个整数状态加几行条件样式。这种"状态-视图"的映射关系,是声明式 UI 最迷人的地方。下次你在任何 App 里看到选项卡,也许会想:这背后,大概就是一个 selectedIndex 和几个三元表达式吧。