一、先校准心智模型:Header 是「页面控制面」

动手之前先做 Benchmark。看四个官方应用怎么用 Header:

应用

Header 承担什么

设置

Search 在标题附近,滚动后 Header 收敛,靠模糊/分割线保持可读

图库

Header 与内容材质融合,Tabs/分类入口与滚动内容共同决定层级

文件

搜索、排序、筛选、更多操作都靠近页面身份区,滚动时操作保持可达

应用市场

Tabs、搜索、消息提醒、页面级菜单同时出现,Header 支持多状态入口

提炼出的设计原则:

  • 标题是页面身份,不是业务内容标题的重复装饰;

  • Search 可以放在标题下方(bottom custom area),滚动后收起;

  • Segment 适合轻量视图切换(Today / Pinned / Shared),Tabs 要克制数量;

  • Toolbar 只放页面级轻操作,批量编辑类强操作下放到内容区或底部;

  • Badge 放在 menu / subIcon,不散落在内容顶部;

  • Dynamic Blur 是 Header 与内容融合的核心,由系统/HDS 实现,不手写 blur overlay。

一句话:Header 是系统托管的页面控制面(页面身份 + 页面级命令 + 轻量状态提醒),不是一个自由布局的容器。这个心智模型直接决定了后面所有 API 怎么用。

本实验还基于一条已有决策(ADR-001):Navigation 拥有标题栏。所有页面身份、菜单、模糊都配置在 HdsNavigation.titleBar / HdsNavDestination.titleBar 上,页面内容区不再出现第二个顶栏。

二、能力地图:HdsNavigation Header 给了你什么

先盘点当前 SDK(本机 @kit.UIDesignKit.d.ts + @hms.hds.hdsBaseComponent.d.ets 交叉验证)暴露的 Header 能力:

能力

入口

实验验证结果

标题 + 副标题

titleBar.content.title

mainTitle / mainTitleSize

页面菜单

titleBar.content.menu

maxCount: 3,超出自动收进更多

菜单红点

menu[].badge

count / value / accessibilityText

副图标

subIcon

✅ 编译验证

顶部自定义区

stackBuilder

✅ 编译验证;要显式留状态栏缓冲

底部自定义区

bottomBuilder

成熟项:单一 Search

工具栏

toolbarConfiguration

✅ 编译验证,未进基线

半模态标题模式

HdsNavDestinationTitleMode.MODAL

✅ 用于 Search Overlay / Quick Access

滚动联动

bindToScrollable([scroller])

✅ 每个目的页自己的 Scroller

动态模糊

scrollEffectOpts

✅ 核心能力,见第五节

分割线

divider.showType

DividerShowType.AUTO

覆盖度结论:页面身份、搜索、分段、Tabs、提醒、页面级菜单和半模态快捷入口都能留在 HDS Navigation 体系内,不需要手写顶栏。

下面挑四个最有料的展开。

三、Badge:状态驱动,不是按钮自增

第一个验证项:菜单 ANQ 带动态 Badge,验证状态提醒、多 Badge 形态、点击更新和 Header 刷新。

实现的关键心智是:Badge 是状态的投影,不是按钮自己的计数器。菜单项构造时从页面状态读 Badge:

private MenuItem(label: string, badgeCount: number = -1, badgeValue: string = ''): HdsNavigationMenuItemOptions {
  const item: HdsNavigationMenuItemOptions = {
    content: {
      label: label,
      type: TextStyleMode.SINGLE_CHARACTER,
      action: (): void => {
        // 点击只改状态,不改 UI
        if (label === '一个') {
          this.syncBadgeCount = this.syncBadgeCount + 1;   // 模拟同步进度
        }
        if (label === '氮') {
          this.notificationBadgeCount = 0;                  // 进入即清除未读
        }
      }
    }
  };

  if (badgeCount >= 0 || badgeValue.length > 0) {
    item.badge = {
      count: badgeCount,                                    // 数字形态
      value: badgeValue,                                    // 字符形态('!')
      accessibilityText: `${label} status badge`            // 无障碍必配
    };
  }
  return item;
}

private TitleMenuItems(): Array<HdsNavigationMenuItemOptions> {
  return [
    this.MenuItem('一个', this.syncBadgeCount),   // 数字 Badge
    this.MenuItem('氮', this.notificationBadgeCount),
    this.MenuItem('问', -1, '!')                   // 字符 Badge
  ];
}

syncBadgeCount / notificationBadgeCount 是宿主 @State。状态一变,TitleMenuItems() 重新求值,Header 整体刷新——Badge 的生命周期、消除策略、同步来源都由业务状态机控制,菜单项只是渲染器。这正是实验沉淀的 Navigation Badge Pattern。

