【共创稿事节】HarmonyOS 7空间信息层级:焦点、景深与注意力引导

平面界面里,用户的眼睛被屏幕边界框着,注意力顶多在矩形内跳来跳去。空间界面没有这个框,用户能看的地方变多了,注意力反而更容易散。这时候设计的活儿就是主动引导:明确告诉用户"先看这里,再看那里"。焦点和景深是两把最顺手的工具。

焦点怎么建立

让一个元素成为视觉焦点,可用的手段不止放大一种。把它们列全了,才好知道什么时候用哪个:

手段 具体做法 强度 副作用
尺度 放大元素、缩短视距 强 占视野,挤压周围
亮度 提高brightness,焦点更亮 中 过亮会刺眼
对比 焦点明、背景暗,或反色 强 对比过强显生硬
位置 放在视线中心、中轴线上 强 与舒适区绑定
颜色 焦点用饱和色,背景降饱和 中 品牌色过多会失效
运动 焦点有微动,背景静止 强 滥用会干扰阅读
清晰度 焦点实,背景虚 强 算力开销

这些手段单独用都成立,但叠两到三种就够。七种全上,用户看到的是一团互相打架的信号,焦点反而没了。经验值是:尺度 + 清晰度作为主手段,亮度或颜色作为辅助,运动只在需要"抓住"用户时用。

一个常见问题:焦点一堆,等于没有焦点。空间界面里,同一时刻的强焦点建议只留一个,其余降一档。

景深(DoF)为什么能引导注意力

景深原本是光学概念------镜头对焦在某段距离上,那段距离内清晰,前后逐渐模糊。人眼本身也有这个机制,所以"清晰 vs 模糊"天然对应"关注 vs 忽略",不需要额外学习。

在 ArkUI 里没有真正的物理景深相机,但可以用视觉信号模拟,而且效果够用:

  • 模糊 :blur(value) 给离焦层加模糊,值越大越虚,单位 px;
  • 亮度 :brightness(value) 乘数调亮度,值小于 1 变暗(1 为原样);
  • 饱和度 :saturate(value) 降饱和(1 为原样),让离焦内容退成灰调;
  • 透明度 :opacity 降透明,让离焦层更"退后";
  • 线性渐变模糊 :linearGradientBlur 可以只在某个方向上渐次模糊,适合做"近处清晰、远处渐散"的空间纵深。

组合起来就是一套"离焦权重":

typescript 复制代码
// 离焦程度 0~1:0 为完全聚焦,1 为完全离焦
@Builder
defocused(level: number) {
  Column() {
    // 组件内容
  }
  .blur(level * 14)                  // 模糊随离焦加深
  .brightness(1 - level * 0.35)      // 变暗,退到次要位置
  .saturate(1 - level * 0.6)         // 降饱和,收掉颜色干扰
  .opacity(1 - level * 0.25)         // 稍微透明
}

这几个值按同一个 level 联动,比各调各的要稳。聚焦态 level = 0,完全一致地回到原样。

视觉权重怎么分配

多层信息同时在场时,给每层定一个"权重预算",避免所有东西都在喊"看我"。下面是一份可直接套用的起始表,权重总和控制在 100% 以内:

层级 尺度 亮度系数 饱和度 模糊 px 透明度 权重预算
焦点 1.0 1.05 1.0 0 1.0 55%
次焦点 0.95 0.9 0.8 0~4 0.95 25%
内容 0.9 0.8 0.7 6~10 0.9 15%
环境 0.8 0.6 0.5 14~20 0.8 5%

这套值不是硬规定,是"起步价"。真正落地时按场景调,但层级之间要拉开可见的差距,相邻两层差别太小,用户读不出主次。

焦点切换的参数化实现

把上面这张表写成一个可复用的焦点档位,用枚举表达语义,代码里就不会到处散落魔法数字。

typescript 复制代码
// 焦点档位:越靠前权重越高
enum FocusLevel {
  Focus = 0,      // 焦点
  Secondary = 1,  // 次焦点
  Content = 2,    // 内容
  Ambient = 3     // 环境
}

// 每个档位对应一组视觉参数
interface FocusStyle {
  blur: number
  brightness: number
  saturate: number
  opacity: number
  scale: number
}

function styleOf(level: FocusLevel): FocusStyle {
  switch (level) {
    case FocusLevel.Focus:
      return { blur: 0, brightness: 1.05, saturate: 1.0, opacity: 1.0, scale: 1.0 }
    case FocusLevel.Secondary:
      return { blur: 3, brightness: 0.9, saturate: 0.8, opacity: 0.95, scale: 0.96 }
    case FocusLevel.Content:
      return { blur: 8, brightness: 0.78, saturate: 0.68, opacity: 0.9, scale: 0.92 }
    default:
      return { blur: 16, brightness: 0.6, saturate: 0.5, opacity: 0.8, scale: 0.88 }
  }
}

案例:空间化通知中心的聚焦切换

