在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言
1.1 从静态到动态:为什么需要旋转动画
在移动应用设计中,动画是最重要的交互手段之一。一个没有动画的应用给人的感觉是「生硬」的——按钮按下没有反馈、页面跳转没有过渡、加载过程没有指示。而在众多动画类型中,旋转动画是最基础、最常用、也是视觉冲击力最强的一种。

旋转动画的应用场景无处不在:

加载指示器:App 启动时的旋转 Logo、页面加载中的转圈动画
交互反馈:开关切换的旋转变换、菜单弹出的旋转展开
3D 效果:卡片翻转、立体轮播、翻牌游戏
趣味细节:Emoji 表情的旋转入场、图标悬停旋转
品牌展示:Logo 的旋转展示、品牌吉祥物的旋转动画
在 HarmonyOS NEXT 中,ArkUI 框架提供了强大的 rotate() 属性和配套的动画系统,让开发者可以用简洁的声明式代码实现复杂的旋转动画效果。

1.2 本文核心内容
本文将基于 RotateAnimation.ets 这个完整的示例项目,深入解析鸿蒙原生旋转动画的技术体系:

rotate() 属性详解:angle、x/y/z 旋转轴、centerX/centerY 旋转中心
ArkTS 动画体系:隐式动画(.animation())与显式动画(animateTo())的对比与选型
六大演示场景逐段精析:从基础旋转到 3D 翻转、从持续旋转到组合动画
跨平台对比:与 iOS Core Animation / Android Property Animation / CSS transform 的对比
性能优化与常见陷阱:让动画更流畅、代码更健壮
1.3 前置知识
阅读本文需要:

了解 ArkTS 语言基本语法(@Component、@State、build() 方法)
了解鸿蒙 UI 组件的基本用法(Column、Row、Button、Text 等)
对 CSS transform 或移动端动画有基本概念会更有帮助,但不是必须
二、rotate() API 深度解析
2.1 API 签名
.rotate(value: RotateOptions)
RotateOptions 接口定义如下:

declare interface RotateOptions {
/** 旋转角度(单位:度)。正值为顺时针,负值为逆时针 */
angle: number;

/** 绕 X 轴旋转的因子。0 = 不绕 X 轴旋转,1 = 完全绕 X 轴旋转 */
x?: number;

/** 绕 Y 轴旋转的因子。0 = 不绕 Y 轴旋转,1 = 完全绕 Y 轴旋转 */
y?: number;

/** 绕 Z 轴旋转的因子。0 = 不绕 Z 轴旋转,1 = 完全绕 Z 轴旋转 */
z?: number;

/** 旋转中心点的 X 坐标。百分比(如 ‘50%’)或像素值(如 100) */
centerX?: number | string;

/** 旋转中心点的 Y 坐标。百分比(如 ‘50%’)或像素值(如 100) */
centerY?: number | string;
}
2.2 angle —— 旋转角度
angle 是 rotate() 最核心的参数,它决定了旋转的幅度:

正值(如 45):组件顺时针旋转
负值(如 -45):组件逆时针旋转
值域:无限制。可以设置为 360 表示完整一圈,720 表示两圈,以此类推
重要概念: rotate 是「变换」而非「布局属性」。这意味着 rotate() 改变的是组件的视觉呈现,而不改变组件在布局中占用的空间。旋转后的组件可能会视觉上覆盖到相邻元素,但相邻元素的位置不会因此被挤开。

// 顺时针旋转 45°
.rotate({ angle: 45 })

// 逆时针旋转 90°
.rotate({ angle: -90 })

// 旋转两圈
.rotate({ angle: 720 })
2.3 x / y / z —— 三维旋转轴
rotate() 支持三维旋转,通过 x、y、z 参数控制绕哪个轴旋转:

参数 控制轴 视觉效果 典型应用
z(默认) Z 轴(垂直于屏幕) 平面内旋转,像转盘 加载动画、钟表指针
y Y 轴(水平轴) 左右翻转,像翻书 3D 卡片翻转、翻牌效果
x X 轴(垂直轴) 上下翻转,像翻日历 3D 立体轮播
默认行为: 当不指定 x、y、z 时,默认为绕 Z 轴旋转(即 z=1)。这也是最常见的 2D 平面旋转。

三维旋转示例:

// 绕 Z 轴旋转(2D 平面旋转,默认)
.rotate({ angle: 45 }) // 等价于 { angle: 45, z: 1 }

// 绕 Y 轴旋转(3D 翻转效果)
.rotate({ x: 0, y: 1, z: 0, angle: 180 }) // 完全翻转到背面

// 绕 X 轴旋转(上下翻转)
.rotate({ x: 1, y: 0, z: 0, angle: 180 }) // 上下颠倒
2.4 centerX / centerY —— 旋转中心
默认情况下,组件的旋转中心是它的几何中心(即 centerX: ‘50%’, centerY: ‘50%’)。通过修改这两个参数,可以让组件绕任意点旋转。

// 绕右下角旋转(像开门)
.rotate({
angle: 45,
centerX: ‘100%’, // 右边缘
centerY: ‘100%’ // 底边缘
})

// 绕左上角旋转
.rotate({
angle: 45,
centerX: 0,
centerY: 0
})

// 绕顶部中心旋转(像钟摆)
.rotate({
angle: 30,
centerX: ‘50%’,
centerY: ‘0%’ // 顶部边缘
})
中心点取值类型:

字符串百分比:‘50%’、‘100%’、‘0%’
数值像素:100、0(单位 vp)
默认值:‘50%’
2.5 与 CSS transform 的类比
对于有 Web 开发经验的读者,rotate() 属性与 CSS 的 transform: rotate() 非常相似:

鸿蒙 ArkTS CSS 等效
.rotate({ angle: 45 }) transform: rotate(45deg)
.rotate({ x:0, y:1, z:0, angle: 180 }) transform: rotateY(180deg)
.rotate({ centerX: ‘100%’, centerY: ‘100%’ }) transform-origin: 100% 100%
但 ArkTS 的 rotate() 在语法上更加声明式——你把旋转参数作为一个对象传入,而不是拼接字符串。这使得代码更易读、更不容易出错。

