37、鸿蒙HarmonyOS Next开发:使用动画-动画曲线(传统曲线和弹簧曲线)

目录

概述

传统曲线

弹簧曲线

概述

动画曲线是属性关于时间的变化函数,决定属性变化时产生动画的运动轨迹。某一时刻下动画曲线的斜率代表动画的速度,对应属性变化的快慢。一条优秀的动画曲线具备连续光滑、符合用户意图、符合物理世界客观规律的特点。开发者可结合用户的使用场景和意图,为动效选取合适的动画曲线。

根据动画曲线是否符合物理世界客观规律,可将其分为物理曲线(ArkUI当前提供了多种物理弹簧曲线)和传统曲线两种类型。

传统曲线

传统曲线基于数学公式,创造形状符合开发者预期的动画曲线。以三阶贝塞尔曲线为代表,通过调整曲线控制点,可以改变曲线形状,从而带来缓入、缓出等动画效果。对于同一条传统曲线,由于不具备物理含义,其形状不会因为用户行为发生任何改变,缺少物理动画的自然感和生动感。建议优先采用物理曲线创建动画,将传统曲线作为辅助用于极少数必要场景中。

ArkUI提供了贝塞尔曲线、阶梯曲线等传统曲线接口。

传统曲线的示例和效果如下:

class MyCurve {

public title: string;

public curve: Curve;

public color: Color | string;

constructor(title: string, curve: Curve, color: Color | string = '') {

this.title = title;

this.curve = curve;

this.color = color;

}

}

const myCurves: MyCurve[] = [

new MyCurve(' Linear', Curve.Linear, '#317AF7'),

new MyCurve(' Ease', Curve.Ease, '#D94838'),

new MyCurve(' EaseIn', Curve.EaseIn, '#DB6B42'),

new MyCurve(' EaseOut', Curve.EaseOut, '#5BA854'),

new MyCurve(' EaseInOut', Curve.EaseInOut, '#317AF7'),

new MyCurve(' FastOutSlowIn', Curve.FastOutSlowIn, '#D94838')

]

@Entry

@Component

struct CurveDemo {

@State dRotate: number = 0; // 旋转角度

build() {

Column() {

// 曲线图例

Grid() {

ForEach(myCurves, (item: MyCurve) => {

GridItem() {

Column() {

Row()

.width(30)

.height(30)

.borderRadius(15)

.backgroundColor(item.color)

Text(item.title)

.fontSize(15)

.fontColor(0x909399)

}

.width('100%')

}

})

}

.columnsTemplate('1fr 1fr 1fr')

.rowsTemplate('1fr 1fr 1fr 1fr 1fr')

.padding(10)

.width('100%')

.height(300)

.margin({ top: 50 })

Stack() {

// 摆动管道

Row()

.width(290)

.height(290)

.border({

width: 15,

color: 0xE6E8EB,

radius: 145

})

ForEach(myCurves, (item: MyCurve) => {

// 小球

Column() {

Row()

.width(30)

.height(30)

.borderRadius(15)

.backgroundColor(item.color)

}

.width(20)

.height(300)

.rotate({ angle: this.dRotate })

.animation({

duration: 2000,

iterations: -1,

curve: item.curve,

delay: 100

})

})

}

.width('100%')

.height(200)

.onClick(() => {

this.dRotate ? null : this.dRotate = 360;

})

}

.width('100%')

}

}

弹簧曲线

阻尼弹簧曲线(以下简称弹簧曲线)对应的阻尼弹簧系统中,偏离平衡位置的物体一方面受到弹簧形变产生的反向作用力,被迫发生振动。另一方面,阻尼的存在为物体振动提供阻力。除阻尼为0的特殊情况,物体在振动过程中振幅不断减小,且最终趋于0,其轨迹对应的动画曲线自然连续。

采用弹簧曲线的动画在达终点时动画速度为0,不会产生动画“戛然而止”的观感,以避免影响用户体验。

ArkUI提供了四种阻尼弹簧曲线接口。

1、springMotion:创建弹性动画,动画时长由曲线参数、属性变化值大小和弹簧初速度自动计算,开发者指定的动画时长不生效。