两条实践细节:

  • accessibilityText 必须配。红点对读屏用户是「有东西」,配了文本才是「有 3 条同步提醒」。

  • 菜单数量克制在 maxCount: 3。生产力应用的 Header 菜单超过三个,就该进「更多」面板而不是排开。

四、Search bottomBuilder:一个高价值轻控件的基线

第二个验证项:Search 放进 Header 的底部自定义区,并且随滚动方向收起/展开。

4.1 官方基线:56vp 容器,40vp 胶囊

按官方示例,bottom custom area 放一个 Search,容器固定 56vp、内部搜索胶囊 40vp、左右 16vp 边距:

titleBar: {
  content: {
    ...
    bottomBuilder: {
      builder: (): void => { this.SearchBottomBuilder(); },
      height: this.searchVisible ? 56 : 0,
      showType: BottomBuilderShowType.DIRECTLY_SHOW
    }
  }
}

4.2 收起/展开:动画驱动,禁止 if 插拔

显隐用一个 220ms 的小动画,height 和 opacity 同步变化:

private SetSearchVisible(visible: boolean): void {
  if (this.searchVisible === visible) {
    return;   // 状态幂等,避免重复动画
  }
  this.getUIContext().animateTo({
    duration: 220,
    curve: Curve.EaseOut
  }, () => {
    this.searchVisible = visible;   // bottomBuilder.height / capsule.height / opacity 全部由它派生
  });
}

Bottom custom area 不能用 if 生硬插拔——插拔是布局突变,动画驱动才是连续收敛。胶囊的高度与透明度同样从 searchVisible 派生:

SharedSearchCapsule({ displayText: this.searchText, placeholder: '搜索' })
  .height(this.searchVisible ? 40 : 0)
  .opacity(this.searchVisible ? 1 : 0)
  .margin({ left: 16, right: 16 })

4.3 滚动方向:4vp 阈值防抖

什么时候收、什么时候展?由列表滚动方向驱动,但要做阈值防抖,否则手指微抖搜索框会闪烁:

.onDidScroll((): void => {
  const offset: OffsetResult = this.scroller.currentOffset();
  this.onChromeStateChange(offset.yOffset > 28);      // 标题栏收敛态

  const delta: number = offset.yOffset - this.lastScrollOffset;
  if (offset.yOffset <= 2) {
    this.onSearchVisibilityChange(true);              // 回到顶部:强制显示
  } else if (delta > 4) {
    this.onSearchVisibilityChange(false);             // 向上滑(内容下行):收起
  } else if (delta < -4) {
    this.onSearchVisibilityChange(true);              // 向下滑:展开
  }
  this.lastScrollOffset = offset.yOffset;
})

三条规则各司其职:回顶强制显示(顶部永远是搜索入口)、±4vp 阈值(方向判定去抖)、保持 bindToScrollable([pageScroller])(每个目的页用自己的 Scroller,滚动联动由 HDS 自己消费)。

4.4 撤回的故事:为什么只留一个 Search

实验过程中真的做过「Search + Segment + Tabs + Toolbar 同时塞进 Header」的版本——能力上全部编译通过、运行正常,然后我们把它撤回了

原因不是技术,是信息架构:

  • Segment/Tabs 进 Header 后,Header 变成了二级导航树,和底部 TabBar、页面内切换职责打架;

  • Toolbar 的批量编辑类强操作放头部,拇指可达性差,系统规范本来就建议下放;

  • 多控件堆叠后,stackBuilder 自定义区还要自己处理状态栏缓冲与稳定高度,否则控件贴到系统栏。

所以基线收敛为:bottomBuilder 只放一个高价值轻控件(Search),Quick Access 拆成独立 Destination。Segment/Tabs/Toolbar 若要启用,需重新做视觉密度、safe area、动效与跨页面复用验证——这是「能力可用」和「该不该用」的区别。

五、动态模糊与双态配色:Header 融合的核心

5.1 开启沉浸式渐变模糊

Header 与内容融合靠系统动态模糊,一条配置:

style: {
  scrollEffectOpts: {
    enableScrollEffect: true,
    scrollEffectType: ScrollEffectType.IMMERSIVE_GRADIENT_BLUR
  },
  blurStrategy: BlurStrategy.ADAPTIVE
}

模糊由系统/HDS 实现,应用侧只能配置类型、样式和阈值——不要手写 blur overlay 替代,那是 E006 之前各项目反复交过的学费。

5.2 双态样式:originalStyle 与 scrollEffectStyle

HDS Header 有两个视觉状态:原始态(未滚动,Header 与页面融合、背景透明)和滚动态(滚动后,模糊+背景+分割线出现)。样式必须两套都配