手机通知中心就是把一堆通知平铺排列,用户靠滑动和分组找重点。空间化后,可以让通知卡片围成一圈或几排,用户把视线落在哪张卡上,那张卡就自动进入焦点档,其余按距离退到次焦点、内容、环境档。

typescript 复制代码
// NotificationOrbit.ets ------ 通知卡片环形排布,聚焦项自动升权
@Entry
@Component
struct NotificationOrbit {
  // 当前聚焦的卡片索引,-1 表示无聚焦
  @State focusIndex: number = -1

  // 示例通知数据
  private notices: string[] = ['日程提醒', '系统更新', '消息回复', '待办临近', '快递到达', '账单提醒']

  // 根据与聚焦项的距离推导档位:越远越离焦
  private levelFor(index: number): FocusLevel {
    if (this.focusIndex < 0) return FocusLevel.Content // 未聚焦时全部等权
    const dist = Math.abs(index - this.focusIndex)
    if (dist === 0) return FocusLevel.Focus
    if (dist === 1) return FocusLevel.Secondary
    if (dist === 2) return FocusLevel.Content
    return FocusLevel.Ambient
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      ForEach(this.notices, (text: string, index: number) => {
        Column() {
          Text(text).fontSize(16).fontColor(Color.White)
        }
        .width(160).height(96)
        .borderRadius(16)
        .backgroundColor('#1E2740')
        // 用角度把卡片摆成一圈,转头的动作对应切换聚焦
        .rotate({ angle: index * 30, centerX: '50%', centerY: '700%', perspective: 1200 })
        .applyFocus(this.levelFor(index)) // 自定义扩展:套用焦点档位
        .onHover((isHover: boolean) => {
          // 视线/指针落在哪张卡上,哪张卡成为焦点
          this.getUIContext()?.animateTo({ duration: 220, curve: Curve.Friction }, () => {
            this.focusIndex = isHover ? index : -1
          })
        })
      }, (text: string) => text)
    }
    .width('100%').height('100%')
    .backgroundColor('#0B1020')
  }
}

// 通过 AttributeModifier / 自定义修饰器把 FocusStyle 落到组件上
@Extend(Column)
function applyFocus(level: FocusLevel) {
  .blur(styleOf(level).blur)
  .brightness(styleOf(level).brightness)
  .saturate(styleOf(level).saturate)
  .opacity(styleOf(level).opacity)
  .scale({ x: styleOf(level).scale, y: styleOf(level).scale })
}

这套逻辑的关键是档位由"距离"推导 ,而不是每张卡单独设状态。聚焦项一动,整排的权重自动重排,不用维护一堆 if。onHover 在这里只是示意入口,实际项目里换成 onFocus、视线追踪回调或者指针位置都可以。
#mermaid-svg-q0X2n9BdUT5umv9r{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q0X2n9BdUT5umv9r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q0X2n9BdUT5umv9r .error-icon{fill:#552222;}#mermaid-svg-q0X2n9BdUT5umv9r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q0X2n9BdUT5umv9r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q0X2n9BdUT5umv9r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q0X2n9BdUT5umv9r .marker.cross{stroke:#333333;}#mermaid-svg-q0X2n9BdUT5umv9r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q0X2n9BdUT5umv9r p{margin:0;}#mermaid-svg-q0X2n9BdUT5umv9r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q0X2n9BdUT5umv9r .cluster-label text{fill:#333;}#mermaid-svg-q0X2n9BdUT5umv9r .cluster-label span{color:#333;}#mermaid-svg-q0X2n9BdUT5umv9r .cluster-label span p{background-color:transparent;}#mermaid-svg-q0X2n9BdUT5umv9r .label text,#mermaid-svg-q0X2n9BdUT5umv9r span{fill:#333;color:#333;}#mermaid-svg-q0X2n9BdUT5umv9r .node rect,#mermaid-svg-q0X2n9BdUT5umv9r .node circle,#mermaid-svg-q0X2n9BdUT5umv9r .node ellipse,#mermaid-svg-q0X2n9BdUT5umv9r .node polygon,#mermaid-svg-q0X2n9BdUT5umv9r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q0X2n9BdUT5umv9r .rough-node .label text,#mermaid-svg-q0X2n9BdUT5umv9r .node .label text,#mermaid-svg-q0X2n9BdUT5umv9r .image-shape .label,#mermaid-svg-q0X2n9BdUT5umv9r .icon-shape .label{text-anchor:middle;}#mermaid-svg-q0X2n9BdUT5umv9r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q0X2n9BdUT5umv9r .rough-node .label,#mermaid-svg-q0X2n9BdUT5umv9r .node .label,#mermaid-svg-q0X2n9BdUT5umv9r .image-shape .label,#mermaid-svg-q0X2n9BdUT5umv9r .icon-shape .label{text-align:center;}#mermaid-svg-q0X2n9BdUT5umv9r .node.clickable{cursor:pointer;}#mermaid-svg-q0X2n9BdUT5umv9r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q0X2n9BdUT5umv9r .arrowheadPath{fill:#333333;}#mermaid-svg-q0X2n9BdUT5umv9r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q0X2n9BdUT5umv9r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q0X2n9BdUT5umv9r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0X2n9BdUT5umv9r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q0X2n9BdUT5umv9r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0X2n9BdUT5umv9r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q0X2n9BdUT5umv9r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q0X2n9BdUT5umv9r .cluster text{fill:#333;}#mermaid-svg-q0X2n9BdUT5umv9r .cluster span{color:#333;}#mermaid-svg-q0X2n9BdUT5umv9r div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q0X2n9BdUT5umv9r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q0X2n9BdUT5umv9r rect.text{fill:none;stroke-width:0;}#mermaid-svg-q0X2n9BdUT5umv9r .icon-shape,#mermaid-svg-q0X2n9BdUT5umv9r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0X2n9BdUT5umv9r .icon-shape p,#mermaid-svg-q0X2n9BdUT5umv9r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q0X2n9BdUT5umv9r .icon-shape .label rect,#mermaid-svg-q0X2n9BdUT5umv9r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0X2n9BdUT5umv9r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q0X2n9BdUT5umv9r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q0X2n9BdUT5umv9r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中卡片 i
用户视线/指针移动
落在哪张卡片
focusIndex = i
按距离推导档位
i 本身:Focus 档
i±1:Secondary 档
更远:Content / Ambient 档
前移 + 去模糊 + 提亮
轻微后退 + 微模糊
后退 + 强模糊 + 降饱和

