HarmonyOS ArkTS 自定义导航与视觉设计:从星空运势应用看顶部导航栏的动画编排与暗色主题实现
引子:底部 Tab 太丑,换成顶部导航
星空运势应用有三个页面:转盘、星图、塔罗。一开始用的是底部 Tab 导航,但放上去发现和暗色主题不搭——Tab 栏的白色背景在深色页面上特别突兀。
索性把导航移到顶部,用自定义的图标+文字+下划线实现。效果比底部 Tab 好很多,但实现起来有几个细节要注意:图标选中时的缩放动画、下划线的宽度过渡、和 Swiper 的联动。
完整效果
顶部导航栏的结构
导航栏分两部分:标题区和图标区。
┌─────────────────────────────┐
│ 塔罗牌阵 │ ← 标题(随页面切换)
│ 默念问题·翻开命运之牌 │ ← 副标题
├─────────────────────────────┤
│ 🎡 ✨ 🃏 │ ← 图标(选中放大)
│ 转盘运势 星座星图 塔罗占卜 │ ← 文字(选中变色)
│ ━━━━━━━ │ ← 下划线(选中显示)
└─────────────────────────────┘
标题区:动态切换
Column() {
Text(this.navTitle())
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E8DFF8')
.animation({ duration: 250 })
Text(this.navSub())
.fontSize(10).fontColor('#6B5B9A').margin({ top: 4 })
.animation({ duration: 250 })
}
.width('100%').alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 8 })

标题和副标题通过 this.page 状态动态切换:
navTitle(): string {
if (this.page === 0) { return '命运转盘'; }
if (this.page === 1) { return '星座星图'; }
return '塔罗牌阵';
}
animation({ duration: 250 }) 让标题切换时有渐变效果。250ms 是经过考虑的——太短看不清变化,太长会觉得卡顿。
图标区:三栏导航
Row() {
ForEach([0, 1, 2], (i: number) => {
Column() {
Text(this.navIcon(i))
.fontSize(22)
.opacity(i === this.page ? 1 : 0.4)
.scale({ x: i === this.page ? 1.15 : 1, y: i === this.page ? 1.15 : 1 })
.animation({ duration: 300, curve: Curve.EaseOut })
Text(this.navLabel(i))
.fontSize(11)
.fontWeight(i === this.page ? FontWeight.Bold : FontWeight.Normal)
.fontColor(i === this.page ? '#FFD700' : '#5B4FAF')
.animation({ duration: 250 })
.margin({ top: 6 })
Divider()
.width(i === this.page ? 24 : 0).height(2).color('#FFD700').borderRadius(1)
.margin({ top: 8 })
.animation({ duration: 300, curve: Curve.EaseOut })
}
.width(90).padding({ top: 8, bottom: 4 })
.onClick(() => { this.page = i; })
})
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly)

