如果你也对鸿蒙开发感兴趣,加入“Harmony自习室”吧!扫描下方名片,关注公众号,公众号更新更快,同时也有更多学习资料和技术讨论群。
1、概述
ArkUI提供了预置动画曲线函数(指定了动画属性从起始值到终止值的变化规律)如Linear、Ease、EaseIn等。
另外,ArkUI也提供了由弹簧振子物理模型产生的弹簧曲线。通过弹簧曲线,开发者可以设置超过设置的终止值,在终止值附近震荡,直至最终停下来的效果。弹簧曲线的动画效果比其他曲线具有更强的互动性、可玩性。
弹簧曲线的接口包括两类,一类是springCurve,另一类是springMotion和responsiveSpringMotion,这两种方式都可以产生弹簧曲线。
下面分别做介绍。
2、springCurve
springCurve的接口定义如下:
springCurve(velocity: number, mass: number, stiffness: number, damping: number)
其中四个参数的含义如下:
-
-
velocity(必填)
初始速度。是由外部因素对弹性动效产生的影响参数,其目的是保证对象从之前的运动状态平滑的过渡到弹性动效。取值范围:(-∞, +∞)
-
mass(必填)
质量。弹性系统的受力对象,会对弹性系统产生惯性影响。质量越大,震荡的幅度越大,恢复到平衡位置的速度越慢。取值范围:(0, +∞)
-
stiffness(必填)
刚度。是物体抵抗施加的力而形变的程度。在弹性系统中,刚度越大,抵抗变形的能力越强,恢复到平衡位置的速度就越快。取值范围:(0, +∞)
-
damping(必填)
阻尼。是一个纯数,无真实的物理意义,用于描述系统在受到扰动后震荡及衰减的情形。阻尼越大,弹性运动的震荡次数越少、震荡幅度越小。取值范围:(0, +∞)
-
springCurve可以设置初速度,单一属性存在多个动画时不会互相影响,观察到的是多个动画效果的叠加。
一个初速度分别是50与200的Demo如下:
import curves from '@ohos.curves';
@Entry
@Component
struct SpringTest {
@State translateX: number = 0;
private jumpWithSpeed(speed: number) {
this.translateX = -1;
animateTo({ duration: 2000, curve: curves.springCurve(speed, 1, 1, 1.2) }, () => {
// 以指定初速度进行x方向的平移的弹簧动画
this.translateX = 0;
})
}
build() {
Column() {
Button("button")
.fontSize(14)
.width(100)
.height(50)
.margin(30)
.translate({ x: this.translateX })
Row({space:50}) {
Button("jump 50").fontSize(14)
.onClick(() => {
// 以初速度50的弹簧曲线进行平移
this.jumpWithSpeed(50);
})
Button("jump 200").fontSize(14)
.onClick(() => {
// 以初速度200的弹簧曲线进行平移
this.jumpWithSpeed(200);
})
}.margin(30)
}.height('100%').width('100%')
}
}
示例中,点击不同的按钮,给定springCurve的不同初速度,button会有“弹性”的到达指定位置,且button的振幅随着速度的增大而变大。另外也可以修改springCurve的质量、刚度、阻尼参数,达到想要的弹性的程度。
💡 其中速度只是放大了振荡的效果,但系统能否产生振荡的效果,取决于弹簧振子本身的物理参数,即质量、刚度、阻尼三个参数。刚度越小、阻尼越大,springCurve的“弹性”越弱,振荡效果越弱。随着刚度减小或阻尼变大,达到过阻尼状态后,无论速度为多大,都不会有在终点值附近振荡的效果。
3、springMotion与responsiveSpringMotion
springMotion接口定义如下:
springMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
其中三个参数含义如下:
-
-
response(可选)
弹簧自然振动周期,决定弹簧复位的速度。默认值:0.55, 单位:秒,取值范围:[0, +∞)
-
dampingFraction(可选)
阻尼系数。0表示无阻尼,一直处于震荡状态;大于0小于1的值为欠阻尼,运动过程中超出目标值;等于1为临界阻尼;大于1为过阻尼,运动过程中逐渐趋于目标值。默认值:0.825,单位:秒,取值范围:[0, +∞)
-
overlapDuration(可选)
弹性动画衔接时长。发生动画继承时,如果前后两个弹性动画response不一致,response参数会在overlapDuration时间内平滑过渡。默认值:0,单位:秒,取值范围:[0, +∞)。弹性动画曲线为物理曲线,animation、animateTo中的duration参数不生效,动画持续时间取决于springMotion动画曲线参数和之前的速度。时间不能归一,故不能通过该曲线的interpolate函数获得插值。
-
responsiveSpringMotion接口定义如下:
responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
其中三个参数与springMotion的相同。
springMotion虽然内部有速度机制,但不可由开发者设置。在单一属性存在多个动画时,后一动画会取代前一动画,并继承前一动画的速度。
使用springMotion和responsiveSpringMotion曲线时,duration不生效,适合于跟手动画。
import curves from '@ohos.curves';
@Entry
@Component
struct SpringMotionTest {
@State positionX: number = 100;
@State positionY: number = 100;
diameter: number = 50;
build() {
Column() {
Row() {
Circle({ width: this.diameter, height: this.diameter })
.fill(Color.Blue)
.position({ x: this.positionX, y: this.positionY })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Move) {
// 跟手过程,使用responsiveSpringMotion曲线
animateTo({ curve: curves.responsiveSpringMotion() }, () => {
// 减去半径,以使球的中心运动到手指位置
this.positionX = event.touches[0].screenX - this.diameter / 2;
this.positionY = event.touches[0].screenY - this.diameter / 2;
console.info(`move, animateTo x:${this.positionX}, y:${this.positionY}`);
})
} else if (event.type === TouchType.Up) {
// 离手时,使用springMotion曲线
animateTo({ curve: curves.springMotion() }, () => {
this.positionX = 100;
this.positionY = 100;
console.info(`touchUp, animateTo x:100, y:100`);
})
}
})
}
.width("100%").height("80%")
.clip(true) // 如果球超出父组件范围,使球不可见
.backgroundColor(Color.Orange)
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) {
Text("拖动小球").fontSize(16)
}
.width("100%")
Row() {
Text('点击位置: [x: ' + Math.round(this.positionX) + ', y:' + Math.round(this.positionY) + ']').fontSize(16)
}
.padding(10)
.width("100%")
}.height('100%').width('100%')
}
}
以上代码是跟手动画的一个示例。通过在onTouch事件中,捕捉触摸的位置,改变组件的translate或者position属性,使其在跟手过程中运动到触摸位置,松手后回到原位置。跟手动画的效果如下:
跟手过程推荐使用responsiveSpringMotion曲线,松手过程推荐使用springMotion曲线。跟手过程随着手的位置变化会被多次触发,所以会接连启动多次responsiveSpringMotion动画,松手时启动一次springMotion动画。跟手、松手过程在对同一对象的同一属性上执行动画,且使用了springMotion或responsiveSpringMotion曲线,每次新启动的动画会继承上次动画使用的速度,实现平滑过渡。
4、one more thing
在鸿蒙UI系统组件06——进度条(Progress)文章的留言中,有朋友提到:如何做到5%但10%是滑动增加效果而不是闪到10%?
该文章中,我们使用的是Progress系统组件,而Progress使用的是value值来控制进度的显示,因此普通的显示动画和属性动画无法直接作用。此时我们还想实现类似的效果,可以考虑使用本章中提到的Curves函数。
实现代码如下:
import Curves from '@ohos.curves'
@Entry
@Component
struct SecondPage {
@State progressValue: number = 0 // 设置进度条初始值为0
@State step: number = 10;
build() {
Row() {
Column() {
Progress({ value: 0, total: 100, type: ProgressType.Capsule })
.width(200)
.height(50)
.style({ strokeWidth: 50 })
.value(this.progressValue)
Row().width('100%').height(5)
Button("进度条+" + this.step)
.onClick(async () => {
let curveValue = Curves.initCurve(Curve.EaseInOut) // 创建一个默认先慢后快插值曲线
let curValue = this.progressValue;
// 动画执行1s,每帧10s的间隙
const duration = 1000, frameTime = 10;
const count = duration / frameTime; // 需要渲染的帧数
for (let current = 0; current < count; current++) {
let value = curveValue.interpolate(current / count);
this.progressValue = curValue + value * this.step;
current++;
await new Promise<boolean>(res => setTimeout(res, frameTime)); // 等待绘制下一帧
}
})
}
.width('100%')
}
.height('100%')
}
}
效果如下: