【OpenHarmony/HarmonyOs 】数学视界实战:悬浮导航栏、沉浸光感与全新交互体验

项目类型:OpenHarmony / HarmonyOS ArkTS 数学学习应用
项目名称:数学视界
关键词:ArkUI、Tabs、沉浸式视觉、深色模式、底部导航、点击动效、响应式布局 ✨

一、为什么要做“轻沉浸”的数学学习体验?

数学类应用很容易做成“工具集合”:计算器、公式表、单位换算、题库练习各自独立,页面之间缺少情绪连接。我的这个项目希望让用户打开应用时,不只是看到一堆功能按钮,而是进入一个有节奏、有反馈、有学习目标的空间。

所以在 UI 设计上,我重点做了三件事:

  • 🌈 用暖色光感做首页视觉入口,让学习氛围更轻松;
  • 🧭 用底部导航承载高频页面:首页、挑战、成就、收藏、我的;
  • 👆 给按钮、卡片、弹窗都加上统一点击反馈,让每一次操作都有回应。

最终效果上,应用并不是简单堆 ArkUI 组件,而是围绕“今日目标 -> 功能探索 -> 学习数据 -> 成就反馈”形成一个完整体验闭环。

二、项目首页结构:Tabs 承载五大核心场景

项目的主入口在 entry/src/main/ets/pages/Index.ets,首页没有使用多个独立 Ability,而是通过 Tabs 把几个主要场景组织到同一个主页面里。

核心结构如下:

Tabs({ barPosition: BarPosition.End, controller: this.tabController }) {
  TabContent() {
    this.buildHomePage()
  }
  .tabBar(this.buildBottomTab(0, '🏠', '首页'))

  TabContent() {
    this.buildChallengeTab()
  }
  .tabBar(this.buildBottomTab(1, '🎯', '挑战'))

  TabContent() {
    this.buildAchievementTab()
  }
  .tabBar(this.buildBottomTab(2, '🏆', '成就'))

  TabContent() {
    this.buildFavoritesTab()
  }
  .tabBar(this.buildBottomTab(3, '💖', '收藏'))

  TabContent() {
    MyPage()
  }
  .tabBar(this.buildBottomTab(4, '👤', '我的'))
}
.barHeight(56)
.barBackgroundColor(this.getColor('#FFFDF7', '#1C1C1E'))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

这里有几个细节很关键:

  1. barPosition: BarPosition.End:把导航固定在底部,更符合移动端单手操作习惯。
  2. TabsController:为后续主动切换 Tab 留出扩展空间。
  3. expandSafeArea:处理底部安全区域,让导航不会被系统手势区域遮挡。
  4. 自定义 tabBar:不用系统默认样式,而是自己绘制图标、文字、角标。

三、底部导航栏:不只是切换页面,还要承担状态表达

数学视界的底部导航有一个小设计:收藏 Tab 会显示收藏数量角标。这样用户不用进入收藏页,也能知道自己积累了多少内容。

@Builder
buildBottomTab(index: number, icon: string, title: string) {
  Column({ space: 2 }) {
    Stack({ alignContent: Alignment.Center }) {
      Text(icon)
        .fontSize(24)

      if (index === 3 && AppState.favorites.length > 0) {
        Text(AppState.favorites.length > 99 ? '99+' : AppState.favorites.length.toString())
          .fontSize(9)
          .fontColor('#FFFFFF')
          .backgroundColor(this.isDarkMode ? '#FF7A8A' : '#FF6B9D')
          .borderRadius(10)
          .padding({ left: 3, right: 3, top: 1, bottom: 1 })
          .offset({ x: 14, y: -10 })
      }
    }

    Text(title)
      .fontSize(11)
      .fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
      .fontColor(this.currentIndex === index ? getThemeColors().primary : this.getColor('#AAAAAA', '#666666'))
      .textAlign(TextAlign.Center)
  }
}

这个写法的好处是:

  • 当前选中态通过字重和颜色表达,用户不会迷路;
  • 收藏数量直接关联 AppState.favorites.length,数据变化会反映到 UI;
  • 深色模式下角标颜色也会调整,不会出现浅色主题好看、深色主题刺眼的问题。

四、沉浸光感:从首页头图到进度卡片

首页最醒目的区域是顶部标题和今日进度。它们都使用了比较明亮的暖粉色,形成统一的品牌感。

Text('🌟 数学视界')
  .fontSize(this.isLargeScreen ? 26 : 22)
  .fontWeight(FontWeight.Bold)
  .fontColor('#FFFFFF')

Text('探索数学的奇妙世界')
  .fontSize(this.isLargeScreen ? 14 : 12)
  .fontColor('rgba(255,255,255,0.85)')

进度卡片则把“今日目标”直接视觉化:

Column()
  .width(Math.min(AppState.studyData.todayCount / AppState.studyData.dailyGoal * 100, 100) + '%')
  .height('100%')
  .backgroundColor('#FFFFFF')
  .borderRadius(10)

这里我没有把学习数据藏在“我的”页面里,而是放在首页首屏。原因很简单:学习类产品最重要的是持续激励,用户一打开就应该知道“今天还差多少”。

页面文案也做了轻量反馈:

Text(
  AppState.studyData.todayCount >= AppState.studyData.dailyGoal
    ? '🎉 太棒了!今日目标达成!'
    : '💪 再完成 ' + Math.max(AppState.studyData.dailyGoal - AppState.studyData.todayCount, 0) + ' 次即可达成今日目标!'
)

