【OpenHarmony/HarmonyOs 】CheckMe 悬浮导航栏与沉浸光感体验实践:从系统栏到实时仪表盘的视觉升级

项目背景:本文基于我的 HarmonyOS 项目 CheckMe 展开。它不是一个单纯的信息展示 Demo,而是一个面向真实设备状态监控的工具类应用,覆盖 CPU、内存、电池、网络、定位、媒体能力、硬件检测和桌面卡片等模块。

很多 HarmonyOS 工具类应用容易做成“表格列表”:能用,但缺少视觉记忆点。CheckMe 的目标不只是把设备参数列出来,而是让用户一打开应用,就能感受到一种实时、清爽、有层次的设备看板体验。✨

这篇文章重点拆解三个方向:

  • 悬浮感底部导航栏
  • 沉浸式系统栏与安全区适配
  • 光感卡片、实时数据与交互动效

本文不重复展开服务卡片、设备信息采集、WorkScheduler 等工程链路,而是更聚焦 UI 体验与交互设计。


一、为什么工具类 App 也需要“视觉体验”

设备信息类应用的核心当然是数据准确,但只追求“把数据展示出来”是不够的。

用户真正打开这类应用时,往往有几个典型场景:

  • 想看当前 CPU 是否过高
  • 想快速确认电池、存储、网络状态
  • 想进行一次硬件检测
  • 想知道系统状态是否异常

这些场景都强调“快速判断”。因此页面设计应该让用户一眼看到重点,而不是陷入密密麻麻的参数列表。

CheckMe 采用的思路是:

  1. 首页做成实时 Dashboard,而不是普通设置页。
  2. 底部导航固定在可触达区域,降低切换成本。
  3. 系统栏、底栏和页面背景颜色保持一致,营造沉浸感。
  4. 图表和卡片使用柔和光感,不做过度炫技。
  5. 只让当前可见页面运行轮询和动画,避免为了“好看”牺牲性能。

这也是我认为 HarmonyOS 工具应用可以变得更高级的地方:不是堆控件,而是把系统能力、状态数据和视觉反馈组织成一个统一体验。


二、沉浸式系统栏:先把页面“铺满”

项目里系统栏处理放在 EntryAbility.ets 中。核心代码如下:

private applySystemBarToWindow(win: window.Window): void {
  const isDark = this.isEffectiveDarkMode();
  const barBackground = isDark ? '#FF1E293B' : '#FFFFFFFF';
  const barContent = isDark ? '#FFFFFFFF' : '#FF000000';

  const props: window.SystemBarProperties = {
    statusBarColor: barBackground,
    statusBarContentColor: barContent,
    navigationBarColor: barBackground,
    navigationBarContentColor: barContent
  };

  win.setWindowLayoutFullScreen(true);
  win.setWindowSystemBarProperties(props);
}

这里有两个关键点:

  • setWindowLayoutFullScreen(true):让应用内容进入全屏布局语境。
  • setWindowSystemBarProperties(props):主动控制状态栏、导航栏背景与文字颜色。

很多页面看起来“不高级”,原因不是组件写得不好,而是系统栏和页面割裂:页面是白色,导航栏是黑色;页面是深色,状态栏文字又不清楚。CheckMe 在浅色和深色模式下分别计算系统栏颜色,让系统区域和页面卡片区域保持一致。

官方文档中也提到窗口避让和沉浸布局相关能力,实际开发时需要结合设备形态、安全区域和系统栏属性综合处理。


三、底部导航栏:固定可触达,但不遮挡内容

CheckMe 的主界面采用底部导航,包含:

  • 概览
  • 硬件
  • 工具
  • 位置
  • 媒体

底部导航代码片段:

@Builder
BottomTabBar() {
  Column() {
    Divider()
      .color($r('app.color.border_divider'))
      .strokeWidth(0.5)
      .width('100%')

    Row() {
      this.TabItemBuilder('overview', '概览', $r('sys.symbol.house'))
      this.TabItemBuilder('hardware', '硬件', $r('sys.symbol.externaldrive_fill'))
      this.TabItemBuilder('tools', '工具', $r('sys.symbol.gearshape_fill'))
      this.TabItemBuilder('location', '位置', $r('sys.symbol.map'))
      this.TabItemBuilder('media', '媒体', $r('sys.symbol.music_fill'))
    }
    .width('100%')
    .height(49)

    Column()
      .width('100%')
      .height(this.bottomSafeInsetVp)
  }
  .width('100%')
  .backgroundColor($r('app.color.card_background'))
}

这个底栏看似简单,但它做了一个很重要的细节:把系统导航指示条区域单独补出来。

在全面屏设备上,如果底部栏只设置固定高度,可能出现文字贴底、被手势条遮挡、视觉重心下坠等问题。CheckMebottomSafeInsetVp 单独计算底部避让高度,让导航栏既贴近系统区域,又不会压住内容。


四、安全区高度如何计算

Index.ets 中,项目通过 window.getWindowAvoidArea() 获取系统导航指示条区域:

private updateBottomSafeInset(): void {
  const ctx: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  window.getLastWindow(ctx).then((win: window.Window) => {
    const avoid: window.AvoidArea =
      win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
    const pxH = avoid.bottomRect.height;

    const d = display.getDefaultDisplaySync();
    const dpi = d.densityDPI;

    this.bottomSafeInsetVp = (pxH * 160) / dpi;
  }).catch((_err: Error) => {
    this.bottomSafeInsetVp = 0;
  });
}

这里把像素高度转换成 vp

this.bottomSafeInsetVp = (pxH * 160) / dpi;

这段代码非常适合写进项目亮点,因为它体现了一个成熟 App 的思维:不是只在模拟器上看起来正常,而是考虑真实设备、安全区、不同 DPI 和系统手势区域。