三、ArkTS 动画体系详解
在深入代码精析之前,我们需要先理解 ArkTS 中两种动画模式的核心概念。

3.1 隐式动画(.animation())
工作原理: 在组件上声明 .animation() 后,每当组件的任何可动画属性发生变化,框架自动以动画形式过渡到新值。

// 隐式动画示例
Column()
.rotate({ angle: this.myAngle })
.animation({ duration: 500, curve: Curve.FastOutSlowIn })
// ↑ 当 this.myAngle 变化时,旋转自动以动画过渡
特点:

声明式:在组件上声明「任何时候的属性变化都要动画」
自动触发:状态变量变化 → 自动动画
适用范围广:任何可动画属性(rotate、scale、opacity、width、height 等)都受控
适用场景: 组件的一个或多个属性需要持续响应状态变化。

3.2 显式动画(animateTo())
工作原理: animateTo() 是一个全局函数,它包裹一段状态变更代码,将这个代码块中的所有状态变化以动画形式应用到 UI 上。

// 显式动画示例
animateTo({
duration: 600,
curve: Curve.FastOutSlowIn,
onFinish: () => { /* 动画完成回调 */ }
}, () => {
// 在这个闭包中修改状态变量
this.myAngle = 360;
})
特点:

命令式:明确指定「现在要执行一个动画」
批量更新:一个 animateTo 可以同时驱动多个状态变化
回调支持:支持 onFinish 回调,方便做动画串联
适用场景: 点击事件触发的「一次性」动画、需要串联多个动画的场景。

3.3 对比总结
维度 隐式动画 .animation() 显式动画 animateTo()
触发方式 状态变量自动变化 手动调用
代码风格 声明式(声明在 UI 上) 命令式(包裹在闭包中)
批量更新 自动,所有属性一起 包裹在闭包中的一起
回调支持 不支持 onFinish 支持 onFinish
精细控制 每个属性独立 一组属性整体
适用场景 持续动画、频繁变化 一次性触发、点击交互
最佳实践: 在实际项目中,通常两种模式结合使用:

使用 .animation() 设置组件的「默认动画行为」
使用 animateTo() 处理「点击触发」的交互动画
3.4 动画曲线(Curve)
动画曲线(也叫插值器 / easing function / timing function)控制动画速度随时间的变化模式。选择合适的曲线,能让动画看起来更自然、更有质感。

ArkTS 提供了丰富的内置曲线,覆盖了绝大多数场景:

曲线 速度特征 运动直觉 适用场景
Curve.Linear 全程匀速 机械、均匀 持续旋转、进度条、加载圈
Curve.Ease 慢→快→慢(默认) 自然、柔和 通用场景、默认选择
Curve.EaseIn 慢→快 加速启动 入场动画、飞出屏幕
Curve.EaseOut 快→慢 减速停止 出场动画、落位效果
Curve.FastOutSlowIn 快→慢→更慢 迅速响应、优雅结束 推荐用于旋转交互动画
Curve.LinearOutSlowIn 匀速→慢 物理减速 减速停止、滚动结束
Curve.ExtremeDeceleration 极速→极慢 强烈减速 强调的结束效果
Curve.Sharp 快→快→慢 干脆利落 列表项入场、强调动画
Curve.Rhythm 节奏感 有韵律地变化 音乐相关、节拍动画
Curve.Smooth 平滑过渡 丝滑平顺 翻页、轮播过渡
如何选择旋转动画的曲线?

持续旋转(如加载指示器):用 Curve.Linear,因为用户期待均匀的速度。如果用 EaseInOut,旋转会忽快忽慢,看起来节奏不连贯
点击交互(如翻牌、开关):用 Curve.FastOutSlowIn,让卡片快速响应手指、缓慢停稳,用户感知到的延迟最低
物理模拟(如钟摆、弹簧):用 Curve.FastOutSlowIn,模拟钟摆在最低点速度最快、最高点速度最慢的真实物理运动
装饰性动画(如 Logo 旋转展示):用 Curve.EaseInOut,让旋转有呼吸感和节奏感
曲线调试技巧: 设计动画时,建议先使用 FastOutSlowIn 作为默认曲线——它在绝大多数交互场景中表现最佳。如果感觉太「软」,换成 Linear;如果感觉太「硬」,换成 Ease。百分之八十的动画场景,用这三个曲线就足够覆盖了。

3.5 动画参数完整说明
animateTo() 和 .animation() 都接受一个 AnimateParam 配置对象:

interface AnimateParam {
/** 动画时长(毫秒),默认值因场景而异 */
duration: number;

/** 动画曲线(速度模式),默认 Curve.Ease */
curve?: Curve;

/** 延迟开始时间(毫秒),默认 0 */
delay?: number;

/** 动画结束回调 */
onFinish?: () => void;

/** 动画预期帧数,默认无限制 */
expectedFrameCount?: number;
}
在实际项目中使用最频繁的配置组合:

// 最简形式(仅指定时长)
animateTo({ duration: 300 }, () => { this.angle = 45; })

// 标准形式(时长 + 曲线 + 延迟 + 回调)
animateTo({
duration: 600,
curve: Curve.FastOutSlowIn,
delay: 100, // 延迟 100ms 再开始
onFinish: () => { /* 动画完成后的操作 */ }
}, () => {
this.angle = 180;
this.scale = 1.2;
})

// 隐式动画也是相似的配置结构
.animation({
duration: 500,
curve: Curve.FastOutSlowIn,
delay: 0
})
delay 的典型使用场景——序列动画: 当需要做「串行动画」时——比如卡片 A 先翻转 180°,然后卡片 B 再翻转 180°——可以借助 delay 实现,不需要多层嵌套定时器:

// 卡片 A:立即开始,延迟 0ms
animateTo({ duration: 400 }, () => { this.cardAFlipped = true; })

