欢迎光临
我们一直在努力

HarmonyOS6 PC端旋转动画rotate详解——角度变化与缓动曲线的花式玩法

文章目录

      • 旋转动画在哪里最常见
      • rotate属性的工作原理
      • 不同Curve在旋转中的视觉差异
      • 完整案例代码
      • 代码解析
      • 旋转角度的累加策略
      • PC端大屏适配
      • 写在最后

旋转动画封面图

旋转动画在所有属性动画里算是最"戏剧性"的一种。一个元素在屏幕上转起来,那种动态感是opacity和translate完全给不了的。今天就来把rotate这个属性彻底搞明白,顺便对比一下不同缓动曲线在旋转场景下的表现差异。

旋转动画在哪里最常见

坦白讲,日常App里旋转动画出镜率最高的地方是Loading图标。那个不停转圈圈的小图标,本质上就是一个无限循环的旋转动画。除此之外,下拉刷新的箭头旋转、设置页面的齿轮图标、卡片翻转效果、指南针UI——这些都是rotate的用武之地。

在PC端,旋转动画还有一个很特别的用途:窗口过渡效果。当用户在Dashboard和详情页之间切换时,一个微妙的旋转动画可以让过渡感觉更自然。HarmonyOS6 PC端的大屏幕让这种效果更有发挥空间。

rotate属性的工作原理

旋转动画概念图

ArkUI的.rotate()接受一个对象参数,最核心的就是angle属性,单位是角度。0度是原始方向,90度顺时针转90度,360度转一整圈回到原位。

这里有个关键的概念要搞清楚:角度是可以累加的。你把angle从0改到360,元素转一圈;从360改到720,元素再转一圈。也就是说,angle不是"目标朝向",而是"总共转了多少度"。这个区别在做连续旋转的时候特别重要。

如果angle只能到360就"到头"了,那你每次想继续转就得先重置回0,而这个重置本身也会触发动画(元素会突然跳回起始角度)。所以正确的做法是一直累加角度值。

不同Curve在旋转中的视觉差异

旋转动画是观察Curve差异的最佳场景,因为旋转的运动感特别强,不同曲线带来的"节奏感"差异一目了然。

Curve.EaseInOut在旋转里表现为"慢慢启动、中间加速、慢慢停下",像一个有经验的司机在开车。Curve.FastOutSlowIn则是"快速起步、缓缓到位",感觉更急促一些,适合需要快速反馈的交互场景。Curve.Linear就是匀速转动,适合Loading这种需要稳定节奏的场景。

完整案例代码

这个demo提供了90°、180°、360°旋转按钮和一个重置按钮,还有两种Curve曲线的对比切换。

@Entry
@Component
struct RotateAnimDemo {
@State angle: number = 0
@State currentCurve: Curve = Curve.EaseInOut
@State curveName: string = 'EaseInOut'
@State totalRotation: number = 0
@State boxColor: string = '#e17055'

build() {
Column({ space: 16 }) {
Text('HarmonyOS6 PC 旋转动画 rotate')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1a1a2e')
.margin({ top: 20 })

Text('角度变化与缓动曲线效果对比')
.fontSize(14)
.fontColor('#888888')

// 动画展示区域
Stack() {
// 角度参考刻度
Column() {
Text('0°')
.fontSize(11)
.fontColor('#b2bec3')
Blank()
Text('180°')
.fontSize(11)
.fontColor('#b2bec3')
}
.height('80%')
.justifyContent(FlexAlign.SpaceBetween)
.position({ x: 10, y: '10%' })

// 旋转元素
Column() {
Row() {
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#ffffff')
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#ffffff80')
.margin({ left: 8 })
}
.margin({ bottom: 8 })

Text('↑')
.fontSize(24)
.fontColor('#ffffff')
.fontWeight(FontWeight.Bold)

Text(`${this.totalRotation}°`)
.fontSize(12)
.fontColor('#ffffffcc')
.margin({ top: 4 })
}
.width(90)
.height(90)
.borderRadius(16)
.backgroundColor(this.boxColor)
.justifyContent(FlexAlign.Center)
.rotate({ angle: this.angle })
.shadow({ radius: 12, color: '#00000020', offsetX: 0, offsetY: 4 })
}
.width('85%')
.height(220)
.borderRadius(16)
.backgroundColor('#f1f2f6')

// 状态信息
Row({ space: 24 }) {
Column({ space: 4 }) {
Text('当前角度')
.fontSize(12)
.fontColor('#999999')
Text(`${this.totalRotation}°`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#e17055')
}

Column({ space: 4 }) {
Text('实际rotate')
.fontSize(12)
.fontColor('#999999')
Text(`${this.angle}°`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#0984e3')
}

Column({ space: 4 }) {
Text('当前曲线')
.fontSize(12)
.fontColor('#999999')
Text(this.curveName)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#6c5ce7')
}
}

// 旋转控制按钮
Text('旋转角度')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 10 })

Row({ space: 10 }) {
Button('+90°')
.fontSize(14)
.height(36)
.backgroundColor('#e17055')
.onClick(() => {
this.totalRotation += 90
this.boxColor = '#e17055'
animateTo({
duration: 600,
curve: this.currentCurve,
iterations: 1
}, () => {
this.angle += 90
})
})

Button('+180°')
.fontSize(14)
.height(36)
.backgroundColor('#0984e3')
.onClick(() => {
this.totalRotation += 180
this.boxColor = '#0984e3'
animateTo({
duration: 800,
curve: this.currentCurve,
iterations: 1
}, () => {
this.angle += 180
})
})

Button('+360°')
.fontSize(14)
.height(36)
.backgroundColor('#6c5ce7')
.onClick(() => {
this.totalRotation += 360
this.boxColor = '#6c5ce7'
animateTo({
duration: 1200,
curve: this.currentCurve,
iterations: 1
}, () => {
this.angle += 360
})
})
}

Row({ space: 10 }) {
Button('重置归零')
.fontSize(14)
.height(36)
.backgroundColor('#636e72')
.onClick(() => {
this.totalRotation = 0
this.boxColor = '#e17055'
animateTo({
duration: 600,
curve: Curve.EaseOut,
iterations: 1
}, () => {
this.angle = 0
})
})

Button('连续旋转3圈')
.fontSize(14)
.height(36)
.backgroundColor('#fd79a8')
.onClick(() => {
this.totalRotation += 1080
this.boxColor = '#fd79a8'
animateTo({
duration: 2000,
curve: Curve.FastOutSlowIn,
iterations: 1
}, () => {
this.angle += 1080
})
})
}

// Curve曲线切换
Text('切换缓动曲线')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 12 })

Row({ space: 10 }) {
Button('EaseInOut')
.fontSize(12)
.height(32)
.backgroundColor(this.curveName === 'EaseInOut' ? '#6c5ce7' : '#b2bec3')
.fontColor(this.curveName === 'EaseInOut' ? '#ffffff' : '#2d3436')
.onClick(() => {
this.currentCurve = Curve.EaseInOut
this.curveName = 'EaseInOut'
})

Button('FastOutSlowIn')
.fontSize(12)
.height(32)
.backgroundColor(this.curveName === 'FastOutSlowIn' ? '#6c5ce7' : '#b2bec3')
.fontColor(this.curveName === 'FastOutSlowIn' ? '#ffffff' : '#2d3436')
.onClick(() => {
this.currentCurve = Curve.FastOutSlowIn
this.curveName = 'FastOutSlowIn'
})

Button('Linear')
.fontSize(12)
.height(32)
.backgroundColor(this.curveName === 'Linear' ? '#6c5ce7' : '#b2bec3')
.fontColor(this.curveName === 'Linear' ? '#ffffff' : '#2d3436')
.onClick(() => {
this.currentCurve = Curve.Linear
this.curveName = 'Linear'
})
}

// 特殊效果
Text('特殊效果')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 12 })

