【共创稿事节】 HarmonyOS 7从 2D 到 3D:空间计算带来的到底是什么范式变革

HarmonyOS 7从 2D 到 3D:空间计算带来的到底是什么范式变革

HarmonyOS 7(API 26)里,空间计算第一次被单独拎出来当作一个板块,而不是塞在图形或媒体能力里顺手提一句。很多人第一反应是"UI 能转起来了""能做 3D 卡片了",但如果只把它当成视觉增强,就会漏掉真正难的部分:我们过去十年熟练的这套 2D 设计语言,在 Z 轴出现以后,有一半的默认假设都不成立了。

这篇文章想聊清楚两件事:系统级能力到底把哪些门槛抹平了,以及为什么"认知门槛"和"设计门槛"还在原地等着你。

系统把技术门槛按了下去,但没按掉认知门槛

先看系统这次到底给了什么。以前要在 App 里做真实的 3D 效果,你得自己引入渲染引擎、自己算透视矩阵、自己处理光照模型。ArkUI 现在把这些能力做进了声明式组件里:

  • 图形变换:rotate(带 x/y/z 旋转轴)、translate(支持 z)、scale、perspective(视距)在一套属性链里就能组合出三维效果;
  • 真正的三维变换:从 API 20 起提供 transform3D,可以直接喂一个 4x4 矩阵,处理带透视的变换;
  • 场景级 3D:ArkGraphics 3D 通过 Component3D 渲染 glTF 模型,相机、光源、材质都在 ArkTS 侧管理;
  • 端侧重建:Spatial Recon Kit 支持加载 3DGS 模型(MP4 / PLY / GLB);
  • 空间音频:AudioKit 的 AudioSpatializationManager 可以查询和订阅设备的空间音频渲染状态。

矩阵推导、投影、着色这些过去劝退设计师和大部分应用开发者的东西,现在被封装成了属性。这是"技术门槛下降"的真实含义。

但门槛下降的是"实现",不是"判断"。系统不会告诉你一个按钮该浮在前景还是沉到背景,不会告诉你把导航放到用户左侧 30 度意味着什么,也不会告诉你某个动效会不会让人头晕。能不能跑 是工程问题,该不该这么跑是设计问题------后者才是空间计算真正的分水岭。

从 X/Y 到 X/Y/Z,改的不是多一个轴

把 2D 界面往 Z 轴一推,很多人以为只是"多了个维度"。实际变动的是整套坐标语义。

在 2D 里,屏幕的 X/Y 是位置 ,深度没有概念。控件要么在,要么不在;层叠靠 zIndex 做遮挡排序,那只是绘制顺序,用户感知不到"远近"。

到了空间里,Z 轴同时承担了四种含义:

  1. 遮挡关系------近的挡住远的,这是最表层的物理直觉;
  2. 距离感------元素离观察者多远,直接对应"重要程度"和"可交互程度";
  3. 信息层级------前景是主任务,中景是上下文,背景是环境;
  4. 注意力资源------人眼在空间中一次只能聚焦一个焦平面,深度天然就是注意力分配器。

也就是说,Z 轴不再是渲染参数,而是一种信息组织手段。这是范式变革里最需要先扭转的一点:平面设计里的"上/下、左/右"对应的是阅读顺序,空间设计里的"近/远"对应的是认知优先级。

同一张卡片,2D 和空间两种写法

下面用一个商品卡片做对照。2D 版本是常见的平铺卡片;空间版本把同一张卡片放进一个有纵深的 Stack 里,用 perspective 和 z 位移拉开层次。

typescript 复制代码
import { curves } from '@kit.ArkUI';

@Entry
@Component
struct CardParadigmDemo {
  @State private spatial: boolean = false;