springMotion不提供速度设置接口,速度通过继承获得,无需开发者指定。对于某个属性,如果当前存在正在运行的springMotion或者responsiveSpringMotion类型动画,新创建的弹簧动画将停止正在运行的动画,并继承其当前时刻的动画属性值和速度作为新建动画的初始状态。此外,接口提供默认参数,便于开发者直接使用。

function springMotion(response?: number, dampingFraction?: number, overlapDuration?: number): ICurve;

2、responsiveSpringMotion:是springMotion动画的一种特例,仅默认参数不同。一般用于跟手做成动画的场景,离手时可用springMotion创建动画,此时离手阶段动画将自动继承跟手阶段动画速度,完成动画衔接。

当新动画的overlapDuration参数不为0,且当前属性的上一个springMotion动画还未结束时,response和dampingFraction将在overlapDuration指定的时间内,从旧动画的参数值过渡到新动画的参数值。

function responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number): ICurve;

3、interpolatingSpring:适合于需要指定初速度的动效场景,动画时长同样由接口参数自动计算,开发者在动画接口中指定的时长不生效。

曲线接口提供速度入参,且由于接口对应一条从0到1的阻尼弹簧曲线,实际动画值根据曲线进行插值计算。所以速度也应该为归一化速度,其值等于动画属性改变的绝对速度除以动画属性改变量。因此不适合于动画起点属性值和终点属性值相同的场景,此时动画属性改变量为0,归一化速度不存在。

function interpolatingSpring(velocity: number, mass: number, stiffness: number, damping: number): ICurve;

4、springCurve:适合于需要直接指定动画时长的场景。springCurve接口与interpolatingSpring接口几乎一致,但是对于采用springCurve的动画,会将曲线的物理时长映射到指定的时长,相当于在时间轴上拉伸或压缩曲线,破坏曲线原本的物理规律,因此不建议开发者使用。

function springCurve(velocity: number, mass: number, stiffness: number, damping: number): ICurve;

弹簧曲线的示例代码和效果如下。

import { curves } from '@kit.ArkUI';

class Spring {

public title: string;

public subTitle: string;

public iCurve: ICurve;

constructor(title: string, subTitle: string, iCurve: ICurve) {

this.title = title;

this.iCurve = iCurve;

this.subTitle = subTitle;

}

}

// 弹簧组件

@Component

struct Motion {

@Prop dRotate: number = 0

private title: string = ""

private subTitle: string = ""

private iCurve: ICurve | undefined = undefined

build() {

Column() {

Circle()

.translate({ y: this.dRotate })

.animation({ curve: this.iCurve, iterations: -1 })

.foregroundColor('#317AF7')

.width(30)

.height(30)

Column() {

Text(this.title)

.fontColor(Color.Black)

.fontSize(10).height(30)

Text(this.subTitle)

.fontColor(0xcccccc)

.fontSize(10).width(50)

}

.borderWidth({ top: 1 })

.borderColor(0xf5f5f5)

.width(80)

.alignItems(HorizontalAlign.Center)

.height(100)

}

.height(110)

.margin({ bottom: 5 })

.alignItems(HorizontalAlign.Center)

}

}

@Entry

@Component

export struct SpringCurve {

@State dRotate: number = 0;

private springs: Spring[] = [

new Spring('springMotion', '周期1, 阻尼0.25', curves.springMotion(1, 0.25)),

new Spring('responsive' + '\n' + 'SpringMotion', '弹性跟手曲线', curves.responsiveSpringMotion(1, 0.25)),

new Spring('interpolating' + '\n' + 'Spring', '初始速度10,质量1, 刚度228, 阻尼30',

curves.interpolatingSpring(10, 1, 228, 30)),

new Spring('springCurve', '初始速度10, 质量1, 刚度228, 阻尼30', curves.springCurve(10, 1, 228, 30))

];

build() {

Row() {

ForEach(this.springs, (item: Spring) => {

Motion({

title: item.title,

subTitle: item.subTitle,

iCurve: item.iCurve,

dRotate: this.dRotate

})

})

}

.justifyContent(FlexAlign.Center)

.alignItems(VerticalAlign.Bottom)

.width('100%')

.height(437)

.margin({ top: 20 })

.onClick(() => {

this.dRotate = -50;

})

}

}