【OpenHarmony/HarmonyOs 】政治学习 App 的悬浮导航栏、沉浸光感与全新交互体验实践

在做 HarmonyOS NEXT / ArkTS 项目时,我越来越明显地感受到:一个学习工具不能只停留在“能用”,还要让用户愿意反复打开。尤其是初高中政治学习这种内容密度比较高的场景,如果界面过于生硬,学生很容易产生压力感。

这次我以自己的项目 政治视界 为例,整理一下如何在 ArkUI 中做出更轻、更柔和的学习 App 体验:包括底部导航、沉浸式窗口、安全区适配、全局背景、深浅色模式、页面切换动画等。整体目标是:打开就像进入一个专属学习空间,而不是进入一张冷冰冰的题单 😊

一、项目视觉定位

项目结构大致如下:

entry/src/main/ets/
├── pages/
│   ├── Index.ets              # 主页面,底部 Tab 导航
│   ├── HomePage.ets           # 首页学习看板
│   ├── QuizPage.ets           # 题库练习
│   ├── NotesPage.ets          # 笔记整理
│   ├── FlashCardPage.ets      # 闪卡记忆
│   ├── DailyPoliticsPage.ets  # 每日政治
│   └── ProfilePage.ets        # 我的/成就/目标
├── common/
│   ├── AppTheme.ets           # 统一主题色
│   ├── ResponsiveUtils.ets    # 响应式适配
│   └── AppLocalStorage.ets    # 全局状态
└── components/
    └── AppBackground.ets      # 全局背景

这个项目不是单纯堆页面,而是把“首页看板 + 题库 + 笔记 + 闪卡 + 报纸 + 我的”组织成一个完整学习闭环。视觉上我选择了偏柔和的学习氛围:浅色模式用插画背景和淡色渐变,深色模式用低亮度背景和柔和强调色,减少夜间学习时的视觉刺激。

二、沉浸式窗口:让内容自然延伸到系统区域

移动端 App 最容易显得“割裂”的地方,就是状态栏、导航区和页面背景不统一。项目中在 EntryAbility.ets 里开启了全屏布局,并读取系统避让区,把安全区高度同步到全局 LocalStorage

核心代码节选如下:

private async setupImmersiveWindow(windowStage: window.WindowStage): Promise<void> {
  const win = await windowStage.getMainWindow();
  this.mainWindow = win;
  await win.setWindowLayoutFullScreen(true);

  await win.setWindowSystemBarProperties({
    statusBarColor: '#00000000',
    statusBarContentColor: '#E6000000'
  });

  this.refreshSafeAreaInsets(win);
  win.on('avoidAreaChange', this.onAvoidAreaChange);
}

这里有几个细节值得注意:

  • setWindowLayoutFullScreen(true) 让页面可以进入系统栏区域;
  • 状态栏颜色设置为透明,让背景可以延展;
  • 通过 getWindowAvoidArea() 获取刘海、状态栏、底部导航指示区;
  • 把顶部和底部安全区写入 appLocalStorage,页面只需要读取状态即可。

对于学习类 App 来说,沉浸式不是为了“炫”,而是为了减少边界感。用户进入首页时,背景、卡片、导航栏像一个整体空间,阅读和刷题的体验会更连贯。

三、底部导航栏:不是简单 Tab,而是学习路径入口

主页面 Index.ets 使用底部 Tab 管理一级页面:首页、报纸、笔记、题库、闪卡、我的。

我没有直接用最朴素的文本按钮,而是给每个 Tab 加了按压缩放、颜色变化和页面滑动切换。这样用户每次切换模块时都有明确反馈。

@Builder
TabItem(icon: string, label: string, index: number) {
  Column() {
    Text(icon)
      .fontSize(22)
      .fontColor(this.currentTab === index ? AppTheme.tabSelected : AppTheme.textMuted)
      .scale({
        x: this.tabPressed === index ? 0.85 : 1.0,
        y: this.tabPressed === index ? 0.85 : 1.0
      })
      .animation({ duration: 100, curve: Curve.EaseInOut })

    Text(label)
      .fontSize(10)
      .fontColor(this.currentTab === index ? AppTheme.tabSelected : AppTheme.textMuted)
  }
  .onClick(() => {
    animateTo({ duration: 80, curve: Curve.EaseIn }, () => {
      this.tabPressed = index;
    });
  })
}

底部栏本身也做了“悬浮感”的处理:

.backgroundColor(AppTheme.cardBg)
.border({ width: { top: 1 }, color: AppTheme.divider })
.padding({ bottom: 8 + this.safeBottomVp })
.zIndex(200)
.shadow({
  radius: 24,
  color: '#12000000',
  offsetX: 0,
  offsetY: -6
})

这里的关键不是阴影越重越好,而是让导航栏在视觉层级上浮起来。学习 App 的页面内容很多,如果底部导航完全贴死在页面里,会显得很拥挤;加上轻阴影和安全区 padding 后,底部操作区域更稳。

