鸿蒙新特性实战:matrix4 矩阵变换打造 3D 变换实验室
前言
在 HarmonyOS NEXT 的声明式 UI 体系里,我们平时用得最多的空间变换属性是 .rotate()、.scale()、.translate() 这一类"单一维度"的属性。它们足够应付大多数场景,但一旦你想做出更有层次感的效果——比如卡片翻转、书页展开、立体倾斜、组合动画——单独叠加这些属性就会变得笨拙且难以控制先后顺序。
这时候就该请出 ArkUI 提供的底层能力:@ohos.matrix4(在最新 Kit 化的写法里从 @kit.ArkUI 导出的 matrix4)。它把所有二维/三维空间变换统一抽象成一个 4×4 的齐次变换矩阵,你可以任意组合旋转、缩放、平移、斜切,用一个矩阵一次性作用到组件上,还能反过来用这个矩阵去计算任意坐标点变换后的位置。
本文将带你从矩阵变换的原理讲起,用一个可交互的"3D 变换实验室"页面把 matrix4 的核心 API 全部串起来:实时滑块调参、预览卡片的 3D 姿态变化、六种预设组合一键应用,以及 transformPoint 坐标点变换演示。全文包含完整可运行代码,适合有一定 ArkUI 基础、想深入理解空间变换的中级开发者。
一、为什么需要 matrix4
1.1 单一变换属性的局限
假设你想让一张卡片"先绕 Y 轴旋转 30 度,再整体放大 1.3 倍,最后向右平移 20"。用属性叠加你可能会写:
Column() { /* ... */ }
.rotate({ x: 0, y: 1, z: 0, angle: 30 })
.scale({ x: 1.3, y: 1.3 })
.translate({ x: 20, y: 0 })
这看上去没问题,但有几个隐患:
- 顺序耦合在属性链里,难以在运行时动态调整变换的组合次序。
- 每个属性都是一次独立的图形运算,叠加多个属性时,框架内部要做多次矩阵合成。
- 无法反向计算——你没法问框架"这个变换把点 (100, 100) 变到了哪里"。
matrix4 的思路完全不同:它先在数据层把所有变换合成成一个矩阵,再把这个"结果矩阵"一次性交给组件渲染。变换的组合、顺序、复用都在你自己手里。
1.2 什么是 4×4 变换矩阵
在计算机图形学里,二维/三维空间中的旋转、缩放、平移、斜切都可以用矩阵乘法统一表示。为了让"平移"也能纳入矩阵乘法(平移本质是加法),数学上引入了齐次坐标,把 3 维点扩展成 4 维 (x, y, z, 1),于是所有变换都能写成一个 4×4 矩阵:
| m00 m01 m02 m03 |
| m10 m11 m12 m13 |
| m20 m21 m22 m23 |
| m30 m31 m32 m33 |
- 左上角 3×3 子矩阵负责旋转、缩放、斜切;
- 第 4 列
m03/m13/m23负责平移; m30/m31/m32在做透视投影时用到(3D 旋转的近大远小效果就来自这里)。
你不需要手算这些数字,matrix4 提供了一整套链式 API 帮你构建。理解这个模型有一个直接好处:多个变换合成 = 多个矩阵相乘,而矩阵乘法不满足交换律——这就是为什么"先旋转后平移"和"先平移后旋转"结果完全不同。
二、matrix4 核心 API 速览
引入方式(推荐 Kit 化写法):
import { matrix4 } from '@kit.ArkUI';
2.1 创建矩阵
// 单位矩阵(恒等变换,什么都不改变)
let m1: matrix4.Matrix4Transit = matrix4.identity();
// 用 16 个数字自定义一个矩阵(列优先)
let m2: matrix4.Matrix4Transit = matrix4.init([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
// 复制一个已有矩阵
let m3: matrix4.Matrix4Transit = m1.copy();
Matrix4Transit 是矩阵对象的类型,后续所有变换方法都返回它本身,因此可以链式调用。
2.2 组合变换
let matrix = matrix4.identity()
.rotate({ x: 0, y: 1, z: 0, angle: 45 }) // 绕 Y 轴旋转 45°
.scale({ x: 1.5, y: 1.5, z: 1 }) // XY 放大 1.5 倍
.translate({ x: 30, y: 10, z: 0 }) // 平移
.skew(0.2, 0); // 斜切
各方法参数含义:
| 方法 | 参数 | 说明 |
|---|---|---|
rotate |
{ x, y, z, angle, centerX?, centerY? } |
(x,y,z) 是旋转轴向量,angle 为角度 |
scale |
{ x, y, z, centerX?, centerY? } |
各轴缩放比例,可指定缩放中心 |
translate |
{ x, y, z } |
各轴平移量(单位 vp) |
skew |
(x, y) |
X/Y 方向斜切因子 |
combine |
(other) |
把另一个矩阵与当前矩阵相乘合成 |
invert |
无 | 求逆矩阵(常用于把屏幕坐标反算回原始坐标) |
2.3 应用到组件
matrix4 构建好的矩阵直接传给组件的 .transform() 属性:
Column() { /* 卡片内容 */ }
.transform(matrix)
2.4 坐标点变换 transformPoint
这是 matrix4 相比普通变换属性最独特的能力:
let matrix = matrix4.identity().translate({ x: 50, y: 20 });
let result: [number, number] = matrix.transformPoint([100, 100]);
// result => [150, 120]
它返回一个点在变换后的新坐标。做拖拽命中检测、手势坐标还原、自定义绘图时非常有用。


三、实战:3D 变换实验室
下面我们把这些 API 全部落地到一个可交互页面。页面结构如下:
- 预览区:一张渐变卡片,实时应用当前组合矩阵,展示 3D 姿态;
- 参数摘要条:显示当前旋转/缩放的关键数值;
- Tab 控制区:分"旋转 / 缩放 / 平移"三组滑块,拖动即时更新预览;
- 预设组合:六个一键应用的组合变换(恒等、水平翻转、立体卡片、倾斜放大、书页展开、角标缩小);
- transformPoint 演示:用当前矩阵计算点 (100, 100) 变换后的坐标;
- 重置按钮:一键回到单位矩阵。
3.1 状态与数据结构
import { router, matrix4 } from '@kit.ArkUI';
import { FontSize, Spacing } from '../common/Constants';
interface PresetItem {
name: string;
rx: number; ry: number; rz: number;
sx: number; sy: number;
tx: number; ty: number;
}
@Entry
@Component
struct TransformLabPage {
@State rotateX: number = 0;
@State rotateY: number = 0;
@State rotateZ: number = 0;
@State scaleX: number = 1;
@State scaleY: number = 1;
@State translateX: number = 0;
@State translateY: number = 0;
@State activeTab: number = 0;
@State pointX: number = 100;
@State pointY: number = 100;
@State resultX: number = 100;
@State resultY: number = 100;
private presets: PresetItem[] = [
{ name: '恒等', rx: 0, ry: 0, rz: 0, sx: 1, sy: 1, tx: 0, ty: 0 },
{ name: '水平翻转', rx: 0, ry: 180, rz: 0, sx: 1, sy: 1, tx: 0, ty: 0 },
{ name: '立体卡片', rx: 20, ry: -30, rz: 0, sx: 1, sy: 1, tx: 0, ty: 0 },
{ name: '倾斜放大', rx: 0, ry: 0, rz: 15, sx: 1.3, sy: 1.3, tx: 0, ty: 0 },
{ name: '书页展开', rx: 0, ry: -60, rz: 0, sx: 1, sy: 1, tx: 20, ty: 0 },
{ name: '角标缩小', rx: 0, ry: 0, rz: -8, sx: 0.6, sy: 0.6, tx: -40, ty: -40 }
];
}
用 @State 把每个变换维度拆成独立的响应式变量,任何一个改变都会触发 build() 重新执行,进而重新构建矩阵、刷新预览。
3.2 核心:动态构建矩阵
这是整个页面的心脏。我们把 7 个状态变量按固定顺序合成一个矩阵:
// 根据当前状态组合出一个 4x4 变换矩阵
private buildMatrix(): matrix4.Matrix4Transit {
return matrix4.identity()
.rotate({ x: 1, y: 0, z: 0, angle: this.rotateX })
.rotate({ x: 0, y: 1, z: 0, angle: this.rotateY })
.rotate({ x: 0, y: 0, z: 1, angle: this.rotateZ })
.scale({ x: this.scaleX, y: this.scaleY })
.translate({ x: this.translateX, y: this.translateY });
}
这里有几个关键点:
- 每次都从
identity()起步,保证矩阵不会在多次渲染中累积误差。 - 三个
rotate分别绕 X/Y/Z 轴,这样滑块可以独立控制每个轴的旋转角度。 - 顺序固定为"旋转 → 缩放 → 平移",符合大多数人的直觉;如果你把 translate 放到 rotate 前面,平移方向会随旋转一起被"扭"过去,得到完全不同的结果。这正是矩阵乘法不可交换的直观体现。
在 build() 里直接调用它:
Column() {
Text('3D').fontSize(40).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text('matrix4 变换预览').fontSize(FontSize.CAPTION).fontColor('#FFFFFFCC')
}
.width(180).height(120)
.borderRadius(16)
.linearGradient({ angle: 135, colors: [['#6366F1', 0], ['#8B5CF6', 1]] })
.shadow({ radius: 20, color: '#6366F160', offsetX: 0, offsetY: 10 })
.transform(this.buildMatrix()) // ← 一行接入
注意:
.transform()接受的是 matrix4 构建出的Matrix4Transit对象,而不是普通的对象字面量。因为buildMatrix()是一个返回值的方法调用(表达式),可以直接写在 UI 描述里;但不能在声明式 UI 块里写let x = ...这样的语句,否则编译会报 “Only UI component syntax can be written here”。若需要中间变量,请封装成方法返回。
3.3 滑块控制区
用一个可复用的 @Builder 封装"标签 + 数值 + 滑块":
@Builder
sliderRow(label: string, value: number, min: number, max: number,
onChange: (v: number) => void) {
Column() {
Row() {
Text(label).fontSize(FontSize.CAPTION).fontColor('#475569')
Blank()
Text(value.toString())
.fontSize(FontSize.CAPTION).fontColor('#6366F1').fontWeight(FontWeight.Bold)
}.width('100%')
Slider({ value: value, min: min, max: max, step: (max - min) > 10 ? 1 : 0.1 })
.trackColor('#E2E8F0')
.selectedColor('#6366F1')
.blockColor('#6366F1')
.onChange((v: number) => { onChange(v); })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(10)
.padding({ left: Spacing.MD, right: Spacing.MD, top: 10, bottom: 10 })
}
根据当前 Tab 渲染不同的滑块组:
Column() {
if (this.activeTab === 0) {
this.sliderRow('绕 X 轴旋转', this.rotateX, -180, 180,
(v: number) => { this.rotateX = Math.round(v); })
this.sliderRow('绕 Y 轴旋转', this.rotateY, -180, 180,
(v: number) => { this.rotateY = Math.round(v); })
this.sliderRow('绕 Z 轴旋转', this.rotateZ, -180, 180,
(v: number) => { this.rotateZ = Math.round(v); })
} else if (this.activeTab === 1) {
this.sliderRow('X 轴缩放', this.scaleX, 0.3, 2,
(v: number) => { this.scaleX = Math.round(v * 10) / 10; })
this.sliderRow('Y 轴缩放', this.scaleY, 0.3, 2,
(v: number) => { this.scaleY = Math.round(v * 10) / 10; })
} else {
this.sliderRow('X 轴平移', this.translateX, -120, 120,
(v: number) => { this.translateX = Math.round(v); })
this.sliderRow('Y 轴平移', this.translateY, -120, 120,
(v: number) => { this.translateY = Math.round(v); })
}
}
这里对旋转和平移做 Math.round(v) 取整,对缩放保留一位小数 Math.round(v * 10) / 10,让数值显示更干净。Slider 的 step 也按量程动态调整:大量程(角度、平移)步长为 1,小量程(缩放)步长为 0.1。
3.4 预设组合一键应用
private applyPreset(p: PresetItem): void {
this.rotateX = p.rx;
this.rotateY = p.ry;
this.rotateZ = p.rz;
this.scaleX = p.sx;
this.scaleY = p.sy;
this.translateX = p.tx;
this.translateY = p.ty;
}
用 Flex 流式布局把六个预设铺开:
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.presets, (p: PresetItem) => {
Text(p.name)
.fontSize(FontSize.CAPTION)
.fontColor('#6366F1')
.backgroundColor('#EEF2FF')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ left: Spacing.SM, bottom: Spacing.SM })
.onClick(() => { this.applyPreset(p); })
})
}
点击任意一个,预设的七个参数会一次性写入状态,预览卡片随即变成对应姿态。这几个预设本身就是很好的教学案例:
- 水平翻转(ry=180):像翻卡片一样,看到卡片"背面"。
- 立体卡片(rx=20, ry=-30):同时绕两个轴旋转,产生真实的 3D 悬浮感,3D 旋转自带的透视让远端边缘变窄。
- 书页展开(ry=-60 + tx=20):模拟书页翻开的透视效果。
- 角标缩小(rz=-8 + 缩小 + 左上平移):像把卡片收进角落变成一个小角标。
3.5 transformPoint 坐标点变换
这一段展示 matrix4 独有的"反问"能力——给定一个点,算出它经过当前矩阵变换后落在哪里:
// 使用当前矩阵把一个坐标点变换到新位置
private computePoint(): void {
const out: [number, number] =
this.buildMatrix().transformPoint([this.pointX, this.pointY]);
this.resultX = Math.round(out[0] * 100) / 100;
this.resultY = Math.round(out[1] * 100) / 100;
}
对应的 UI:
Column() {
Text('坐标点变换 transformPoint')
.fontSize(FontSize.BODY).fontColor('#1F2937').fontWeight(FontWeight.Bold)
Row() {
Text('原点 (' + this.pointX + ', ' + this.pointY + ')')
.fontSize(FontSize.CAPTION).fontColor('#6B7280')
Blank()
Text('→').fontSize(FontSize.MEDIUM).fontColor('#9CA3AF')
Blank()
Text('(' + this.resultX + ', ' + this.resultY + ')')
.fontSize(FontSize.CAPTION).fontColor('#6366F1').fontWeight(FontWeight.Bold)
}.width('100%')
Button('用当前矩阵变换 (100, 100)')
.backgroundColor('#6366F1').fontColor('#FFFFFF')
.borderRadius(8).width('100%').height(40)
.onClick(() => { this.computePoint(); })
}
拖动滑块改变矩阵后点这个按钮,你会看到点 (100, 100) 的变换结果同步变化。比如设置平移 X=50、Y=20 后,结果就是 (150, 120);再叠加缩放,结果又会成比例放大。这套坐标变换在实际项目里非常实用:做自定义拖拽时,你可以用变换矩阵计算元素落点;做手势缩放画布时,可以用逆矩阵 invert() 把手指的屏幕坐标还原回画布坐标。
3.6 完整页面骨架
把上面的片段组装起来,build() 的主体结构如下:
build() {
Column() {
// 标题栏
Row() {
Text('‹').fontSize(28).fontColor('#FFFFFF')
.onClick(() => { router.back(); })
Text('3D 变换实验室').fontSize(FontSize.TITLE)
.fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Blank()
Text('matrix4').fontSize(FontSize.CAPTION).fontColor('#FFFFFFCC')
}
.backgroundColor('#0F172A')
.padding({ left: Spacing.LG, right: Spacing.LG, top: 14, bottom: 14 })
Scroll() {
Column() {
// 预览区 → 参数摘要 → Tab → 滑块 → 预设 → transformPoint → 重置
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%').height('100%')
.backgroundColor('#F1F5F9')
}
Tab 切换按钮和参数摘要也各用一个 @Builder 抽出来,减少重复代码:
@Builder
tabBtn(label: string, index: number) {
Text(label)
.fontColor(this.activeTab === index ? '#FFFFFF' : '#64748B')
.backgroundColor(this.activeTab === index ? '#6366F1' : '#E2E8F0')
.borderRadius(8).layoutWeight(1).textAlign(TextAlign.Center).height(38)
.margin({ left: 4, right: 4 })
.onClick(() => { this.activeTab = index; })
}
@Builder
paramChip(label: string, value: string) {
Column() {
Text(value).fontSize(FontSize.BODY).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text(label).fontSize(10).fontColor('#94A3B8').margin({ top: 2 })
}
}
四、深入理解:几个容易踩的坑
4.1 变换顺序决定结果
前面反复强调过,这里用一个具体例子说明。假设我们要"绕 Z 轴转 90 度并向右平移 100":
// 写法 A:先旋转再平移
matrix4.identity().rotate({ x:0,y:0,z:1, angle:90 }).translate({ x:100, y:0 });
// 写法 B:先平移再旋转
matrix4.identity().translate({ x:100, y:0 }).rotate({ x:0,y:0,z:1, angle:90 });
写法 A 中,平移是在"已经旋转过的坐标系"里进行,向右的方向已经被转到了向下,于是卡片实际是向下移动;写法 B 中先平移后旋转,卡片先右移再整体绕原点转。两者视觉结果截然不同。记住:链式调用越靠后的变换,越先作用于"局部坐标系"。 当你的效果不符合预期,第一件事就是检查变换顺序。
4.2 旋转中心 centerX/centerY
rotate 和 scale 都支持 centerX/centerY 指定变换的锚点。默认锚点是组件中心。如果你想做"以左上角为轴翻页"的效果,就要显式指定:
matrix4.identity().rotate({ x:0, y:1, z:0, angle:60, centerX: 0, centerY: 0 });
不指定时以中心为轴,指定后旋转/缩放的视觉支点会随之改变,这在做展开、折叠类动效时非常关键。
4.3 UI 块内不能写普通语句
这是 ArkTS 声明式语法的硬性限制。下面这种写法会导致编译错误 Only UI component syntax can be written here:
Column() {
let m = this.buildMatrix(); // ❌ 编译报错
Text('x').transform(m)
}
正确做法是把中间计算封装到方法里,在 UI 中只写方法调用表达式:
Column() {
Text('x').transform(this.buildMatrix()) // ✅
}
4.4 结合动画
matrix4 的变换可以和 animateTo 结合,做出丝滑的过渡。因为矩阵作用于 .transform(),而 transform 是可动画属性,只要在动画闭包里修改驱动矩阵的状态变量,框架就会自动插值:
animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
this.rotateY = 180; // 状态变化触发 transform 重算,自动补间
});
这样点击预设时如果包在 animateTo 里,卡片就会平滑地"转"过去,而不是瞬间跳变。你可以在本文页面的 applyPreset 里试着包一层 animateTo 感受差异。
五、matrix4 的典型应用场景
理解了这套 API,以下场景都能轻松实现:
- 翻转卡片:绕 Y 轴 0→180 度动画,配合中途切换正反面内容,做成记忆卡片、答案揭晓等交互。
- 3D 轮播/画廊:给左右两侧卡片施加不同的 Y 轴旋转 + 缩放,营造纵深感的 Cover Flow 效果。
- 书页/折叠动效:指定左侧为旋转锚点,做出书本翻页、抽屉展开。
- 自定义拖拽命中检测:用
transformPoint把手指坐标映射到变换后的元素坐标系,精确判断点击落在哪个已变换的元素上。 - 画布缩放平移:维护一个"视图矩阵",所有内容统一
.transform(),手势时用invert()把屏幕坐标还原成画布坐标。
相比零散的 rotate/scale/translate 属性,matrix4 的优势在于把变换当作数据来管理:可以存储、复制、合成、求逆、反算坐标,这是构建复杂交互式图形界面的基础能力。
六、运行效果
将 TransformLabPage.ets 放入 entry/src/main/ets/pages/ 目录,在 main_pages.json 注册路由 pages/TransformLabPage,并在首页入口列表添加一张跳转卡片即可。运行后:
- 拖动"旋转"Tab 下的三个滑块,预览卡片会实时呈现绕 X/Y/Z 轴的 3D 姿态,3D 旋转自带的透视让远端明显变窄;
- 切到"缩放""平移"Tab,继续叠加效果;
- 点击"立体卡片""书页展开"等预设,卡片一键切换到设计好的组合姿态;
- 点击 transformPoint 按钮,查看点 (100, 100) 经过当前矩阵后的精确坐标;
- 点击"重置全部参数"回到单位矩阵。
运行截图由开发者在真机/模拟器上自行运行获取。
七、小结
本文以一个"3D 变换实验室"为载体,完整讲解了 HarmonyOS NEXT 中 matrix4 矩阵变换的用法:
- 创建矩阵:
identity()/init()/copy(); - 组合变换:
rotate/scale/translate/skew/combine/invert; - 应用到组件:
.transform(matrix); - 坐标点变换:
transformPoint([x, y]),这是普通变换属性无法替代的独特能力; - 关键原理:4×4 齐次矩阵、变换顺序不可交换、旋转中心锚点、UI 块内不能写语句、结合 animateTo 做过渡动画。
matrix4 是 ArkUI 空间变换的底层基石。当你想做的效果超出单一 rotate/scale/translate 属性的表达能力时,把变换抽象成矩阵去管理,会让代码更可控、更可复用。掌握它,你就拿到了打造 3D 卡片、翻页动效、缩放画布等高级交互的钥匙。
更多推荐




所有评论(0)