originalStyle: {
  backgroundStyle: {
    backgroundColor: ThemeTransparentWhiteColor(),
    maskExtraHeight: 56,        // 给 bottomBuilder 的 Search 留遮罩高度
    blurRadius: 0
  },
  contentStyle: { titleStyle: {...}, menuStyle: {...} }
},
scrollEffectStyle: {
  backgroundStyle: {
    backgroundColor: NavigationTitleBarButtonBackground(),
    maskExtraHeight: 84,        // 收敛后遮罩更高
    blurRadius: 72              // 模糊半径
  },
  contentStyle: { titleStyle: {...}, menuStyle: {...}, dividerStyle: {...} }
}

两个容易栽的细节:

  1. maskExtraHeight 要跟着 Header 内容高度走。加了 56vp 的 Search bottomBuilder,原始态遮罩至少 56,否则模糊区域罩不住搜索框。

  2. 只配一套 = 状态切换时穿帮。比如只配了 scrollEffectStyle 的浅色标题,未滚动时标题沿用默认深色,一滚动颜色跳变。

5.3 三套样式入口互不相干

真机验证中最重要的 API 行为发现:

menuStyle(右上菜单)、backIconStyle(返回按钮)、titleStyle(标题)是三套独立的样式入口,互相不影响。

配了 menuStyle 的颜色,返回按钮纹丝不动;配了 backIconStyle,菜单也不受影响。而 HDS 默认的返回按钮本身是状态机:透明/未滚动态使用浅色图标和淡背景,滚动触发 scrollEffectStyle 后切换为更深的图标和背景。

由此推出三条决策:

  • 产品接受 HDS 默认状态机 → 不配 backIconStyle,让返回按钮跟随系统;

  • 产品要求 title/back/menu 滚动前后颜色固定 → 必须同时配置 originalStyle.contentStylescrollEffectStyle.contentStyle,一套都不能少;

  • 颜色永远来自系统 token,不硬编码十六进制。

六、系统颜色 token:别靠名称猜颜色

上一节的「固定颜色」用什么值?实验沉淀了四个系统 token,封装成 NavigationTitleBarTokens.ets

Token

用途

sys.color.ohos_id_color_sub_background

内页背景 / 滚动态标题栏背景

sys.color.ohos_id_color_component_normal

标题栏胶囊按钮背景

sys.color.ohos_id_color_text_primary

标题、返回图标、菜单文字/图标

sys.color.ohos_id_color_list_separator

标题栏 divider

export function NavigationTitleBarButtonForeground(): ResourceColor {
  return $r('sys.color.ohos_id_color_text_primary');
}

为什么要封装成函数而不是各页面散落 $r(...)?两个原因:一处改全局生效,以及真机/深色模式/高对比模式下 token 的实际渲染值会变,散落的十六进制颜色做不到跟随。

配套还做了一个 SystemColorTokenLab 目的页——不是替代设计系统,而是在真机、深色、主题变化下直接观察 token 实际渲染值。「靠名称猜颜色」是深色模式翻车的第一大来源。

七、Quick Access:独立 Destination,不混进 AppShell

最后一个架构决策:Quick Access(下拉快捷入口)这类半模态页面,独立成 QuickAccessDestination 文件和路由,使用独立 Scroller、动态模糊标题栏和生命周期显隐,标题模式用 HdsNavDestinationTitleMode.MODAL

反面做法是把它塞进 NavigationAppShellif/else 分支里——AppShell 会迅速膨胀成千行文件,且半模态生命周期与主页面互相污染。Shell 只负责壳,每个 Lab/功能页是独立 Destination,这是从 E001 一路保持的模块边界(ADR-002)。

八、沉淀:五条 Pattern 与使用守则

实验沉淀的 Navigation Header Pattern:

  1. Header Status Entries:AI、同步、通知、未读统一建模为状态实体,Badge 由状态驱动。

  2. Badge Pattern:状态驱动 + accessibilityText,禁止按钮内部自增计数长期持有。

  3. Search bottomBuilder Pattern:单一 Search,56/40/16 尺寸,滚动方向 4vp 阈值 + 回顶强制显示。

  4. TitleBar Token Pattern:original 与 scroll 双态共用一套 token helper,不散落硬编码。

  5. Quick Access Destination Pattern:独立页面文件 + 独立路由,不混入 AppShell。

使用守则一句话版:

不要手写 Navigation Header。 优先 HdsNavigation.titleBar / HdsNavDestination.titleBar;高级需求优先官方 bottomBuildermenu.badge 和独立 HdsNavDestination;禁止为了 Search、Tabs、Toolbar 重新手写一个顶部栏。Header 的职责是页面身份、页面级命令和轻量状态提醒——HDS 负责框架和模糊,信息架构永远归你自己

结语

E006 验证完后,ArkUILab 的 Navigation Framework 标记为 v2 Candidate。尚欠的功课也很明确:真机 Benchmark 录屏对比、深色/高对比模式检查、Header 状态从实验态局部 @State 升级为统一模型。这些不影响本文结论:能力是够的,约束你的应该是信息架构判断,而不是框架能力恐慌。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