这个导航栏有三个视觉层次:图标、文字、下划线。每个层次在选中/未选中状态之间切换,带动画。
选中状态的三重反馈
选中状态通过三个维度反馈给用户:
1. 图标:透明度 + 缩放
Text(this.navIcon(i))
.fontSize(22)
.opacity(i === this.page ? 1 : 0.4)
.scale({ x: i === this.page ? 1.15 : 1, y: i === this.page ? 1.15 : 1 })
.animation({ duration: 300, curve: Curve.EaseOut })
- 选中:不透明(1)+ 放大 15%(1.15)
- 未选中:半透明(0.4)+ 原始大小(1)
为什么用 1.15 而不是 1.2?
放大 15% 刚好能感觉到变化,但不会太大导致布局抖动。1.2 会让图标明显"跳出",视觉上不协调。
2. 文字:粗细 + 颜色
Text(this.navLabel(i))
.fontSize(11)
.fontWeight(i === this.page ? FontWeight.Bold : FontWeight.Normal)
.fontColor(i === this.page ? '#FFD700' : '#5B4FAF')
.animation({ duration: 250 })
- 选中:加粗 + 金色
- 未选中:正常 + 暗紫色
金色(#FFD700)和暗紫色(#5B4FAF)是这个应用的主题色,选中状态用主题色突出,未选中用低饱和度的颜色弱化。
3. 下划线:宽度过渡
Divider()
.width(i === this.page ? 24 : 0).height(2).color('#FFD700').borderRadius(1)
.margin({ top: 8 })
.animation({ duration: 300, curve: Curve.EaseOut })
- 选中:宽度 24px
- 未选中:宽度 0(隐藏)
下划线的宽度动画是最明显的反馈——用户一眼就能看出哪个页面被选中。borderRadius(1) 让下划线两端圆润,不会太生硬。
为什么需要三重反馈?
单独一个维度(比如只改颜色)也能区分选中状态,但三重反馈的好处是:
- 容错性:即使用户色弱(看不清颜色差异),也能通过大小和下划线判断
- 层次感:三个维度同时变化,视觉冲击更强
- 专业感:细节到位的应用看起来更精致
Swiper 联动:导航栏和页面的同步
导航栏和 Swiper 的联动通过 this.page 状态实现:
// Swiper 的 onChange 回调
Swiper() { ... }
.onChange((idx: number) => { this.page = idx; })
// 导航栏的点击事件
.onClick(() => { this.page = i; })
双向绑定:
- 滑动 Swiper → 更新
this.page→ 导航栏高亮对应图标 - 点击导航栏 → 更新
this.page→ Swiper 切换到对应页面
这种"状态驱动"的联动方式比手动操作 DOM 简洁得多。
Swiper 的配置
Swiper() {
this.WheelPage();
this.StarPage();
this.TarotPage();
}
.index(this.page)
.indicator(false)
.loop(false)
.duration(300)
.onChange((idx: number) => { this.page = idx; })
.layoutWeight(1)
.width('100%')
.indicator(false):隐藏默认指示器(用自定义导航代替).loop(false):禁用循环滑动(三个独立页面不需要循环).duration(300):页面切换动画时长.layoutWeight(1):占据剩余空间
颜色系统:紫色 + 金色的设计逻辑
这个应用的颜色系统以紫色和金色为主调。为什么选这两个颜色?
紫色:神秘感
紫色在色彩心理学中代表神秘、灵性、魔法。星座和塔罗都和"神秘"相关,紫色是天然的选择。
代码中用了多种紫色:
| 颜色 | 用途 | 说明 |
|---|---|---|
| #06060F | 页面背景 | 最深的紫黑色 |
| #08081A | 导航栏背景 | 略浅的深紫色 |
| #0E0B25 | 卡片背景 | 中等深度的紫色 |
| #1C1545 | 选中背景 | 较亮的紫色 |
| #5B4FAF | 次要文字 | 中饱和度紫色 |
| #8B7BC0 | 辅助文字 | 浅紫色 |
| #D4C5F0 | 标题文字 | 亮紫色 |
| #E8DFF8 | 导航标题 | 最亮的紫色 |
从深到浅,形成了完整的紫色梯度。这种"同色系多梯度"的设计让界面有层次感,不会单调。
金色:高级感
金色代表财富、权力、高级感。在这个应用中,金色用于强调和高亮:
| 颜色 | 用途 |
|---|---|
| #FFD700 | 选中状态、主按钮、重要文字 |
| #F59E0B | 转盘指针、星座图标边框 |
金色和紫色是互补色,搭配在一起视觉冲击很强。但金色不能用太多,否则会显得"土豪"。代码中金色只用于关键元素(选中状态、按钮、指针),比例控制得当。
暗色主题的实现
暗色主题不是简单地把背景换成黑色,而是需要重新设计整个颜色体系:
- 背景色:不是纯黑 #000000,而是深紫 #06060F。纯黑在 OLED 屏幕上会导致"发光"效果
- 文字色:不是纯白 #FFFFFF,而是浅紫 #E8DFF8。纯白在深色背景上对比度太高,刺眼
- 边框色:用低饱和度的紫色 #1C1545,而不是灰色。保持色调统一
- 强调色:用金色 #FFD700,在深色背景上特别醒目
颜色的可访问性
暗色主题最容易犯的错误是对比度不够。代码中的颜色选择考虑了可访问性:
- 标题文字 #E8DFF8 在背景 #06060F 上的对比度约 12:1(远超 WCAG AA 标准的 4.5:1)
- 次要文字 #8B7BC0 在背景 #0E0B25 上的对比度约 5:1(刚好达标)
- 弱化文字 #4B3F70 在背景 #06060F 上的对比度约 3:1(低于标准,但用于装饰性文字可接受)
踩坑记录
坑 1:导航栏的高度不固定
导航栏的高度取决于标题和副标题的行数。如果标题太长换行,导航栏高度会变化,导致 Swiper 区域抖动。
解决方案是给导航栏设置固定高度,或者用 aspectRatio 限制比例。
坑 2:图标的缩放导致布局抖动
.scale(1.15) 会让图标占用更多空间,可能挤占相邻元素。代码中给每个导航项设置了固定宽度(90),避免了这个问题。
坑 3:动画时长不一致
图标动画 300ms,文字动画 250ms,下划线动画 300ms。这种微小的差异让用户感觉"有节奏",而不是所有元素同时变化。但如果差异太大(比如图标 500ms,文字 100ms),会显得混乱。
坑 4:Swiper 的预加载
Swiper 会预加载相邻页面,如果页面很复杂,可能导致内存占用过高。可以通过 .cachedCount(1) 控制预加载数量。
代码改进建议
1. 导航栏组件化
当前导航栏代码直接写在主组件里,如果要复用(比如在其他应用中),需要提取成独立组件:
@Component
struct TopNav {
@Prop page: number = 0;
@Prop titles: string[] = [];
@Prop icons: string[] = [];
@Prop labels: string[] = [];
onNavigate: (idx: number) => void = () => {};
build() { ... }
}
2. 颜色系统外部化
当前颜色值散落在代码各处。建议像骰子应用一样,用主题配置文件统一管理:
const Theme = {
bg: '#06060F',
surface: '#0E0B25',
primary: '#FFD700',
text: '#E8DFF8',
// ...
};
3. 导航栏的响应式设计
当前导航栏固定三栏,如果要扩展到四栏或五栏,需要修改布局。建议用 Grid 或 Flex 实现自适应。
4. 无障碍支持
当前导航栏没有无障碍属性。建议添加 accessibilityText,让屏幕阅读器能识别导航项。
总结
顶部导航栏和颜色系统是这个应用的"门面"——用户第一眼看到的就是导航栏和整体配色。好的导航栏让用户快速理解应用结构,好的颜色系统让用户愿意继续使用。
适用边界:这个部分适合用作 ArkUI 自定义导航和暗色主题的学习案例,涵盖了自定义导航栏、动画编排、颜色系统、Swiper 联动等核心知识点。但如果要上架应用商店,还需要补充无障碍支持、响应式设计、组件化、主题外部化等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。
说白了,导航栏就是"告诉用户在哪",颜色系统就是"让用户愿意待着"。两个都做好了,应用的"第一印象"就稳了。
更多推荐

所有评论(0)