【共创稿事节】HarmonyOS 7深度即层级:用 Z 轴组织信息的交互设计
2D 界面表达层级靠的是位置和大小:重要的放上面、放大、加粗。空间界面多了一个可用的维度——深度。用户天生会用"远近"理解"重要程度",前景的东西就是此刻该看的,背景的东西是环境。把 Z 轴当成一条层级轴来用,界面会立刻变得有主次,而且不用靠加粗和描边硬撑。
为什么深度能表达层级
深度和层级之间的映射,不是设计师拍脑袋定的,跟人的知觉习惯有关。几个稳定的规律:
- 近大远小。距离越近,物体在视野里占比越大、细节越清晰,天然更吸引注意。这是透视在替你做视觉权重。
- 前景清晰、背景虚化。相机的景深效果,人从小看到大,已经把"清晰=焦点、模糊=背景"写进了直觉。
- 遮挡即优先。前面挡住后面的,被挡的就自动退到次要位置,这是遮挡关系最原始的信息编码。
- 视差与位移。用户移动时,近处的物体位移大、远处的位移小,深度差会自动强化层次感。
所以 Z 轴并不只是一个坐标,它是一条可以被"读"的语义轴。近 = 当下焦点,中 = 内容主体,远 = 环境上下文。 设计师要做的,是把信息的重要性映射到这条轴上。
Z 轴的三个语义带
给空间界面定三档深度,每档对应一个语义带,比无级地"随便挪"要好管得多:
| 语义带 | z 参考值 | 视觉特征 | 承载内容 | 交互 |
|---|---|---|---|---|
| 前景 / 焦点 | +80 ~ +160 vp | 大、亮、清晰、有阴影 | 主任务、详情、弹层 | 点按、输入 |
| 中景 / 内容 | -40 ~ -80 vp | 正常尺度、正常亮度 | 列表、卡片、图文 | 滚动、选择 |
| 背景 / 环境 | -160 ~ -260 vp | 小、暗、虚化、无阴影 | 背景氛围、空间锚点 | 无 |
参考值的意义是让团队用同一套"刻度"说话,而不是每次拍脑袋。数值本身可以调,但档与档之间要拉开足够差距,z 相差太小,透视压缩后几乎看不出区别。
一个常见误区:以为把元素 translate 到更近的 z 就会变大。在 ArkUI 里,translate 的 z 分量只有在透视生效时才会改变视觉大小,而透视由 rotate 的 perspective 参数控制(API 10 起支持)。没开透视,z 平移就是纯粹的前后遮挡位移。
zIndex 与 z 坐标:别把它们混着用
这两个都带个"z",但管的事完全不同,是空间布局里最容易绕晕的一对概念。
zIndex 管渲染顺序(谁盖住谁)。 它是同一容器内兄弟组件的堆叠顺序,值越大显示层级越高。它不影响透视、不影响大小,纯靠声明顺序和数值决定覆盖。跨容器的组件,zIndex 是无效的。
z 坐标管空间位置(谁离用户近)。 通过 translate({ z })、rotate 的 centerZ、scale 的 z,或者 transform3D 矩阵来改。它决定透视下的尺度和视差,但不管遮挡——一个 z 更近的元素,如果 zIndex 更低,照样可能被盖住。
正确的配合方式是:用 z 坐标定义层次关系,用 zIndex 保证遮挡顺序跟着层次走。 别指望只动一个。
// 用常量把"语义层级"固化下来,避免代码里散落魔数
const enum DepthBand {
Background = -200, // 环境层
Content = -60, // 内容层
Focus = 120 // 焦点层
}
// zIndex 也按层级给,保证遮挡与深度一致
const enum LayerOrder {
Background = 0,
Content = 1,
Focus = 10
}
前后层切换:用状态驱动深度
空间层级切换的骨架很简单:一个状态表示"当前哪层在前",深度和虚化都由它推导出来。下面是一个可复用的实现。
// DepthStage.ets —— 用 z 坐标 + zIndex 组织三层,并支持前后层切换
@Component
export struct DepthStage {
// expanded 为 true 时,焦点层前移,其余层后退
@State expanded: boolean = false
private readonly perspective: number = 900
// 统一的深度推导,避免各处硬编码
private backgroundZ(): number { return this.expanded ? -280 : -200 }
private contentZ(): number { return this.expanded ? -120 : -60 }
private focusZ(): number { return this.expanded ? 140 : 0 }
build() {
Stack({ alignContent: Alignment.Center }) {
// 环境层
Column()
.width('150%').height('150%')
.backgroundColor('#0E1220')
.translate({ z: this.backgroundZ() })
.rotate({ perspective: this.perspective })
.blur(this.expanded ? 20 : 4) // 展开时背景更虚
.zIndex(0)
// 内容层
Column() { this.contentCard() }
.translate({ z: this.contentZ() })
.rotate({ perspective: this.perspective })
.opacity(this.expanded ? 0.55 : 1) // 后退时降透明度,进一步让位
.zIndex(1)
// 焦点层:默认收起在内容层附近,展开时前移
Column() { this.detailPanel() }
.width(this.expanded ? 360 : 0)
.translate({ z: this.focusZ() })
.rotate({ perspective: this.perspective })
.zIndex(10)
}
.width('100%').height('100%')
.onClick(() => {
// 一次动画同时改三个深度,层次切换才连贯
this.getUIContext()?.animateTo(
{ duration: 380, curve: Curve.Friction },
() => { this.expanded = !this.expanded }
)
})
}
@Builder
contentCard() {
Column({ space: 8 }) {
Text('云雾乌龙').fontSize(18).fontColor(Color.White)
Text('¥ 88 / 罐').fontSize(16).fontColor('#FF9AA2')
}
.padding(20).borderRadius(16).backgroundColor('#1A2238')
}
@Builder
detailPanel() {
Column({ space: 12 }) {
Text('产地与工艺').fontSize(18).fontColor(Color.White)
Text('轻发酵,炭焙提香,适合 90℃ 水温。')
.fontSize(14).fontColor('#C7CEDB')
}
.padding(24).borderRadius(20).backgroundColor('#2A3350')
}
}
这段代码里有三处安排是刻意的:
- 深度集中推导。 三个
xZ()函数把语义带和状态解耦,以后调参数只改一处。 - 虚化跟着状态走。 背景层
blur随展开加深,配合前景前移,景深关系才成立。 - 一个
animateTo改多个属性。 层次切换是整体的,分几段动画会显得"各走各的",破坏空间感。
案例:卡片详情展开的空间化
把上面的骨架套到商品卡片的"点开看详情"。2D 里的做法是卡片放大、翻页或者弹出新页面,用户会丢失"我从哪来的"这个上下文。空间化后,处理方式变成:
- 卡片本体留在中景原位,稍微后退并虚化,它还在那儿,告诉用户"你从这张卡来";
- 详情面板从卡片位置前移到焦点层,成为当前唯一可读对象;
- 关闭时,详情退回中景、与卡片合并,视觉上是"收回去",而不是"换了个页面"。
// 关键状态与关闭手势
@State detailVisible: boolean = false
// 展开:面板前移 + 原卡片后退虚化
private openDetail() {
this.getUIContext()?.animateTo({ duration: 360, curve: Curve.Friction }, () => {
this.detailVisible = true
})
}
// 关闭:面板退回,卡片复位
private closeDetail() {
this.getUIContext()?.animateTo({ duration: 300, curve: Curve.Friction }, () => {
this.detailVisible = false
})
}
前后层切换的对照
| 处理方式 | 常用 API | 效果 | 适用场景 |
|---|---|---|---|
| 只改 zIndex | zIndex | 元素平移前后遮挡,无纵深 | 浮层、Toast |
| 改 z 坐标 | translate({ z }) + perspective | 有透视缩放和视差 | 层级切换、聚焦 |
| 缩放模拟 | scale | 近大远小,但无视差 | 轻量聚焦 |
| 矩阵变换 | transform3D | 完整的 3D 变换 | 复杂 3D 排布 |
总结一下下
- 语义先定,数值后调。 先把三档语义带定下来,再让设计稿去贴合,比反向凑数值省事。
- 焦点层同一时刻只留一个。 两个东西同时"最近",用户不知道该看谁。
- 虚化要配合后退。 只虚化不后退,看着像图层蒙了层雾;只后退不虚化,背景又会抢戏。
- 给动画留时间。 深度切换 300~400ms 比较舒服,太快会晕,太慢会拖沓。
- 中景是"家",别让焦点层把中景遮死。 留出可见的边缘,用户才有返回的参照。
容易出问题的地方
- 把
zIndex当深度用。 结果是一堆浮层贴在同一个平面上,越叠越乱,没有空间感。 - 跨容器用
zIndex。 不同 Stack 容器里的组件,zIndex无法跨容器改变层级,调了也没反应。 - 动态改
zIndex忽略稳定排序。 在不增删兄弟节点时,zIndex变化会在原层级顺序上稳定排序,结果可能和"值大者在上"的直觉不一致。 - 细节面板宽度直接从 0 动画到 360。 只改宽度会触发反复重排,改
scale或opacity更顺滑。 - 焦点层不限制命中区域。 前移之后它的触摸区域也放大了,容易误触到后面本该被挡住的控件,建议焦点态下让背景层
hitTestBehavior设为None。
更多推荐
所有评论(0)