前言

在 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 })

这看上去没问题,但有几个隐患:

  1. 顺序耦合在属性链里,难以在运行时动态调整变换的组合次序。
  2. 每个属性都是一次独立的图形运算,叠加多个属性时,框架内部要做多次矩阵合成。
  3. 无法反向计算——你没法问框架"这个变换把点 (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 });
}

这里有几个关键点:

  1. 每次都从 identity() 起步,保证矩阵不会在多次渲染中累积误差。
  2. 三个 rotate 分别绕 X/Y/Z 轴,这样滑块可以独立控制每个轴的旋转角度。
  3. 顺序固定为"旋转 → 缩放 → 平移",符合大多数人的直觉;如果你把 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

rotatescale 都支持 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,以下场景都能轻松实现:

  1. 翻转卡片:绕 Y 轴 0→180 度动画,配合中途切换正反面内容,做成记忆卡片、答案揭晓等交互。
  2. 3D 轮播/画廊:给左右两侧卡片施加不同的 Y 轴旋转 + 缩放,营造纵深感的 Cover Flow 效果。
  3. 书页/折叠动效:指定左侧为旋转锚点,做出书本翻页、抽屉展开。
  4. 自定义拖拽命中检测:用 transformPoint 把手指坐标映射到变换后的元素坐标系,精确判断点击落在哪个已变换的元素上。
  5. 画布缩放平移:维护一个"视图矩阵",所有内容统一 .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 卡片、翻页动效、缩放画布等高级交互的钥匙。

Logo

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

更多推荐