// 卡片 B:延时 500ms 开始(等 A 完成)
animateTo({ duration: 400, delay: 500 }, () => {
this.cardBFlipped = true;
})
四、RotateAnimation.ets 完整代码逐段精析
4.1 组件结构与状态定义
@Entry
@Component
struct RotateAnimationDemo {
// ① 基础旋转角度(0° ~ 360° 来回切换)
@State baseAngle: number = 0;

// ② 3D 翻转状态:是否翻转到背面
@State isFlipped: boolean = false;

// ③ 持续旋转动画控制
@State spinningAngle: number = 0;
@State isSpinning: boolean = false;
private spinningTimerId: number = -1;

// ④ 旋转中心偏移模式
@State pivotAngle: number = 0;
@State useCustomPivot: boolean = false;

// ⑤ 摆动动画
@State swingAngle: number = 0;
@State swingDirection: number = 1;
}
状态设计思路:

整个演示包含 6 个独立的示例场景,每个场景需要 1-2 个状态变量。将这些状态集中定义在 struct 顶部,是 ArkTS 的编码规范——便于阅读和集中管理。

关键点:所有的 @State 变量都用于驱动 UI 的重新渲染。当 animateTo() 闭包中修改这些变量时,或者 .animation() 监听到变量的变化时,框架会自动重新执行 build() 中受影响的部分,产生动画效果。

4.2 示例一:基础旋转动画
这是最核心、最基础的旋转用法——点击按钮让组件从 0° 旋转到 360°。