聚焦与离焦的对照

维度 聚焦(Focus) 离焦(Defocus)
距离感 近,前移 远,后退
清晰度 实,blur=0 虚,blur 随距离递增
亮度 略提亮 压暗
颜色 保持饱和 降饱和
动效 前移、轻微呼吸 静止、退让
交互 接收输入 关闭命中

小小经验

  • 同一时刻一个强焦点。 两个同等强度的焦点会互相削弱,用户的视线会在两者间来回跳。
  • 模糊是"降权",不是装饰。 如果只是想加个好看的虚化,别用 blur,它会被用户读成"这里不用看"。
  • 聚焦切换要快。 200~250ms 左右,比深度层级切换(300~400ms)更快,因为它是高频动作。
  • 给离焦内容留"回来的路"。 离焦不等于消失,保留轮廓和轻度可读性,用户才知道可以移回去。
  • 亮度优先于模糊做降权。 模糊开销大,能用亮度和饱和度区分的场合,别急着上 blur。

容易出问题的地方哦

  • 所有元素一起提亮做焦点。 提亮是全屏相对概念,全都亮等于全都没亮,得留暗部做对比。
  • blur 值给太大。 超过 20px 的内容基本认不出,用户会以为加载失败,而不是"这里次要"。
  • 聚焦态还保持可点。 离焦的卡片如果还能点,用户误触后会觉得"我没看它怎么触发了",建议离焦层关闭或降低命中优先级。
  • 只改视觉不改顺序。 焦点在视觉上是前面的,但如果 zIndex 没跟着调,它可能被后面的卡片盖住。
  • 动画里同时改太多属性。 blur、brightness、scale 一起重算,低端设备容易掉帧,建议聚焦切换时限制在 2~3 个属性同时变化。
  • 忽略个体差异。 有的人对运动敏感,大范围的聚焦动画可能引起不适,建议给系统级的"减少动效"开关留一个退化路径。
相关推荐
zhangfeng11334 小时前
Reward Hacking 奖励钻空子 / 奖励作弊 Specification Gaming(规范博弈)
人工智能·华为·ai编程·npu
小师兄吃牛肉4 小时前
ANSYS EMA3D 2025 R2 专业安装详解:系统级电磁兼容与电磁干扰仿真平台全解析
3d
特立独行的猫a6 小时前
用仓颉写一个 Tauri:IPC 的每次往返实现原理(web层到仓颉层的触发过程)
前端·ui·harmonyos·tauri·鸿蒙·仓颉
2501_919749037 小时前
华为鸿蒙桌面时钟与悬浮时钟APP—小羊时钟
华为·harmonyos
HwJack207 小时前
【共创稿事节】HarmonyOS 7空间交互综述:视线、头部、手势的协同
数码相机·交互·harmonyos
李游Leo8 小时前
HarmonyOS 7 ArkUI + Window Kit:折叠屏编辑页的键盘避让快照与焦点恢复时序【鸿蒙心迹】
华为·计算机外设·harmonyos
李游Leo10 小时前
HarmonyOS 7 HSP + Localization Kit:共享组件资源导出、语言回退与相对路径失效诊断【鸿蒙心迹】
harmonyos
m0_7381858210 小时前
Flutter 鸿蒙化实战:flutter_nfc_kit 适配 OpenHarmony,NFC 读写一步到位
flutter·华为·harmonyos·鸿蒙
垆边人似月.10 小时前
华为机考题:字符个数统计
数据结构·算法·华为