引子:导航栏的"差不多"和"刚刚好"

其实 Index.ets 里的导航栏代码经过了好几轮调优——图标的缩放比例从 1.15 改到 1.2,透明度从 0.4 改到 0.3,动画时长从 300ms 改到 350ms,下划线从内联改成了独立的 Row + Blank 结构。

每次改动都不大,但叠在一起差别明显。导航栏是用户一进应用就看到的东西,做好了是"刚好舒服",做不好是"说不上来但就是不对"。

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

在这里插入图片描述

代码结构总览

Index.ets 的导航栏分四个区域:

┌─────────────────────────────────┐
│         命运转盘                 │  ← 标题区(居中)
│   转动转盘 · 揭晓今日运势       │  ← 副标题
├─────────────────────────────────┤
│  🎡      ✨      🃏             │  ← 图标区(三栏)
│  运势      星图      塔罗        │  ← 文字标签
├─────────────────────────────────┤
│    ━━━━━                       │  ← 下划线区(独立 Row)
└─────────────────────────────────┘

对应代码的四个 Column / Row 块,逐个分析。

标题区:居中对齐与呼吸感

Column() {
  Column() {
    Text(this.navTitle())
      .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E8DFF8')
      .animation({ duration: 300 })
    Text(this.navSub())
      .fontSize(10).fontColor('#6B5B9A').margin({ top: 6 })
      .animation({ duration: 300 })
  }
  .width('100%').alignItems(HorizontalAlign.Center)
  .padding({ top: 14, bottom: 12 })
}

在这里插入图片描述

为什么嵌套两层 Column?

外层 Column 是导航栏的容器,内层 Column 专门放标题和副标题。内层设了 alignItems(HorizontalAlign.Center) 让文字居中,外层负责整体布局。

如果不嵌套,直接在内层 Column 上设居中,和下面的图标区会冲突——图标区需要 justifyContent(FlexAlign.SpaceEvenly) 分散排列,不能居中。

padding 的选择

padding({ top: 14, bottom: 12 }) 给标题区留了呼吸空间。top 比 bottom 大 2px,是因为标题文字本身有下伸部(比如字母 g、y 的尾部),底部留少一点视觉上更平衡。

动画时长

标题切换动画 300ms。这个时长是反复试过的——200ms 太快看不清变化,400ms 太慢觉得卡。300ms 刚好能感知到文字从一个变成另一个,又不会让用户等待。

图标区:缩放、透明度与动画曲线