// 旋转组件
Column() {
Text(‘🔄’).fontSize(48)
Text(‘旋转’).fontSize(16).fontColor(Color.White)
.fontWeight(FontWeight.Bold).margin({ top: 4 })
}
.justifyContent(FlexAlign.Center)
.width(160).height(160)
.backgroundColor(‘#667eea’)
.borderRadius(20)
// ★ 核心:旋转角度由状态变量驱动
.rotate({ angle: this.baseAngle })
// ★ 隐式动画:baseAngle 变化时自动动画
.animation({ duration: 500, curve: Curve.FastOutSlowIn })
点击事件处理:

Button(‘顺时针 360°’)
.onClick(() => {
// ★ 显式动画
animateTo({
duration: 600,
curve: Curve.FastOutSlowIn,
onFinish: () => { console.info(‘基础旋转动画完成’); }
}, () => {
// 0° ↔ 360° 来回切换
this.baseAngle = this.baseAngle === 0 ? 360 : 0;
})
})
这段代码展示了四种交互状态:

初始态:baseAngle = 0,组件正常显示
点击:animateTo 将 baseAngle 变为 360
动画中:.animation() 以 500ms 驱动旋转过渡
再点击:baseAngle 回到 0,反向旋转归位
为什么需要「隐式 + 显式」双重动画设置?

初学者可能会困惑:为什么在 rotate() 上已经设置了 .animation(),又在 onClick 里用 animateTo()?

这其实是两层保障:

.animation() 是「安全带」——即使用户通过其他方式修改了 baseAngle(比如定时器、网络回调),也会以动画过渡
animateTo() 是「精确控制」——它提供了 onFinish 回调和精细的曲线控制
最佳实践: 在组件上始终保留 .animation(),这确保了无论状态如何变化,UI 永远都是平滑过渡的。animateTo() 用于需要交互反馈的场景。

4.3 示例二:3D 卡片翻转
这个示例是 rotate 最有「炫酷感」的应用——模拟一张卡片的正面和背面翻转效果。

技术难点: 如何让「正面」和「背面」在翻转过程中无缝切换?

解决方案: 使用 Stack 叠加两张卡片,通过角度 + 透明度的配合:

Stack() {
// ★ 正面卡片
Column() { /* 正面内容 */ }
.rotate({
x: 0, y: 1, z: 0, // 绕 Y 轴旋转
angle: this.isFlipped ? 180 : 0
})
.opacity(this.isFlipped ? 0 : 1) // 翻到背面时隐藏
.animation({ duration: 600, curve: Curve.FastOutSlowIn })

// ★ 背面卡片
Column() { /* 背面内容 */ }
.rotate({
x: 0, y: 1, z: 0,
angle: this.isFlipped ? 0 : -180 // 初始朝后
})
.opacity(this.isFlipped ? 1 : 0) // 翻到背面时显示
.animation({ duration: 600, curve: Curve.FastOutSlowIn })
}
.width(200).height(240)
翻转逻辑拆解:

状态 正面角度 正面透明度 背面角度 背面透明度 用户看到
isFlipped = false(正面朝上) 0° 1(可见) -180° 0(隐藏) 🎴 正面
isFlipped = true(背面朝上) 180° 0(隐藏) 0° 1(可见) 🌟 背面
透明度技巧详解:

为什么需要同时控制角度和透明度?因为纯角度控制有一个问题——在翻转过程中,卡片在「侧面」时(角度接近 90°)其实是不可见的(厚度为 0)。如果只有角度没有透明度控制,你会看到卡片逐渐缩小为一条线再展开。加上透明度后在 90° 附近淡出/淡入,视觉效果更加平滑。

触发翻转:

Button(this.isFlipped ? ‘翻回正面’ : ‘翻转到背面’)
.onClick(() => {
animateTo({ duration: 600, curve: Curve.FastOutSlowIn }, () => {
this.isFlipped = !this.isFlipped; // ★ 状态取反
})
})
4.4 示例三:持续旋转动画
持续旋转在加载场景中非常常见——比如一个无限旋转的沙漏或齿轮。

// UI 部分
Text(‘⏳’).fontSize(72)
.rotate({ angle: this.spinningAngle })
.animation({ duration: 300, curve: Curve.Linear })

// 控制逻辑
startSpinning() {
this.isSpinning = true;
this.spinningTimerId = setInterval(() => {
// 每次增加 12°,30ms 一次
this.spinningAngle = (this.spinningAngle + 12) % 360;
}, 30);
}
持续旋转的设计要点:

使用 Curve.Linear:持续旋转需要均匀的速度,不能用 FastOutSlowIn——后者会导致旋转忽快忽慢
定时器间隔 30ms:接近 30fps,保证流畅的同时不会过度消耗性能
每次增加 12°:30ms × 12° = 每 30ms 转 12°,相当于每秒转 400°,约 1.1 圈/秒
取模 360:(angle + 12) % 360 防止角度无限增大,保持数值在合理范围
停止逻辑:

stopSpinning() {
this.isSpinning = false;
if (this.spinningTimerId !== -1) {
clearInterval(this.spinningTimerId);
this.spinningTimerId = -1;
}
}
为什么用 setInterval 而不是 setTimeout?

setInterval 更适合持续动画,因为它会以固定间隔持续触发。setTimeout 需要每次手动重设定时器,容易产生时间漂移。

4.5 示例四:旋转中心偏移
这个示例通过「并排对比」的方式,直观展示了旋转中心对旋转效果的影响。

Row() {
// 左侧:默认中心旋转
Column() {
Text(‘默认中心’).fontSize(12)
// … 旋转组件
.rotate({ angle: this.pivotAngle }) // 中心默认 (50%,50%)
}

// 右侧:右下角轴心旋转
Column() {
Text(‘右下角轴心’).fontSize(12)
// … 旋转组件
.rotate({
angle: this.pivotAngle,
centerX: ‘100%’, // ★ 右边缘
centerY: ‘100%’ // ★ 底边缘
})
}
}
两种旋转中心的效果对比:

中心位置 视觉表现 类比
centerX: ‘50%’, centerY: ‘50%’(默认) 组件绕自身中心旋转 陀螺
centerX: ‘100%’, centerY: ‘100%’ 组件绕右下角旋转 开门、翻书页
centerX: ‘0%’, centerY: ‘0%’ 组件绕左上角旋转 钟摆、旗帜
centerX: ‘50%’, centerY: ‘0%’ 组件绕顶部中心旋转 钟摆
实际应用场景:

默认中心旋转:加载动画、齿轮转动、仪表盘指针
右下角轴心:翻书页、折叠菜单、卡片展开
顶部中心:钟摆、秋千、吊灯晃动
左上角:旗帜飘动、海报翻页
4.6 示例五:钟摆摆动动画
钟摆是「来回交替」动画的经典案例——视觉元素在一对正负角度之间来回切换。

// 钟摆视觉结构
Stack() {
// 摆杆(细线,从顶部垂下来)
Divider()
.width(4).height(80).color(‘#cccccc’)
.vertical(true).position({ top: 0 })

// 摆锤(底部圆球)
Column() { Text(‘⚽’).fontSize(40) }
.width(56).height(56)
.backgroundColor(‘#f6d365’).borderRadius(28)
.position({ top: 68 }) // 从顶部向下 68vp
}
.width(60).height(130)
// ★ 绕顶部中心摆动
.rotate({
angle: this.swingAngle,
centerX: ‘50%’,
centerY: ‘0%’ // ★ 轴心设在顶部
})
.animation({ duration: 800, curve: Curve.FastOutSlowIn })
钟摆的核心设计:

轴心在顶部:centerY: ‘0%’ 让整个钟摆绕悬挂点摆动,而不是绕中心
摆锤在底部:通过 position({ top: 68 }) 将圆球定位在摆杆末端
角度正负交替:30° ↔ -30° 来回切换
FastOutSlowIn 曲线:模拟物理钟摆在最低点速度最快、最高点速度最慢的运动规律
动画逻辑:

startSwing() {
let swingTimer = setInterval(() => {
if (this.swingDirection === 0) {
clearInterval(swingTimer);
return;
}
// 在 -30° 和 30° 之间来回切换
this.swingAngle = this.swingAngle >= 30 ? -30 : 30;
}, 800);
this.swingAngle = 30;
this.swingDirection = 1;
}
间隔 800ms 切换一次方向,配合 800ms 的动画时长,刚好完成一次「从一端到另一端」的完整摆动。

4.7 示例六:组合动画
在实际项目中,动画很少只变化一个属性——通常是旋转、缩放、透明度、位移同时进行。

Column() {
Text(‘🎯’).fontSize(64)
}
// ★ 三个变换属性同时变化
.rotate({ angle: this.spinningAngle })
.scale({ x: this.isFlipped ? 1.5 : 1.0, y: this.isFlipped ? 1.5 : 1.0 })
.opacity(this.isFlipped ? 0.6 : 1.0)
// ★ 三属性共用同一个动画配置
.animation({ duration: 800, curve: Curve.FastOutSlowIn })
触发逻辑:

Button(‘🎬 播放组合动画’)
.onClick(() => {
animateTo({ duration: 800, curve: Curve.FastOutSlowIn }, () => {
// 同时修改两个状态变量,触发 rotate + scale + opacity 变化
this.spinningAngle = this.spinningAngle + 180;
this.isFlipped = !this.isFlipped;
})
})
组合动画的设计价值:

单一属性动画能提供反馈,而多属性组合动画能传达「情感」。例如:

旋转 + 放大 → 「激活」「爆发」
旋转 + 缩小 + 淡出 → 「消失」「消散」
旋转 + 缩放循环 → 「心跳」「脉冲」
这也是为什么现代 UI 框架都支持多属性同时动画——丰富的情感表达往往来自多个属性的协同变化。

4.8 Builder 代码组织
@Builder
buildTitleBar() { /* 标题栏 */ }

@Builder
buildSectionHeader(title: string, desc: string) { /* 章节标题 */ }

@Builder
buildFooterSection() { /* 底部说明 */ }
使用 @Builder 将页面拆分为可复用的构建块,是 ArkTS 的最佳实践。这样做的好处:

代码复用:buildSectionHeader 在 6 个示例中被反复调用
可读性:build() 方法高度抽象,阅读者可以一眼看出页面结构
关注点分离:每个 Builder 只负责自己那一块 UI
五、动画性能优化
5.1 避免不必要的布局 re-layout
rotate() 是变换属性,不会触发布局重新计算。这是它比修改 width、height、margin 做动画性能更好的原因。

// ✅ 好:使用 rotate 做旋转动画
Text(‘🔄’)
.rotate({ angle: this.angle })
.animation({ duration: 500 })

// ❌ 差:通过修改布局属性模拟旋转(不存在这种做法,仅示意)
// 不要这样做——会引起大量布局计算
5.2 动画曲线选择
选择正确的动画曲线可以显著提升感知性能:

加载旋转:Curve.Linear —— 匀速,视觉稳定
交互反馈:Curve.FastOutSlowIn —— 快速响应 + 优雅结束,用户感知最「快」
物理模拟:Curve.Spring 或 FastOutSlowIn —— 模拟真实物理运动
5.3 避免高频定时器
在我们的示例中,持续旋转使用了 30ms 的 setInterval。在真实项目中:

如果只需要 30fps 的动画,间隔可以设为 33ms
如果需要 60fps,间隔设为 16ms
使用 requestAnimationFrame(如果可用)性能更优
5.4 状态变量最小化
每个 @State 变量都会带来 UI 重新渲染的性能开销。设计时应遵循「最小状态原则」:

// ✅ 好:一个状态驱动多个属性
@State isFlipped: boolean = false;
// .rotate({ angle: this.isFlipped ? 180 : 0 })
// .scale({ x: this.isFlipped ? 1.5 : 1.0 })

// ❌ 冗余:为每个属性创建独立状态
@State rotateAngle: number = 0;
@State scaleValue: number = 1.0;
// 当点击时需要同时修改两个变量
5.5 动画与手势的配合
在实际项目中,旋转动画经常与手势配合——比如用户拖拽卡片时跟随手指旋转,松手后自动对齐。这需要结合 PanGesture 或 GestureGroup 使用:

.gesture(
PanGesture()
.onActionStart(() => { /* 记录起始角度 / })
.onActionUpdate((event) => { /
跟随手指旋转 */ })
.onActionEnd(() => {
// 松手后动画对齐
animateTo({ duration: 300 }, () => {
this.angle = Math.round(this.angle / 45) * 45;
})
})
)
六、常见陷阱与排查指南
6.1 陷阱一:忘记设置动画时长
// ❌ 没有 .animation() 也没有 animateTo()
.rotate({ angle: this.myAngle })
// 当 myAngle 变化时,瞬间跳转,没有过渡

// ✅ 添加隐式动画
.rotate({ angle: this.myAngle })
.animation({ duration: 500 })
6.2 陷阱二:误认为 rotate 改变布局
Row() {
Column().width(100).height(100).rotate({ angle: 45 })
// ↑ 旋转后视觉上可能覆盖到右侧组件
Column().width(100).height(100)
// ↑ 但右侧组件的实际位置没有变化
}
如果需要「视觉上旋转 + 布局上占位不变」,rotate() 是正确选择。如果希望旋转后「推开」相邻元素,需要用动画修改 margin 或 position 属性。

6.3 陷阱三:多状态同时变化时序问题
// ❌ 多个状态分别变化,可能导致动画不同步
animateTo({ duration: 300 }, () => { this.angle = 180; })
animateTo({ duration: 500 }, () => { this.scale = 1.5; })
// 角度和缩放不同步

// ✅ 同一 animateTo 闭包中修改
animateTo({ duration: 400 }, () => {
this.angle = 180;
this.scale = 1.5;
})
6.4 陷阱四:忘记 @Builder 的 void 约束
// ❌ 不能在 @Builder 外部链式调用
this.buildRotateComponent()
.rotate({ angle: 45 }) // 编译错误

// ✅ 在 @Builder 内部设置
@Builder
buildRotateComponent() {
Column()
.rotate({ angle: 45 }) // 在内部设置
}
6.5 陷阱五:旋转中心使用像素值时的适配问题
// ❌ 硬编码像素值,在不同屏幕尺寸上效果不同
.rotate({ angle: 45, centerX: 100, centerY: 100 })

// ✅ 使用百分比,自动适配
.rotate({ angle: 45, centerX: ‘50%’, centerY: ‘50%’ })
6.6 调试技巧
可视化组件边界:临时添加 .borderWidth(1).borderColor(Color.Red) 可以清晰看到旋转组件的实际占位和变换后的视觉位置

打印角度值:在 animateTo 的 onFinish 回调和定时器中打印当前角度:

console.info(当前角度: ${this.spinningAngle});
使用 DevEco Studio 的 Inspector 工具:可以实时查看组件的 transform 矩阵状态

七、跨平台对比
7.1 iOS Core Animation
// iOS 核心动画 - CABasicAnimation
let animation = CABasicAnimation(keyPath: “transform.rotation.z”)
animation.toValue = Double.pi * 2 // 360°
animation.duration = 0.5
animation.timingFunction = CAMediaTimingFunction(
name: .easeInEaseOut
)
view.layer.add(animation, forKey: “rotation”)
iOS 特点:

基于 CALayer 层级的动画
需要创建动画对象、设置 keyPath、添加到 layer
代码量较多,写法较为冗长
默认动画完毕后会「跳回」原始状态(需要设置 fillMode)
7.2 Android Property Animation
// Android 属性动画 - ObjectAnimator
val animator = ObjectAnimator.ofFloat(
view, “rotation”, 0f, 360f
)
animator.duration = 500
animator.interpolator = AccelerateDecelerateInterpolator()
animator.start()
Android 特点:

基于属性值变化的动画
需要创建 ObjectAnimator、设置属性和值、启动
支持多种 Interpolator 插值器
View.animate().rotationBy(360f) 有简化写法,但功能有限
7.3 CSS Transform + Transition
/* CSS 旋转动画 */
.element {
transform: rotate(0deg);
transition: transform 0.5s ease-in-out;
}
.element.active {
transform: rotate(360deg);
}
CSS 特点:

声明式写法,与 ArkTS 最相似
transition 对应 ArkTS 的隐式动画
@keyframes 动画对应更复杂的逐帧控制
没有声明式的「显式动画」API,需要用 JS 控制 class
7.4 鸿蒙 ArkTS 的优势
维度 iOS CAAnimation Android Animator CSS Transition 鸿蒙 ArkTS
代码行数 6-8 行 4-6 行 3 行 CSS + JS 2 行
隐式/显式 仅显式 仅显式 仅隐式 两种都支持
回调 delegate AnimatorListener transitionend onFinish 回调
三维旋转 支持 CATransform3D 支持 rotationX/Y 支持 rotateX/Y/Z 原生支持
旋转中心 anchorPoint pivotX/Y transform-origin centerX/Y
声明式风格 ❌ 命令式 ❌ 命令式 ⚠️ 部分 ✅ 完全声明式
八、实际项目中的应用场景
8.1 启动加载动画
应用启动时,显示一个旋转的品牌 Logo:

@Entry
@Component
struct SplashPage {
@State angle: number = 0;

aboutToAppear() {
// 页面显示后启动持续旋转
setInterval(() => {
this.angle = (this.angle + 6) % 360;
}, 16); // ~60fps
}

build() {
Stack() {
Image($r(‘app.media.logo’))
.width(120).height(120)
.rotate({ angle: this.angle })
.animation({ duration: 16, curve: Curve.Linear })
}
.width(‘100%’).height(‘100%’)
.backgroundColor(‘#667eea’)
}
}
8.2 3D 商品展示
电商 App 中的商品 3D 展示——用户左右滑动查看商品不同角度:

@State rotateY: number = 0;

// 手势控制旋转
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionUpdate((event: GestureEvent) => {
this.rotateY = this.rotateY + event.offsetX * 0.5;
})
.onActionEnd(() => {
// 松手后惯性滑动
animateTo({ duration: 300, curve: Curve.FastOutSlowIn }, () => {
this.rotateY = Math.round(this.rotateY / 90) * 90;
})
})
)

// 商品卡片沿 Y 轴旋转
Stack() {
Image($r(‘app.media.product_front’))
.rotate({ x: 0, y: 1, z: 0, angle: this.rotateY % 360 })
// 根据角度控制显示正面还是侧面
}
8.3 音乐播放器旋转唱片
@State diskAngle: number = 0;
@State isPlaying: boolean = false;
private timerId: number = -1;

build() {
Column() {
// 黑胶唱片
Image($r(‘app.media.vinyl_disk’))
.width(280).height(280)
.borderRadius(140)
.rotate({ angle: this.diskAngle })
.animation({ duration: 50, curve: Curve.Linear })

Button(this.isPlaying ? '⏸ 暂停' : '▶ 播放')
  .onClick(() => {
    this.isPlaying = !this.isPlaying;
    if (this.isPlaying) {
      // 开始旋转
      this.timerId = setInterval(() => {
        this.diskAngle = (this.diskAngle + 1) % 360;
      }, 50);
    } else {
      // 暂停旋转,停在当前位置
      clearInterval(this.timerId);
    }
  })

}
}
8.4 翻牌记忆游戏
@State isRevealed: boolean = false;

build() {
Stack() {
// 卡背(始终显示,但翻转后隐藏)
Column() { Text(‘❓’).fontSize(48) }
.rotate({ x: 0, y: 1, z: 0, angle: this.isRevealed ? 180 : 0 })
.opacity(this.isRevealed ? 0 : 1)

// 卡面(翻转后显示)
Column() { Text('🎭').fontSize(48) }
  .rotate({ x: 0, y: 1, z: 0, angle: this.isRevealed ? 0 : -180 })
  .opacity(this.isRevealed ? 1 : 0)

}
.animation({ duration: 400, curve: Curve.FastOutSlowIn })
.onClick(() => {
animateTo({ duration: 400 }, () => {
this.isRevealed = !this.isRevealed;
})
})
}
8.5 可旋转的标签云
@State cloudAngle: number = 0;

// 自动缓慢旋转
aboutToAppear() {
setInterval(() => {
this.cloudAngle = (this.cloudAngle + 0.5) % 360;
}, 50);
}

build() {
Stack() {
// 多个标签围绕中心排列
ForEach(this.tags, (tag, index) => {
Text(tag.name)
.position({
x: 150 + 120 * Math.cos((index * 60 + this.cloudAngle) * Math.PI / 180),
y: 150 + 120 * Math.sin((index * 60 + this.cloudAngle) * Math.PI / 180)
})
})
}
.width(300).height(300)
.animation({ duration: 50, curve: Curve.Linear })
}

8.6 雷达扫描 / 加载占位动画

在数据加载完成之前,显示一个「雷达扫描」式的占位动画,可以有效地降低用户的等待焦虑感:

@Entry
@Component
struct RadarLoadingPage {
  @State scanAngle: number = 0;
  @State pulseScale: number = 1.0;
  private timerId: number = -1;

  aboutToAppear() {
    // 启动后自动开始雷达扫描 + 脉冲动画
    this.timerId = setInterval(() => {
      this.scanAngle = (this.scanAngle + 3) % 360;
      // 脉冲呼吸效果:角度每转一圈,缩放一次
      if (this.scanAngle % 90 < 3) {
        animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
          this.pulseScale = this.pulseScale > 1.0 ? 1.0 : 1.1;
        });
      }
    }, 16);
  }

  aboutToDisappear() {
    clearInterval(this.timerId);
  }

  build() {
    Stack() {
      // 雷达外圈(旋转扫描线)
      Column() {
        // 扫描扇形(用圆角矩形 + rotate + clip 实现)
        Column()
          .width(120).height(120)
          .backgroundColor('rgba(102,126,234,0.15)')
          .borderRadius(120)
          .clip(true)
          .rotate({
            angle: this.scanAngle,
            centerX: '50%',
            centerY: '50%'
          })

        // 中央圆点
        Column()
          .width(16).height(16)
          .backgroundColor('#667eea')
          .borderRadius(8)
          .scale({ x: this.pulseScale, y: this.pulseScale })
      }
      .animation({ duration: 16, curve: Curve.Linear })

      // 加载文字
      Text('正在加载数据...')
        .fontSize(14).fontColor('#999999')
        .position({ bottom: 40 })
    }
    .width('100%').height('100%')
    .backgroundColor('#f5f5f5')
  }
}
这个例子展示了 rotate 与 scale 两种变换的协同工作:扫描线持续旋转,中心圆点同步呼吸脉动。两个动画各自独立运行,但组合在一起时产生了一个「正在工作」的视觉暗示,让用户感知到系统是活跃的。

8.7 菜单展开 / 关闭动画
圆形扩散菜单(FAB Menu)是 Material Design 中的经典交互模式——点击主按钮,子菜单以扇形展开:

@State isMenuOpen: boolean = false;
@State childAngles: number[] = [0, 0, 0, 0];

private readonly menuItems = [
  { icon: '📷', color: '#ff6b6b', label: '拍照' },
  { icon: '🎵', color: '#4facfe', label: '音乐' },
  { icon: '📝', color: '#43e97b', label: '笔记' },
  { icon: '⚙️', color: '#a18cd1', label: '设置' },
];

toggleMenu() {
  animateTo({ duration: 400, curve: Curve.FastOutSlowIn }, () => {
    this.isMenuOpen = !this.isMenuOpen;
    // 每个子菜单按钮旋转到对应角度,形成扇形分布
    const angles = this.isMenuOpen
      ? [0, 90, 180, 270]     // 展开:分布在四个方向
      : [0, 0, 0, 0];          // 收起:回到原点
    this.childAngles = angles;
  });
}

build() {
  Stack() {
    // 子菜单按钮(四个方向)
    ForEach(this.menuItems, (item, index) => {
      Column() {
        Text(item.icon).fontSize(24)
      }
      .width(48).height(48)
      .backgroundColor(item.color)
      .borderRadius(24)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      // 以下是以主按钮为中心,向四个方向旋转展开
      .rotate({
        angle: this.childAngles[index],
        centerX: '50%',
        centerY: '-80%'      // 旋转中心在按钮上方 80vp 处(主按钮位置)
      })
      .opacity(this.isMenuOpen ? 1 : 0)
      .animation({ duration: 400, curve: Curve.FastOutSlowIn })
    })

    // 主按钮
    Button(this.isMenuOpen ? '✕' : '+')
      .fontSize(28).fontColor(Color.White)
      .width(56).height(56)
      .backgroundColor('#667eea')
      .borderRadius(28)
      .rotate({ angle: this.isMenuOpen ? 45 : 0 })  // 主按钮自身旋转 45°
      .animation({ duration: 300, curve: Curve.FastOutSlowIn })
      .onClick(() => this.toggleMenu())
  }
  .width('100%').height('100%')
  .position({ bottom: 40, right: 24 })
}
这个例子结合了 rotate + opacity + 旋转中心偏移 三个技术点:

子菜单通过 centerY: '-80%' 将旋转中心偏移到主按钮的位置
展开时从角度 0° 旋转到各自的展开角度
主按钮自身旋转 45°,形成「加号变叉号」的视觉反馈
透明度从 01 平滑出现
8.8@State + 生命周期联动的最佳实践
在 RotateAnimation.ets 和上述扩展场景中,有一个共同的模式值得总结——如何优雅地管理动画生命周期:

@Entry
@Component
struct AnimationPage {
  @State angle: number = 0;
  private timerId: number = -1;

  // ★ 页面即将显示时启动动画
  aboutToAppear() {
    this.startAnimation();
  }

  // ★ 页面即将消失时停止动画(防止内存泄漏)
  aboutToDisappear() {
    this.stopAnimation();
  }

  startAnimation() {
    this.timerId = setInterval(() => {
      this.angle = (this.angle + 1) % 360;
    }, 16);
  }

  stopAnimation() {
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }
}
关键原则:

aboutToAppear 中启动 → 确保页面已准备就绪
aboutToDisappear 中停止 → 防止页面销毁后定时器仍在运行(内存泄漏)
始终保存 timerId → 才能精确停止
stop 时重置 timerId 为 -1 → 防止重复 clear
如果不遵循这个原则,可能出现的 bug:

用户离开页面后,定时器仍在运行,持续修改 @State 变量
即使页面不可见,UI 渲染仍在后台进行,浪费 GPU 资源
如果再次进入页面,会创建第二个定时器,两个定时器同时运行
这也是为什么 RotateAnimation.ets 中我们的 startSpinning()stopSpinning() 方法被设计为可多次安全调用的——调用两次 stopSpinning() 不会崩溃,调用两次 startSpinning() 也不会重复创建定时器。

8.9 rotate 动画与其他动画属性的搭配速查表
在实际项目中,rotate 很少单独使用。以下是 rotate 与其他变换属性搭配的常见模式:

搭配属性	组合效果	实现方式	适用场景
rotate + scale	旋转并缩放	.rotate({ angle }).scale({ x:1.2 })	加载动画、按钮点击反馈
rotate + opacity	旋转并淡入/淡出	.rotate({ angle }).opacity(0.5)	菜单展开、弹窗出现
rotate + translate	旋转并平移	.rotate({ angle }).translate({ x:10 })	卡片滑入旋转、3D 透视
rotate + scale + opacity	三属性同时	三者链式调用	组合动画示例(示例六)
rotate + borderRadius	旋转并变圆角	.rotate({ angle }).borderRadius(50)	形状变换、趣味过渡
rotate + backgroundColor	旋转并变色	.rotate({ angle }).backgroundColor(...)	状态切换、翻牌变色
高频组合模板:

// 模板一:卡片入场(旋转 + 缩放 + 淡入)
Column()
.rotate({ angle: this.entryAngle })
.scale({ x: this.entryScale, y: this.entryScale })
.opacity(this.entryOpacity)
.animation({ duration: 400, curve: Curve.FastOutSlowIn })

// 模板二:加载脉冲(旋转 + 缩放呼吸)
Column()
.rotate({ angle: this.spinAngle })
.scale({ x: this.pulseScale, y: this.pulseScale })
.animation({ duration: 300, curve: Curve.Linear })

// 模板三:翻牌组合(Y 轴旋转 + 透明度切换)
.rotate({ x: 0, y: 1, z: 0, angle: this.flipAngle })
.opacity(this.isVisible ? 1 : 0)
.animation({ duration: 500, curve: Curve.FastOutSlowIn })
九、总结
9.1 核心要点回顾
rotate() 是变换属性,不会影响组件的布局占位,性能优于修改布局属性
三个旋转轴:Z 轴(平面旋转)、Y 轴(左右翻转)、X 轴(上下翻转)
旋转中心通过 centerX/centerY 控制,默认组件中心,百分比值适配性更好
两种动画模式:.animation() 隐式动画适用于「持续变化」,animateTo() 显式动画适用于「交互触发」
动画曲线对旋转体验影响巨大——Linear 适合持续旋转,FastOutSlowIn 适合交互反馈
组合动画能产生「1 + 1 > 2」的体验效果,rotate + scale + opacity 是最常用的组合
9.2 设计哲学
ArkTS 的 rotate 动画 API 体现了以下设计理念:

声明式优于命令式:开发者只需要告诉框架「组件应该旋转到什么角度」,框架自己计算中间的每一帧过渡。这与 iOS 的 CABasicAnimation(需要创建动画对象、设置 keyPath、配置 timing function 等步骤)形成鲜明对比。声明式 API 让代码量减少了 60% 以上,同时降低了出错的概率。

链式调用的可组合性:.rotate().animation().scale().opacity() 这种链式写法看似简单,实则蕴含了「组合优于继承」的设计思想——每个变换属性都是独立的、可组合的单元,开发者可以根据需要自由拼装。如果要实现一个「旋转 + 缩放 + 淡入」的效果,不需要继承某个基类,只需要把三个属性链在一起即可。

统一的动画模型:在 ArkTS 中,不管是旋转(rotate)还是缩放(scale)还是透明度(opacity)还是位置(translate),都使用同一套隐式/显式动画机制。这种一致性极大地降低了学习成本——开发者学会了一种动画模式,就等于学会了所有属性的动画。

原生 3D 支持:x/y/z 三维旋转是 API 原生支持的,不需要导入任何第三方库或配置复杂的矩阵变换。{ x: 0, y: 1, z: 0, angle: 180 } 就是一次完整的 Y180° 翻转——简洁、直观、强大。

合理默认值原则:rotate 的默认旋转轴是 Z 轴(最常见的 2D 旋转),默认旋转中心是组件几何中心(最符合直觉的行为)。这些默认值的选取基于「最常见的场景」,让 80% 的开发者不需要额外配置就能获得期望的效果。只有在特殊需求(比如绕 Y 轴翻转、绕右下角旋转)时才需要显式指定参数。

与命令式框架的思维差异:

习惯了命令式编程(如 iOS UIKit 或 Android View 体系)的开发者,在第一次接触 ArkTS 的声明式动画时可能会感到不适应。核心差异在于:

命令式思维:「当用户点击时,创建一个动画对象,设置它的属性,然后添加到视图上」
声明式思维:「状态 X 决定了组件的旋转角度,框架负责在状态变化时自动过渡」
理解这种思维转换,是掌握 ArkTS 动画的关键。

9.3 与上一代鸿蒙(API 9)的对比
在 HarmonyOS 3.x(API 9)中,动画主要基于 animateTo + animation,与当前版本的核心机制一致。HarmonyOS NEXTAPI 12+)的主要改进在于:

更多的内置动画曲线
更好的三维旋转渲染性能
更完善的 @Builder 和状态管理生态
9.4 未来展望
随着鸿蒙生态的发展,rotate 动画相关的 API 可以期待以下方向的演进:

更丰富的 3D 变换:类似 CSSperspective、rotate3d() 等
物理引擎集成:弹簧、阻尼、碰撞等物理模型驱动的旋转动画
手势驱动的实时旋转:基于 PanGesture + animateTo 的丝滑交互
流畅度持续优化:原生级别的 120Hz 刷新率支持
9.5 写在最后
旋转动画是 UI 动画的「必修课」——它简单、直观,却能带来显著的交互提升。在 HarmonyOS NEXT 中,ArkTS 框架用极简的 API 封装了复杂的旋转计算,让开发者可以用两行代码实现一个完整的旋转动画效果:

.rotate({ angle: this.myAngle })
.animation({ duration: 500 })
这种「声明式」的简洁性,正是现代 UI 框架的精髓所在——开发者关注「做什么」,框架负责「怎么做」。

附录:完整源码结构
完整源码位于 entry/src/main/ets/pages/RotateAnimation.ets(690 行),结构如下:

RotateAnimation.ets
├── 文件头注释(L1-L15// 布局要点概览
├── @Entry @Component(L17-L49// 状态变量定义
├── build() 方法(L53-L511)
│   ├── buildTitleBar()               // 标题栏
│   ├── 示例一:基础旋转动画(L60-L133// animateTo + rotate
│   ├── 示例二:3D 卡片翻转(L135-L228// Y 轴旋转 + 透明度配合
│   ├── 示例三:持续旋转动画(L230-L280// setInterval + Linear 曲线
│   ├── 示例四:旋转中心偏移(L282-L369// centerX/centerY 对比
│   ├── 示例五:钟摆摆动(L371-L441// 角度交替 + 轴心控制
│   ├── 示例六:组合动画(L443-L495// rotate + scale + opacity
│   └── buildFooterSection()           // 底部技术说明
├── startSpinning() / stopSpinning()   // 持续旋转控制逻辑
├── startSwing()                       // 钟摆控制逻辑
├── @Builder 组件(L556-L689)
│   ├── buildTitleBar()                // 标题栏
│   ├── buildSectionHeader()           // 章节标题
│   └── buildFooterSection()           // 底部说明
└── 文件结束
本文配套代码仓库: Demo0625/entry/src/main/ets/pages/RotateAnimation.ets
运行方式: 使用 DevEco Studio 打开项目,连接 HarmonyOS NEXT 真机或模拟器运行
入口页面: Index.ets → 点击「🔄 Rotate 旋转动画布局」进入示例
Logo

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

更多推荐