  @Builder
  ProductCard(title: string, price: string) {
    Column({ space: 6 }) {
      // 占位图,实际项目替换为 Image
      Row()
        .width('100%')
        .aspectRatio(1)
        .backgroundColor('#E8EDF5')
        .borderRadius(12)
      Text(title).fontSize(15).fontWeight(FontWeight.Medium).maxLines(1)
      Text(price).fontSize(13).fontColor('#E85D3D')
    }
    .padding(10)
    .width(150)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({ radius: 12, color: '#14000000', offsetY: 4 })
  }

  build() {
    Column() {
      // Stack 是空间感的起点:子组件在这里共享同一块画布
      Stack({ alignContent: Alignment.Center }) {
        this.ProductCard('旧旅行箱', '¥429')
          .zIndex(0)
          .translate({ x: this.spatial ? -70 : -50, y: 0, z: this.spatial ? -60 : 0 })
          .rotate({ x: 0, y: 1, angle: this.spatial ? -18 : 0, perspective: 900 })
          .opacity(this.spatial ? 0.75 : 1)

        this.ProductCard('主推商品', '¥199')
          .zIndex(2)
          .translate({ x: 0, y: this.spatial ? 6 : 0, z: this.spatial ? 20 : 0 })
          .rotate({ x: 0, y: 1, angle: this.spatial ? -4 : 0, perspective: 900 })
          .scale({ x: this.spatial ? 1.12 : 1, y: this.spatial ? 1.12 : 1 })

        this.ProductCard('新款上市', '¥359')
          .zIndex(1)
          .translate({ x: this.spatial ? 70 : 50, y: 0, z: this.spatial ? -60 : 0 })
          .rotate({ x: 0, y: 1, angle: this.spatial ? 16 : 0, perspective: 900 })
          .opacity(this.spatial ? 0.75 : 1)
      }
      .width('100%')
      .height(320)

      Button(this.spatial ? '回到平面' : '进入空间')
        .margin({ top: 24 })
        .onClick(() => {
          // 一次性把三张卡片的位移/旋转/缩放补间到目标状态
          this.getUIContext()?.animateTo(
            { duration: 400, curve: curves.springMotion(0.8, 20) },
            () => { this.spatial = !this.spatial; }
          );
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段代码里,spatial 这个布尔量切换的不是"有没有动画",而是三张卡片在 Z 轴上的相对关系:主推商品被拉到 z: 20 并放大,两侧商品退到 z: -60 并降低不透明度。perspective: 900 提供视距------值越小透视越夸张,值越大越接近正交投影。想深入的话,perspective 和 centerZ 组合还能做出"绕自己所在平面翻转"的效果。

这个例子里,真正决定信息层级的不是颜色也不是字号,而是 z 位置的相对排序。这就是空间 UI 和 2D UI 最本质的区别。

2D UI 与空间 UI 的设计要素对照

把设计要素拆开看,差异更直观:

设计要素 2D 平面 UI 空间 UI 变化实质
信息呈现 靠网格、留白、分区组织,信息量受单屏面积限制 靠距离、遮挡、景深组织,信息可分层堆叠在纵深上 从"平面排布"变成"纵深编排"
导航 固定坐标系,页面跳转即视角切换 视角本身可移动,导航可以是"走过去"或"拉近看" 从"切换页面"变成"移动观察点"
反馈 颜色变化、缩放、位移,幅度小且以像素为单位 可叠加深度弹出、材质高光、空间音效、轻微透视偏移 反馈通道从 1 个扩到 3 个以上
状态 选中/禁用/加载用样式区分 除样式外,还需区分"在视野内/外""可触及/过远" 状态维度与用户姿态相关
层级 zIndex 只决定绘制顺序 Z 位置决定真实远近,参与遮挡与透视 层级从"视觉排序"变成"空间事实"
交互输入 点击、滑动、长按(触点即目标) 视线、手势、头部姿态协同,目标需要"选中"过程 输入从"直接点"变成"先瞄准再动作"

这张表里最容易被低估的是最后两列。层级和交互输入这两栏,直接决定了 2D 那套"所见即所点"的直觉不能照搬。

案例:设置中心从列表变成分层空间

拿最常见的设置中心举例。2D 版本是一列设置项,用户滚动、点进去、返回。空间化之后,可以让"一级入口"作为前景层平铺,"二级详情"沉到中景,环境/预览作为背景层。
#mermaid-svg-8UD1i13g8P6Kktdf{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-8UD1i13g8P6Kktdf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8UD1i13g8P6Kktdf .error-icon{fill:#552222;}#mermaid-svg-8UD1i13g8P6Kktdf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8UD1i13g8P6Kktdf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8UD1i13g8P6Kktdf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8UD1i13g8P6Kktdf .marker.cross{stroke:#333333;}#mermaid-svg-8UD1i13g8P6Kktdf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8UD1i13g8P6Kktdf p{margin:0;}#mermaid-svg-8UD1i13g8P6Kktdf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8UD1i13g8P6Kktdf .cluster-label text{fill:#333;}#mermaid-svg-8UD1i13g8P6Kktdf .cluster-label span{color:#333;}#mermaid-svg-8UD1i13g8P6Kktdf .cluster-label span p{background-color:transparent;}#mermaid-svg-8UD1i13g8P6Kktdf .label text,#mermaid-svg-8UD1i13g8P6Kktdf span{fill:#333;color:#333;}#mermaid-svg-8UD1i13g8P6Kktdf .node rect,#mermaid-svg-8UD1i13g8P6Kktdf .node circle,#mermaid-svg-8UD1i13g8P6Kktdf .node ellipse,#mermaid-svg-8UD1i13g8P6Kktdf .node polygon,#mermaid-svg-8UD1i13g8P6Kktdf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8UD1i13g8P6Kktdf .rough-node .label text,#mermaid-svg-8UD1i13g8P6Kktdf .node .label text,#mermaid-svg-8UD1i13g8P6Kktdf .image-shape .label,#mermaid-svg-8UD1i13g8P6Kktdf .icon-shape .label{text-anchor:middle;}#mermaid-svg-8UD1i13g8P6Kktdf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8UD1i13g8P6Kktdf .rough-node .label,#mermaid-svg-8UD1i13g8P6Kktdf .node .label,#mermaid-svg-8UD1i13g8P6Kktdf .image-shape .label,#mermaid-svg-8UD1i13g8P6Kktdf .icon-shape .label{text-align:center;}#mermaid-svg-8UD1i13g8P6Kktdf .node.clickable{cursor:pointer;}#mermaid-svg-8UD1i13g8P6Kktdf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8UD1i13g8P6Kktdf .arrowheadPath{fill:#333333;}#mermaid-svg-8UD1i13g8P6Kktdf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8UD1i13g8P6Kktdf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8UD1i13g8P6Kktdf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8UD1i13g8P6Kktdf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8UD1i13g8P6Kktdf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8UD1i13g8P6Kktdf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8UD1i13g8P6Kktdf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8UD1i13g8P6Kktdf .cluster text{fill:#333;}#mermaid-svg-8UD1i13g8P6Kktdf .cluster span{color:#333;}#mermaid-svg-8UD1i13g8P6Kktdf 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-8UD1i13g8P6Kktdf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8UD1i13g8P6Kktdf rect.text{fill:none;stroke-width:0;}#mermaid-svg-8UD1i13g8P6Kktdf .icon-shape,#mermaid-svg-8UD1i13g8P6Kktdf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8UD1i13g8P6Kktdf .icon-shape p,#mermaid-svg-8UD1i13g8P6Kktdf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8UD1i13g8P6Kktdf .icon-shape .label rect,#mermaid-svg-8UD1i13g8P6Kktdf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8UD1i13g8P6Kktdf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8UD1i13g8P6Kktdf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8UD1i13g8P6Kktdf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2D 路径
空间路径
用户进入设置中心
选择导航方式
滚动列表
点击条目
页面跳转 push 详情页
返回 pop 回列表
视线/手势选中前台卡片
卡片沿 Z 轴前移并放大
二级详情在背景层淡入
手势后推 = 返回上一层
卡片回落到原始 z 位置

两条路径的差别不只是动效。2D 路径里,返回是"撤销一次操作";空间路径里,返回是"退后一步"。后者更接近人在物理空间里的直觉,代价是用户必须始终知道自己在哪一层------这正是设计门槛,系统替不了你。

再往前一步,如果这个设置中心接入 3DGS 场景(比如相机类 App 让用户在实景空间里调整参数),设置项就不再是列表,而是悬浮在环境里的锚点。Spatial Recon Kit 加载 3DGS 模型后,配合 ArkGraphics 3D 的相机和光源,锚点会随视角变化产生合理的透视关系。这类场景里,设置项的空间位置本身就是语义:贴近实物的参数(比如滤镜强度)就该靠近那件实物。

几条小经验

  • 先问"Z 轴在这个页面承担什么职责",再动手写代码。如果答案只是"好看",大概率不该上 3D。
  • 把深度当层级用,别当装饰用。让用户在纵深里找到方向,比让元素飘起来更有价值。
  • 2D 到 3D 的第一步通常是"分层的 Stack",不是"加载一个 3D 模型"。绝大多数页面用层叠 + 透视就够了。
  • 透视值(perspective)要统一。同屏元素用不同视距,会让人眼判断不了它们的远近关系。
  • 动效时间控制在 300~500ms。空间位移过大又太慢,比平面动画更容易引发不适。

注意一下下哦

  • 把 zIndex 当成 Z 轴 。zIndex 只影响绘制顺序,不产生透视和遮挡的物理感。要真实纵深,得用 translate 的 z 配合 perspective。
  • rotate 和 scale 的锚点打架 。两个属性都设 centerX/centerY 时,以属性链里后设置的为准。写链式调用时别想当然。
  • 透视中心默认在组件自身。做整屏空间感时,往往需要把旋转锚点对齐到屏幕中心或观察者位置,否则所有元素各转各的。
  • 忽略可触及范围。平面里屏幕边缘还能点,空间里"远处"的控件既难点中也不该高频使用。
  • 只做视觉不做状态。元素进出视野、被遮挡、过远这些状态如果没设计,用户会以为自己操作失败了。

空间计算的门槛,一半在"怎么写",另一半在"为什么这么写"。系统把前一半按低了,后一半还得靠自己。

#鸿蒙 #HarmonyOS 7(API 26)

相关推荐
HwJack202 小时前
【HarmonyOS开发小实践】HarmonyOS GC 引用计数 vs 对象追踪,三种回收算法
jvm·算法·harmonyos
500842 小时前
React Native for OpenHarmony 实战:三方库 react-native-torch 的鸿蒙化适配指南
javascript·react native·react.js·harmonyos
动物园猫3 小时前
Compose Multiplatform 三方库 Coil(coil-core)的 OpenHarmony 鸿蒙化适配实战
华为·harmonyos
500843 小时前
React Native for OpenHarmony 实战:三方库 react-native-device-uptime 的鸿蒙化适配指南
javascript·分布式·react native·react.js·harmonyos
星辰徐哥12 小时前
鸿蒙平台 KDevelop 集成开发环境适配实战:基于 Electron 壳方案的跨平台多语言代码编辑器开发
electron·编辑器·harmonyos
熊猫钓鱼>_>12 小时前
开源鸿蒙平台 KMP 三方库 Essenty 适配全流程:从生命周期抽象到状态重建验证
华为·开源·harmonyos·鸿蒙·kmp·atomgit·essenty
李游Leo15 小时前
HarmonyOS 7 文搜图实战 04:图片增删、索引重建与异常恢复完善 SnapSeek 可维护性【鸿蒙心迹】
harmonyos
三掌柜66618 小时前
# ArkWeb 手记 03|把 JSBridge 做成协议
harmonyos
轻口味19 小时前
HarmonyOS 7 新特性5:平行视界——轻视界文集的两栏阅读与窗口状态边界矩阵真机实测
华为·矩阵·harmonyos·鸿蒙·平行视界