Row() {
  ForEach([0, 1, 2], (i: number) => {
    Column() {
      Text(this.navIcon(i))
        .fontSize(24)
        .scale({ x: i === this.page ? 1.2 : 1, y: i === this.page ? 1.2 : 1 })
        .opacity(i === this.page ? 1 : 0.3)
        .animation({ duration: 350, curve: Curve.EaseOut })
      Text(this.navLabel(i))
        .fontSize(10)
        .fontWeight(i === this.page ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(i === this.page ? '#FFD700' : '#5B4FAF')
        .animation({ duration: 250 })
        .margin({ top: 8 })
    }
    .width(100).padding({ top: 8, bottom: 4 })
    .onClick(() => { this.page = i; })
  })
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly)

在这里插入图片描述

缩放比例:1.2 而不是 1.15

选中时图标放大 20%(1.2),未选中保持原始大小(1)。之前试过 1.15,但和 1 差别太小,用户不容易感知。1.2 是"明显但不夸张"的临界点——再大到 1.3 就会挤占相邻图标的空间。

透明度:0.3 而不是 0.4

未选中图标的透明度从 0.4 降到了 0.3。0.4 太亮,和选中图标的差距不够大,用户需要仔细看才能分辨哪个选中了。0.3 让未选中图标几乎"退到背景里",选中图标的视觉权重更突出。

动画曲线:EaseOut

图标动画用了 Curve.EaseOut——开始快,结束慢。这符合物理直觉:选中状态的切换是"快速聚焦",不是"慢慢移动"。如果用 Curve.Linear(匀速),图标缩放会显得机械;如果用 Curve.EaseInOut(两端减速),开始也慢,感觉不够果断。

动画时长:350ms vs 250ms

图标动画 350ms,文字动画 250ms。为什么不一样?

  • 图标有缩放动画,变化幅度大(1 → 1.2),需要更长时间让用户感知
  • 文字只有颜色和粗细变化,变化幅度小,250ms 足够

如果都用 350ms,文字变化会"拖";如果都用 250ms,图标变化会"急"。错开时长让动画有节奏感。

固定宽度 100

每个导航项设了 width(100),确保三栏等宽。如果不设宽度,文字长度不同会导致栏宽不一致("运势"比"塔罗"短),视觉上参差不齐。

下划线区:独立 Row 的精妙之处

Row() {
  ForEach([0, 1, 2], (i: number) => {
    Divider()
      .width(28).height(2).color('#FFD700').borderRadius(1)
      .opacity(i === this.page ? 1 : 0)
      .animation({ duration: 300, curve: Curve.EaseOut })
    Blank()
  })
}
.width('60%').margin({ top: 6 })

为什么下划线要独立成 Row?

之前的版本把下划线放在图标区的 Column 里,和图标、文字混在一起。问题是要控制下划线的对齐比较麻烦——图标区用 SpaceEvenly 分散排列,下划线也跟着分散,但下划线应该居中对齐在图标下方。

独立成 Row 后,下划线有自己的布局空间,可以用 width('60%') 控制整体宽度,用 Blank() 控制间距。

Blank() 的作用

Blank() 是 ArkUI 的弹性空白组件,会自动填充剩余空间。在 ForEach 里用 Divider() + Blank() 的组合:

[Divider] [Blank] [Divider] [Blank] [Divider]

三个 Divider 被两个 Blank 分隔,自动居中分布在 Row 里。不需要手动计算间距。

下划线的样式

  • width(28):固定宽度 28px,太宽会显得笨重,太窄看不清
  • height(2):高度 2px,细线更有精致感
  • borderRadius(1):圆角 1px,避免直角的生硬
  • color('#FFD700):金色,和选中文字颜色一致

opacity 动画

下划线的显示/隐藏用 opacity 动画而不是 width 动画。原因是:

  • opacity 动画:淡入淡出,视觉柔和
  • width 动画:从 0 到 28 的伸展,视觉生硬

选中时 opacity 从 0 变 1(淡入),切换时从 1 变 0(淡出),配合 300ms 的 EaseOut 曲线,效果很自然。

margin({ top: 6 })

下划线和图标区之间留 6px 间距。太近会和图标粘在一起,太远会和图标脱节。6px 是反复调整后的经验值。

整体布局的层次

导航栏整体用 Column 从上到下排列三个区域:

Column (导航栏容器)
├── Column (标题区) — 居中对齐
├── Row (图标区) — 等宽分散
└── Row (下划线区) — 60% 宽度居中

背景色的选择

导航栏背景 #08081A,页面背景 #06060F。两者都是深紫色,但导航栏略浅,形成微妙的层次感。如果用同一个颜色,导航栏和页面内容会融为一体,用户分不清边界。

padding({ top: 6, bottom: 2 })

导航栏整体的 padding 很小——top 6px,bottom 2px。这是因为导航栏本身已经有内部 padding(标题区 14/12,图标区 8/4),外部只需要微调。bottom 特别小是因为下划线本身就有 margin,再加 padding 会显得太松。

动画编排的节奏感

导航栏的动画不是同时触发的,而是有先后顺序:

  1. 图标缩放:350ms,EaseOut
  2. 文字颜色:250ms
  3. 下划线淡入:300ms,EaseOut

用户点击导航项时,先看到图标放大(最明显的视觉变化),然后文字变色,最后下划线出现。这种"主次分明"的动画编排比所有元素同时变化更有层次感。

踩坑记录

坑 1:ForEach 里的 Blank() 不能省

如果把 Blank() 去掉,三个 Divider 会紧挨在一起,视觉上像一条长线而不是三条短线。Blank() 的弹性填充是实现等间距排列的关键。

坐 2:下划线的 width 不能用百分比

width(28) 是固定值,不能用 width('20%') 之类的百分比。因为百分比是相对于父容器的,而父容器宽度是 60%,计算出来的下划线宽度会很小(屏幕宽度 × 60% × 20% ≈ 40px),视觉效果不好。

坑 3:scale 的 x 和 y 必须同时设置

.scale({ x: 1.2, y: 1.2 }) 不能写成 .scale(1.2)(ArkUI 不支持简写)。如果只设 x 不设 y,图标会变形。

坑 4:导航栏高度不固定

导航栏的高度取决于内容,没有设固定高度。如果标题太长换行,导航栏会变高,导致 Swiper 区域抖动。解决方案是给标题设 maxLines: 1textOverflow: TextOverflow.Ellipsis

与之前版本的对比

属性旧版本新版本改动原因
图标缩放1.151.2差别更明显
未选中透明度0.40.3选中/未选中对比更强
图标动画时长300ms350ms缩放幅度大,需要更长时间
下划线实现内联在图标 Column独立 Row + Blank布局更灵活
下划线宽度24px28px稍宽一点更醒目
导航栏背景#08081A#08081A保持一致

代码改进建议

1. 导航数据外部化

当前导航的图标、标签都写在 navIcon()navLabel() 函数里。如果以后要加第四个导航项(比如"设置"),需要同时改三个地方。建议用数组统一管理:

const NAV_ITEMS = [
  { icon: '🎡', label: '运势' },
  { icon: '✨', label: '星图' },
  { icon: '🃏', label: '塔罗' },
];

2. 动画参数常量化

动画的时长、曲线散落在各处,如果要统一调整(比如所有动画改成 400ms),需要逐个修改。建议提取成常量:

const ANIM_DURATION_FAST = 250;
const ANIM_DURATION_NORMAL = 300;
const ANIM_DURATION_SLOW = 350;
const ANIM_CURVE = Curve.EaseOut;

3. 导航栏组件化

当前导航栏代码直接写在 Index.ets 里,如果要复用(比如在详情页加一个返回导航栏),需要提取成独立组件。

4. 无障碍支持

当前导航栏没有 accessibilityText 属性,屏幕阅读器无法识别导航项。建议添加:

Text(this.navIcon(i))
  .accessibilityText(this.navLabel(i))

总结

Index.ets 的导航栏代码看似简单,但每个属性都经过了反复调优——缩放比例、透明度、动画时长、布局结构。这些细节单独看差别不大,但叠在一起决定了用户的第一印象。

导航栏的核心是"视觉层次":选中图标最突出(放大+不透明),未选中图标退后(缩小+半透明),下划线提供位置确认(金色+淡入)。三个层次配合动画节奏,让用户一眼就知道"我在哪"。

适用边界:这个部分适合用作 ArkUI 导航栏精细化设计的学习案例,涵盖了动画曲线、视觉层次、布局结构、交互细节等核心知识点。但如果要上架应用商店,还需要补充无障碍支持、组件化、数据外部化、响应式布局等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

Logo

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

更多推荐