📌 我的经验是:底部导航栏想做出“悬浮感”和“系统感”,避让区比阴影、圆角更重要。


五、导航交互:切 Tab 不只是改状态

CheckMe 切换 Tab 时没有直接改 currentTab 就结束,而是做了过渡动画,并在切换时停止不可见页面的轮询和装饰动画。

private onTabChange(tabKey: string): void {
  if (tabKey === this.currentTab) {
    return;
  }

  const previousTab: string = this.currentTab;
  this.stopDecorAndSecondaryTimers();
  this.stopCpuUsagePolling();
  this.stopRefreshRatePolling();
  this.stopOverviewDashboardPolling();

  this.tabContentOpacity = 0;
  this.tabContentOffset = 20;

  setTimeout(() => {
    this.currentTab = tabKey;
    animateTo({
      duration: 250,
      curve: Curve.EaseOut
    }, () => {
      this.tabContentOpacity = 1;
      this.tabContentOffset = 0;
    });

    this.applyPageVisiblePollingAndAnimations();
  }, 120);
}

这里最值得学习的不是 animateTo,而是前面的几行:

this.stopDecorAndSecondaryTimers();
this.stopCpuUsagePolling();
this.stopRefreshRatePolling();
this.stopOverviewDashboardPolling();

这说明项目把“视觉动效”和“资源管理”放在一起考虑。切走页面后,动画和轮询应该停止,否则用户看不到,系统还在刷新 @State,这对工具类应用来说非常浪费。


六、沉浸光感:卡片不是贴图,而是状态表达

CheckMe 的首页不是单纯使用静态卡片,而是将 CPU、内存、存储、电池、网络等状态做成实时数据图表。AdvancedDashboard.ets 中有大量 Canvas 绘制逻辑,比如平滑折线和面积图:

private drawSmoothArea(
  ctx: CanvasRenderingContext2D,
  points: ChartPoint[],
  baselineY: number
): void {
  if (points.length === 0) {
    return;
  }

  const segments: SmoothSegment[] = buildSmoothSegments(points);
  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);

  for (let i = 0; i < segments.length; i++) {
    const segment = segments[i];
    ctx.bezierCurveTo(
      segment.cp1x,
      segment.cp1y,
      segment.cp2x,
      segment.cp2y,
      segment.x,
      segment.y
    );
  }

  ctx.lineTo(points[points.length - 1].x, baselineY);
  ctx.lineTo(points[0].x, baselineY);
  ctx.closePath();
}

这种实现比普通进度条更适合设备监控,因为设备状态不是一个静态值,而是连续变化的趋势。

比如 CPU 使用率:

  • 当前值告诉用户“现在怎么样”
  • 曲线告诉用户“刚才发生了什么”
  • 峰值和波动告诉用户“是不是异常”

这就是“光感仪表盘”的价值:它不只是装饰,而是让数据更容易被感知。📈


七、主题适配:浅色和深色模式都要舒服

项目中单独抽出了 ThemeHelper,集中管理图表、卡片、文本、状态颜色。

public getCardBackground(): string {
  return this.isDarkMode ? '#1E293B' : '#FFFFFF';
}

public getCardShadowColor(): string {
  return this.isDarkMode
    ? 'rgba(0,0,0,0.25)'
    : 'rgba(15,23,42,0.09)';
}

public getSoftTrendPrimary(): string {
  return this.isDarkMode ? '#60A5FA' : '#2F7CF6';
}

这样做的好处是:

  • 页面不会到处散落颜色值
  • Canvas 图表也能跟随深浅色模式变化
  • 后续调整视觉风格时成本更低

尤其是 Canvas 绘图,如果颜色直接写死,很容易出现深色模式下看不清、浅色模式下太刺眼的问题。


八、实时体验背后的生命周期控制

视觉体验要高级,不能只看“动起来”。真正的关键是:该动的时候动,不该动的时候停。

AdvancedDashboard 中的轮询逻辑是这样设计的:

private startPolling(): void {
  if (!this.isComponentVisible ||
      !AppLifecycleManager.getInstance().isAppInForeground()) {
    return;
  }

  this.stopPolling();
  void this.loadMetrics();

  this.pollTimer = setInterval(() => {
    if (this.isComponentVisible &&
        AppLifecycleManager.getInstance().isAppInForeground()) {
      void this.loadMetrics();
    }
  }, 2000) as number;
}

这段代码体现了一个原则:

实时刷新必须服从页面可见性和应用前后台状态。

项目还用 AppLifecycleManager 统一分发前后台状态:

public setForegroundState(isForeground: boolean): void {
  if (this.isInForeground !== isForeground) {
    this.isInForeground = isForeground;
    this.notifyListeners();
  }
}

这样的处理让首页实时仪表盘既有“活着”的感觉,也不会在后台持续消耗资源。


九、文章小结

这篇文章从视觉体验角度拆解了 CheckMe 的几个实现点:

  • setWindowLayoutFullScreen 和系统栏属性做沉浸式页面基础
  • getWindowAvoidArea 适配底部安全区
  • 用底部 Tab 降低工具类应用的页面切换成本
  • 用 Canvas 曲线和柔和色彩表达实时设备状态
  • 用生命周期管理保证动画和轮询只在需要时运行

如果要给这个主题取一个关键词,我觉得不是“炫酷”,而是:克制的实时感

工具类应用不适合做过度花哨的动效,但非常适合用轻量动画、光感图表、状态色和安全区适配提升专业感。CheckMe 的这套实现思路,也可以迁移到性能监控、网络诊断、电池健康、设备管理等 HarmonyOS 应用中。


参考资料

img

Logo

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

更多推荐