什么是显式动画
啊, 尽管有点糙,但还是解释一下吧, 显式动画里面的“显式”二字, 是程序员在代码调用的时候,就三令五申,明明白白调用动画API而创建的动画。 这个API的名字就是: animateTo。这就是显式动画。说白了您可以大致理解为,显式动画,就是调用animateTo来完成的动画。
animateTo API概述
animateTo是common文件中的一个方法,其声明为:
declare function animateTo(value: AnimateParam, event: () => void): void;
好,接下来我们梳理一下这个方法!
animateTo用法
animateTo由于是一个common的方法,且其声明中并没有指定要改什么组件,那么它是怎么改动相关组件界面的呢?答案是,通过改动状态变量而联动界面变化。改动位置就是animateTo的第二个参数。
declare function animateTo(value: AnimateParam, event: () => void): void;
这个函数里面的event参数。 代表的是一套动画最终的值要变成的样子。哪个组件和这个值有关联,哪个组件的界面就会变。
如下文中的案例:
@Entry
@Component
struct AnimateToTest1 {
@State widthSize:number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
private flag: boolean = true
build() {
Column() {
Button('Change Size')
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(()=>{
if (this.flag) {
this.getUIContext().animateTo({
duration: 200, //2000毫秒
curve: Curve.EaseOut, //快速开始,逐渐减速到终点
iterations: 1, //重复1次
playMode: PlayMode.Normal, //正常播放从头到尾
onFinish:()=>{
console.log('play end')
}
},()=>{
this.widthSize = 150
this.heightSize = 60
})
} else {
this.getUIContext().animateTo({
duration: 200
}, ()=>{
this.widthSize = 250
this.heightSize = 100
})
}
this.flag = !this.flag
})
Button('stop rotating')
.margin(50)
.rotate({x: 0, y: 0, z: 1, angle:this.rotateAngle})
.onAppear(()=>{ //组件出现的时候开始动画
this.getUIContext().animateTo({
duration: 1200,
curve: Curve.Friction, //阻尼效果
delay: 500,
iterations: -1,
playMode: PlayMode.Alternate, //正反正反
expectedFrameRateRange: {
min: 10,
max: 120,
expected: 60
},
}, ()=>{
this.rotateAngle = 90
})
})
.onClick(()=>{
this.getUIContext().animateTo({
duration: 0// 停止动画
}, ()=>{this.rotateAngle = 0})
})
.width(this.widthSize) //这个宽高和上个按钮采取同样的宽高,这样可以展示出
.height(this.heightSize) //animateTo仅仅只和状态变量有关
}
.height('100%')
.width('100%')
}
}
效果展示
动画执行后组件消失
//动画执行结束后组价消失
@Entry
@Component
struct AnimateToTest2 {
@State heightSize: number = 100
@State isShow: boolean = true
@State count: number = 0
private isToBottom: boolean = true
build() {
Column() {
if (this.isShow) {
Column()
.width(200)
.height(this.heightSize)
.backgroundColor(Color.Blue)
.onClick(() => {
this.getUIContext().animateTo({
duration: 600,
curve: Curve.EaseOut,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
console.log("动画onFinish调用")
this.count--
if (0 == this.count && !this.isToBottom) {
this.isShow = false //isShow为false的时候触发条件渲染, 那么这个节点就被删除啦!
}
}
}, () => {
this.count++
if (this.isToBottom) {
this.heightSize = 60
} else {
this.heightSize = 100
}
})
this.isToBottom = !this.isToBottom
})
}
}
.height('100%')
.width('100%')
.margin({top: 5})
.justifyContent(FlexAlign.End)
}
}
效果展示
实战-听歌识曲水波圆环动效
本节代码用于实现一个听歌识曲的动效。在代码实现的时候要注意
- 显式动画的打断或者取消,需要靠 animateTo方法,填入的配置参数中,将播放时长改为0 即可打断相关状态变量的变更。
- 听歌识曲的动画实现,靠的是不同层的叠加,利用时间差,改透明度和倍数。对于动画的剖析有时候要仔细观察,按属性来拆分。以实现UX同学描述的效果。
import { Scale } from '@kit.ArkUI'
@Entry
@Component
struct Index {
@State isListening: boolean = false
@State immediatelyOpacity: number = 0.5
@State immediatelyScale: Scale = { x: 0, y: 0 }
@State delayOpacity: number = 0.5
@State delayScale: Scale = { x: 1, y: 1 }
private readonly BUTTON_SIZE: number = 120
private readonly BUTTON_CLICK_SCALE: number = 0.8
private readonly ANIMATION_DURATION: number = 1300
private readonly ANIMATION_ITERATIONS = -1
private readonly COMMON_NUMBER = 0
@Styles
ripplesStyle(){
.width(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
.height(this.BUTTON_SIZE * this.BUTTON_CLICK_SCALE)
.borderRadius(this.BUTTON_SIZE * (this.BUTTON_CLICK_SCALE / 2))
.backgroundColor(Color.Red)
}
build() {
Stack() {
Stack()//最底部最大圈
.ripplesStyle()
.opacity(this.immediatelyOpacity)
.scale(this.immediatelyScale)
Stack()//上方层次的圆环
.ripplesStyle()
.opacity(this.delayOpacity)
.scale(this.delayScale)
Button({ type: ButtonType.Circle })//音乐图,因为我没有图片资源,直接用颜色代替的.
.width(this.BUTTON_SIZE)
.height(this.BUTTON_SIZE)
.zIndex(1)
.backgroundColor(Color.Red)
.clickEffect({ level: ClickEffectLevel.HEAVY, scale: this.BUTTON_CLICK_SCALE })
.onClick(() => {
this.isListening = !this.isListening
if (this.isListening) {
this.getUIContext().animateTo({
duration: this.ANIMATION_DURATION,
iterations: this.ANIMATION_ITERATIONS,
curve: Curve.EaseInOut, // 缓进缓出, 低速->加速->减速->低速
playMode:PlayMode.Normal
}, () => {
this.immediatelyOpacity = 0 //不透明度降到0,也就是最终不可见
this.immediatelyScale = {
x: 6,
y: 6
}
})
this.getUIContext().animateTo({
duration: this.ANIMATION_DURATION,
iterations: this.ANIMATION_ITERATIONS,
curve: Curve.EaseInOut,
delay: 200,
playMode:PlayMode.Normal
}, () => {
this.delayOpacity = 0
this.delayScale = {
x: 6,
y: 6
}
})
} else {
// duration改为0即可取消动画
//恢复所有状态为初始值, 以确保动画重新开始的时候依然按照以前的惯性去执行
this.getUIContext().animateTo({ duration: this.COMMON_NUMBER, iterations:0 }, () => {
this.immediatelyOpacity = 0.5
this.immediatelyScale = {
x: 0,
y: 0
}
this.delayOpacity = 0.5
this.delayScale = {
x: 1,
y: 1
}
})
}
})
}
.height('100%')
.width('100%')
}
}
显式动画优劣点
优点
- 复杂的动画优先用显式动画。原因是当动画涉及到的状态值,涉及到改动很多值的时候,显式动画会一并改掉您写的event函数中的所有相关值,之后再在界面进行统一的刷新。相比于隐式动画(这个之后我们会开一篇文章学), 它具有事务性,刷新的次数少 。 隐式动画呢,是改其中一个属性就会刷一遍, 写个简单的动画还可以,复杂动画,建议用显式动画。
- 显式动画仅仅修改的是动画的表现,不直接改变属性的值,避免了隐式动画那种改变组件属性值变化而导致的副作用(例如重新测量measure, layout之类)。
缺点
- 要记得清理资源,在销毁资源的时候记得调用animateTo({duration:0}, ()=>{})来终止动画!
- 代码复杂度比较高,so, API最好了解的细致一些。
- 需要手动同步状态, 尤其是在动画结束的时候,对状态进行恢复。就像我们上方代码中,else逻辑中, 所有相关的属性全部回归原值。 如果不回归原值,会出现“回弹”的现象。
- 复杂的关键帧动画,可能导致帧率的下降。