Button('来回摇摆(EaseInOut × 3次循环)')
.fontSize(13)
.height(36)
.width('80%')
.backgroundColor('#00b894')
.onClick(() => {
this.boxColor = '#00b894'
animateTo({
duration: 400,
curve: Curve.EaseInOut,
iterations: 1
}, () => {
this.angle += 30
this.totalRotation += 30
})
setTimeout(() => {
animateTo({
duration: 800,
curve: Curve.EaseInOut,
iterations: 3
}, () => {
this.angle -= 60
this.totalRotation -= 60
})
}, 450)
setTimeout(() => {
animateTo({
duration: 400,
curve: Curve.EaseOut,
iterations: 1
}, () => {
this.angle += 30
this.totalRotation += 30
})
}, 2950)
})
}
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
}
}

运行效果如图: 在这里插入图片描述

代码解析

这段代码有几个设计上的考量。

方块上面画了一个向上的箭头"↑",这样旋转的时候你能清楚地看到当前角度方向。如果没有方向标识,一个正方形转90度和没转看起来是一样的,那旋转动画就失去意义了。

totalRotation和angle两个变量分别记录"逻辑总角度"和"实际传给rotate的角度"。重置的时候,angle通过animateTo平滑回到0,totalRotation直接清零。这样UI上显示的角度信息和实际旋转状态是同步的。

来回摇摆效果用了iterations: 3来实现循环。当iterations大于1时,animateTo会让动画来回播放指定次数。这个效果模拟的是"摇头"或"摆钟"的感觉,在提示用户"操作不正确"之类的场景里特别好用。

旋转角度的累加策略

搞定了基础旋转之后,有个工程上的问题值得聊聊:角度累加会不会溢出?

说实话,JavaScript的Number类型精度足够大,几百几千次累加完全不用担心溢出问题。但如果角度值变得特别大(比如累加到几百万度),浮点数精度可能影响动画的流畅度。一个保险的做法是每隔一段时间把angle取模360,但这个操作本身会引入"跳变"——所以更好的方式是在动画完成后再做取模。

在实际项目中,如果旋转角度是可控的(比如用户每次点击转90度),直接累加就行,不用考虑取模。只有无限循环旋转(Loading图标那种)才需要特别处理。

PC端大屏适配

PC端屏幕大,旋转元素可以适当做大一些。手机上90×90的旋转方块在PC上可能显得太小,放大到120×120甚至150×150都没问题。

另一个PC端的考量是旋转速度。大屏幕上的旋转在视觉上会比小屏幕看起来更慢(因为旋转半径更大,边缘的线速度更明显),所以在PC端可以适当缩短duration或者增加角度,让旋转效果更明显。

写在最后

旋转动画的核心就一个angle值,但加上不同的角度和不同的Curve,就能玩出很多花样。累加角度而不是设绝对值,是做连续旋转的关键技巧。

下一篇来聊translate的8方向位移,用3×3网格按钮控制元素在各个方向上移动,那个demo做出来特别有操控感。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS6 PC端旋转动画rotate详解——角度变化与缓动曲线的花式玩法
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址