四、全局背景:用“光感”统一页面氛围

项目里单独封装了 AppBackground,每个页面外层通过 Stack 叠加背景与内容。

浅色模式下使用插画 + 渐变遮罩:

Image($r('app.media.home_cartoon_sky'))
  .width('100%')
  .height('100%')
  .objectFit(ImageFit.Cover)
  .interpolation(ImageInterpolation.High)

Column()
  .width('100%')
  .height('100%')
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [
      ['#00FFFFFF', 0.0],
      ['#55FFF8FC', 0.4],
      ['#88FFF5FA', 1.0]
    ]
  })

深色模式下则切换为纯色暗背景,并配合更低透明度的装饰色块。这样做的好处是:

  • 浅色模式有轻松、亲和的学习氛围;
  • 深色模式减少图片带来的亮度干扰;
  • 背景逻辑集中在一个组件里,后续维护成本低;
  • 页面本身只关注内容,不需要重复写背景。

这类“沉浸光感”不一定非要用复杂动效实现。很多时候,只要统一背景、透明系统栏、柔和渐变和阴影层级,整体体验就能明显提升 ✨

五、深浅色主题:用 AppTheme 统一色彩

项目里使用 AppTheme 统一管理主题色,并通过 AppTheme.isDarkMode 判断当前模式。

export class AppTheme {
  static isDarkMode: boolean = false;

  static readonly light: ThemeColors = {
    titlePrimary: '#5C4D7A',
    textMuted: '#8C8C8C',
    cardBg: '#FFFFFF',
    tabSelected: '#8B7AE8',
    divider: '#F0F0F0'
  };

  static readonly dark: ThemeColors = {
    titlePrimary: '#C9B8FF',
    textMuted: '#7A7A8C',
    cardBg: '#1E1835',
    tabSelected: '#A894F0',
    divider: '#2E2548'
  };

  static get cardBg(): string {
    return AppTheme.isDarkMode ? AppTheme.dark.cardBg : AppTheme.light.cardBg;
  }
}

这种写法的优点很直接:页面里不散落大量颜色值。比如卡片背景、文字颜色、分割线、分类标签颜色都从 AppTheme 取,后续想调整整体风格,只需要在主题类里修改。

Index.ets 中,深色模式变化后还会通过刷新令牌触发页面重建:

@LocalStorageLink(THEME_REFRESH_KEY)
private themeRefresh: number = 0;

toggleDarkMode(): void {
  const newDark: boolean = !this.isDarkMode;
  this.isDarkMode = newDark;
  AppTheme.isDarkMode = newDark;
  saveDarkModeToStorage(newDark);
  this.themeRefresh = this.themeRefresh + 1;
}

这一点对 ArkUI 很重要:如果页面只是读取一个普通静态变量,UI 不一定会自动刷新。这里通过 @LocalStorageLink 建立响应式触发点,让深浅色切换真正反馈到界面。

六、页面切换动画:让 Tab 切换有方向感

在主页面里,Tab 切换不是直接替换内容,而是通过 pageSlideX 控制页面横向移动:

const direction = index > this.currentTab ? -1 : 1;
this.pageSlideX = direction * px2vp(1080);

setTimeout(() => {
  this.currentTab = index;
  this.pageSlideX = direction * px2vp(-1080);
  animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
    this.pageSlideX = 0;
    this.pageAnimating = false;
  });
}, 80);

这种处理会让用户感知到页面之间的空间关系:从首页切到题库、闪卡,就像在学习模块之间横向移动。对多模块 App 来说,这种“方向感”比单纯闪现更自然。

七、实践中的几个小坑

做 ArkTS / ArkUI 页面时,有几个点非常容易踩坑:

  1. @State 变量要被 UI 直接引用,否则状态变化后界面可能不刷新。
  2. ForEach 建议配合稳定 key,数据多时尤其重要。
  3. 不要在 build() 里写复杂逻辑,复杂计算最好抽到方法里。
  4. 自定义组件如果要使用 scale()animation() 等属性,最好外层用原生组件包住。
  5. 安全区不要写死数值,应从窗口避让区动态同步。

项目里很多交互都遵循了这些原则:按压态用 @State 保存,动画只改变状态,复杂数据由 DataManager 和页面方法处理。

八、总结

这篇文章主要从视觉和交互角度拆解了政治学习 App 的实现:沉浸式窗口、底部悬浮导航、全局背景、深浅色主题、页面切换动画。它们单独看都不复杂,但组合起来会让一个学习工具从“功能集合”变成“完整体验”。

对学习类 App 来说,好的视觉不是装饰,而是降低进入门槛;好的交互不是花哨,而是让用户知道自己在哪里、下一步可以做什么。HarmonyOS NEXT 的 ArkUI 组件化能力很适合做这种体验型页面,只要主题、状态、动画和安全区处理得当,就能做出很舒服的移动端学习产品 🚀

img

Logo

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

更多推荐