这类微文案虽然代码很短,但对学习应用的体验很重要。它让进度条不只是一个数字,而是一个“被鼓励”的瞬间。

五、统一点击动效:让交互有手感

项目中很多按钮、卡片、弹窗关闭按钮都用了同一套点击反馈。核心思路是:点击时记录当前元素的 id,短时间内让该元素缩小和降低透明度。

pulseAnim(id: string): void {
  this.animId = id
  if (this.animTimer >= 0) clearTimeout(this.animTimer)
  this.animTimer = setTimeout((): void => {
    this.animId = ''
  }, 200) as number
}

animScale(id: string): number {
  return this.animId === id ? AnimScale.PRESSED : 1
}

animAlpha(id: string): number {
  return this.animId === id ? AnimAlpha.PRESSED : 1
}

在功能卡片上使用:

.scale({ x: this.animScale('mod_' + title), y: this.animScale('mod_' + title) })
.opacity(this.animAlpha('mod_' + title))
.animation({ duration: AnimDuration.NORMAL, curve: Curve.EaseOut })
.onClick(() => {
  this.pulseAnim('mod_' + title)
  action()
})

体验上会变成这样:

  • 用户点卡片时,卡片轻微下压;
  • 点击反馈在 200ms 左右结束;
  • 动画不会抢戏,但会让页面更“活”。

我把这类动效抽到了 AnimationUtils.ets,统一维护时长、缩放值和透明度:

export const AnimDuration = {
  FAST: 100,
  NORMAL: 150,
  SLOW: 300,
  VERY_SLOW: 500,
}

export const AnimScale = {
  PRESSED: 0.93,
  SMALL: 0.95,
  LARGE: 1.05,
}

export const AnimAlpha = {
  PRESSED: 0.82,
  DISABLED: 0.5,
}

这样后续新增页面时,不需要每个页面都重新想一套动画参数,整套应用的手感会保持一致。

六、深色模式:不是简单反色,而是主题系统

项目中有一个统一的 ThemeManager,用于管理浅色主题、深色主题、图标背景映射和响应式工具。

export const LightTheme: ThemeColors = {
  primary: '#FF7A8A',
  background: '#FFF8F0',
  surface: '#FFFFFF',
  textPrimary: '#2C3E50',
  tabBarBackground: '#FFFDF7',
}

export const DarkTheme: ThemeColors = {
  primary: '#FF7A8A',
  background: '#000000',
  surface: '#1C1C1E',
  textPrimary: '#F2F2F7',
  tabBarBackground: '#1C1C1E',
}

深色模式最容易踩坑的地方是图标背景。浅色模式下很多柔和色块很舒服,但放到黑色背景上会显得发灰或发脏。因此项目里单独维护了图标背景映射:

export function getIconBgColor(lightColor: string, isDark: boolean): string {
  if (!isDark) return lightColor
  const mapping = IconBgColorMap[lightColor]
  if (mapping) return mapping.dark
  return adjustColorForDark(lightColor)
}

这样首页、我的页面、挑战页都可以复用:

getIconBg(bgColor: string): string {
  return getIconBgColor(bgColor, this.isDarkMode)
}

七、状态栏同步:让系统区域也融入视觉

EntryAbility.ets 中,应用会读取系统配置并更新主题状态,同时设置状态栏颜色。

onConfigurationUpdate(newConfig: Configuration): void {
  ThemeManager.getInstance().applyColorMode(newConfig.colorMode)
  this.applyStatusBar()
}

private applyStatusBar(): void {
  if (this.mainWindow === null) return
  const isDark = ThemeManager.getInstance().isDark()
  this.mainWindow.setWindowSystemBarProperties({
    statusBarColor: isDark ? '#1C1C1E' : '#FF9A8B',
    statusBarContentColor: '#FFFFFF'
  })
}

这一步会让应用不只是内容区变色,连状态栏也与页面主色协调起来。对于“沉浸光感”来说,这种细节非常重要。

八、响应式适配:手机和平板都要能用

项目在 ThemeManager.ets 中封装了 ResponsiveUtils

export class ResponsiveUtils {
  private static readonly BASE_WIDTH = 375

  static getScreenWidth(): number {
    return getScreenSize().width
  }

  static isLargeScreen(): boolean {
    return getScreenSize().width >= 600
  }

  static getGridColumns(): number {
    return ResponsiveUtils.isLargeScreen() ? 4 : 2
  }
}

首页里根据屏幕宽度调整字号、间距和内容宽度:

get isLargeScreen() {
  return this.screenWidth >= 600
}

这让应用在手机上保持紧凑,在平板上则不会显得内容过小。

九、实现总结

这篇文章对应的主题是“悬浮导航栏、沉浸光感、全新视觉与交互体验”。在数学视界项目里,我主要通过以下方式实现:

  • 🧭 使用 Tabs + 自定义 tabBar 构建底部导航;
  • 🌈 使用暖色头部、进度卡片和轻渐变营造学习氛围;
  • 👆 抽象 AnimDuration / AnimScale / AnimAlpha,统一点击动效;
  • 🌙 使用 ThemeManager 管理深色模式和图标背景映射;
  • 📱 使用 ResponsiveUtils 处理手机和平板适配;
  • 🧩 把首页、挑战、成就、收藏、我的组成完整学习闭环。

如果你也在做 OpenHarmony / HarmonyOS 应用,不建议一开始就追求复杂动画。先把导航、主题、点击反馈、状态栏、安全区域这些基础体验做好,应用的完成度会立刻提升一大截。🚀

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