HarmonyOS 关键帧动画 keyframeAnimateTo
使用UI上下文API中的.keyframeAnimateTo()方法创建关键帧动画,其中传两个参数,第一个参 数是个对象,此对象有三个参数delay、iterations、onFinish;第二个参数是数组,其中是一个个对象,每个对象都是一个关键帧
基础使用
@Entry
@Component
struct Index {
@State scaleXY:number = 1
build() {
Text().width(100).height(100).backgroundColor(Color.Red)
.scale({ x: this.scaleXY, y: this.scaleXY }) // 1. who ✅
.onClick(() => {
// 设置关键帧动画整体播放3次 2. fixed 💕
this.getUIContext().keyframeAnimateTo({ iterations: 3 }, [
{
// 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画
duration: 800,
event: () => {
this.scaleXY = 1.5;
}
},
{
// 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画
duration: 500,
event: () => {
this.scaleXY = 1;
}
}
])
})
}
}
实战案例1:图片点击按下效果
@Entry
@Component
struct Index {
@State scaleXY: number = 1
@State selectIndex:number = -1
@State imgSrc: string[] = [
'https://tse2-mm.cn.bing.net/th/id/OIP-C.CFev6LAEXxvcqAH9BkJvMwHaNK?rs=1&pid=ImgDetMain',
'https://pic2.zhimg.com/v2-379be37e0b4d372aa60046f9ce771f12_r.jpg',
'https://pic-image.yesky.com/uploadImages/newPic/2023/188/04/7H87NC6W8T0A.png',
'https://c-ssl.duitang.com/uploads/item/202004/04/20200404163239_avipl.jpg',
'https://tse1-mm.cn.bing.net/th/id/OIP-C.njlhPi8v28sP8mpG3LlujwAAAA?rs=1&pid=ImgDetMain',
'https://tse2-mm.cn.bing.net/th/id/OIP-C.WgwRdIP_PFIE19eyu-cHgQHaJW?rs=1&pid=ImgDetMain'
]
build() {
Column() {
WaterFlow() {
ForEach(this.imgSrc, (item: string,index:number) => {
FlowItem() {
Image(item)
.width('100%')
.scale({ x:this.selectIndex===index? this.scaleXY:1, y: this.selectIndex===index? this.scaleXY:1 })
}
.onClick(() => {
this.selectIndex = index
this.getUIContext().keyframeAnimateTo({ iterations: 1 }, [
{
// 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画
duration: 100,
event: () => {
this.scaleXY = 0.75;
}
},
{
// 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画
duration: 200,
event: () => {
this.scaleXY = 1;
}
}
])
})
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}
.width('100%')
.padding(10)
}
}
实战案例2:多组关键帧动画
import { UIContext } from '@kit.ArkUI';
@Entry
@Component
struct MotionPathExample {
uiContext: UIContext | undefined = undefined;
aboutToAppear() {
this.uiContext = this.getUIContext?.();
}
// x、y、scale
@State people: number[] = [0, 0, 1]
@State star1: number[] = [40, -100, 1]
@State star2: number[] = [60, -100, 1]
@State star3: number[] = [80, -100, 1]
build() {
Column() {
Stack({ alignContent: Alignment.Bottom }) {
Image('https://pic.quanjing.com/0a/10/QJ8291629136.jpg@%21794ws')
.width(160)
.offset({ x: this.people[0], y: this.people[1] })
.scale({ x: this.people[2], y: this.people[2] })
.onClick(() => {
if (this.uiContext) {
this.uiContext.keyframeAnimateTo({ iterations: 1 }, [
{
// 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画
duration: 2000,
event: () => {
this.people[0] = 40 // x
this.people[1] = -200 // y
this.people[2] = 1.2 // scale
this.star1[0] = 60 // x
this.star1[1] = -300 // y
this.star1[2] = 0.8 // scale
this.star2[0] = 90 // x
this.star2[1] = -300 // y
this.star3[2] = 0.8 // scale
this.star3[0] = 120 // x
this.star3[1] = -300 // y
this.star3[2] = 0.8 // scale
}
},
{
// 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画
duration: 1500,
event: () => {
this.people[0] = 100 // x
this.people[1] = -300 // y
this.people[2] = 0.8 // scale
this.star1[0] = 100 // x
this.star1[1] = -400 // y
this.star2[0] = 100 // x
this.star2[1] = -400 // y
this.star3[0] = 100 // x
this.star3[1] = -400 // y
}
}
]);
}
})
Image('https://img0.baidu.com/it/u=2134749333,229655791&fm=253&fmt=auto&app=138&f=JPEG?w=300&h=277')
.width(30)
.offset({ x: this.star1[0], y: this.star1[1] })
.scale({ x: this.star1[2], y: this.star1[2] })
Image('https://img2.baidu.com/it/u=3837575474,649267201&fm=253&fmt=auto&app=138&f=PNG?w=304&h=304')
.width(20)
.offset({ x: this.star2[0], y: this.star2[1] })
.scale({ x: this.star2[2], y: this.star2[2] })
Image('https://img1.baidu.com/it/u=3978696291,2335621355&fm=253&fmt=auto&app=138&f=PNG?w=256&h=256')
.width(20)
.offset({ x: this.star3[0], y: this.star3[1] })
.scale({ x: this.star3[2], y: this.star3[2] })
}.width('100%').height('100%')
}.width('100%').height('100%')
}
}
实战案例3:抖动效果
可以查一下手机震动api效果更佳
@Entry
@Component
struct Index {
@State translateX:number = 0
build() {
Column() {
Text('请阅读并勾选协议')
.backgroundColor(Color.Black).fontColor(Color.White)
.padding({left:10,right:10,top:5,bottom:5}).margin({top:20,bottom:20})
.translate({x:this.translateX})
Button('本机号码号一键登录')
.onClick(() => {
this.getUIContext().keyframeAnimateTo({ iterations: 2 }, [
{
// 第一段关键帧动画时长为50ms
duration: 50,
event: () => {
this.translateX = 5;
}
},
{
// 第二段关键帧动画时长为100ms
duration: 100,
event: () => {
this.translateX = 0;
}
}
])
})
}
}
}
- 或者用显示动画也阔以
@Entry
@Component
struct Index {
@State translateX:number = 0
build() {
Column() {
Text('请阅读并勾选协议')
.backgroundColor(Color.Black).fontColor(Color.White)
.padding({left:10,right:10,top:5,bottom:5}).margin({top:20,bottom:20})
.translate({x:this.translateX})
Button('本机号码号一键登录')
.onClick(() => {
animateTo({
duration: 100,
playMode: PlayMode.AlternateReverse,
iterations: 3,
// onFinish: () => {
// this.translateX = 0
// }
}, () => {
this.translateX = 100
})
})
}
}
}
实战案例4:骨架片
@Entry
@Component
export struct Index {
@State translateValue: string = '-100%'
@State arr: number[] = [1,2,3,4,5]
@State columnOpacityCom: number = 0.5;
@State listItemHeight: string = '20%'
@Builder
columnShowBuilder(width: string, height: string, aspectRatioNum?: number) {
Stack() {
Column()
.linearGradient({
angle: 90,
colors: [['#f2f2f2', 0.25], ['#e6e6e6', 0.37], ['#f2f2f2', 0.63]],
})
.height('100%')
.width('100%')
Column()
.height('100%')
.width('100%')
.translate({ x: this.translateValue })
.linearGradient({
angle: 90,
colors: [
['rgba(255,255,255,0)', 0],
['rgba(255,255,255,1)', 0.5],
['rgba(255,255,255,0)', 1]
]
})
}
.width(width)
.height(height)
.borderRadius(2)
.aspectRatio(aspectRatioNum)
.clip(true)
}
build() {
Column() {
List({ initialIndex: 0 }) {
ForEach(this.arr, () => {
ListItem() {
Column({ space: 20 }) {
Row({ space: 15 }) {
this.columnShowBuilder('20%', '', 1)
Column({ space: 10 }) {
this.columnShowBuilder('50%', '25%')
this.columnShowBuilder('30%', '20%')
}
.alignItems(HorizontalAlign.Start)
}
.justifyContent(FlexAlign.Start)
.width('100%')
.height('25%')
Column({ space: 10 }) {
this.columnShowBuilder('80%', '25%')
this.columnShowBuilder('90%', '25%')
this.columnShowBuilder('60%', '25%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.height('25%')
}
.margin({ top: '4%', bottom: '2%' })
}
.width('90%')
.height(this.listItemHeight)
}, (item: number) => JSON.stringify(item))
}
.width('100%')
.height('100%')
.alignListItem(ListItemAlign.Center)
.listDirection(Axis.Vertical)
.divider({ strokeWidth: 1, color: Color.Gray })
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Off)
.onAppear(() => {
this.getUIContext().keyframeAnimateTo({
iterations: -1,
}, [
{
duration: 500,
curve: Curve.Linear,
event: () => {
this.translateValue = '0'
}
},
{
duration: 500,
curve: Curve.Linear,
event: () => {
this.translateValue = '100%'
}
},
]);
})
}
.width('100%')
.padding({ top: 10 })
}
}
实战案例5:呼吸灯
- 关键帧 也可以通过显示动画如下
@Entry
@Component
struct Index {
@State myScale: number = 1
uiContext: UIContext | undefined = undefined;
@State timeId: number = 0
@State @Watch("changeSpeed") speed: number = 0
@State flag: boolean = false
aboutToAppear() {
this.uiContext = this.getUIContext?.();
this.timeId = setInterval(() => {
this.speed++
}, 500)
}
changeSpeed() {
if (this.speed == 5) {
// 触发关键帧动画
if (!this.flag) this.haha()
this.flag = !this.flag
} else if (this.speed == 10) {
clearInterval(this.timeId)
this.timeId = setInterval(() => {
this.speed--
}, 500)
} else if (this.speed == 0) {
clearInterval(this.timeId)
}
}
// 关键帧动画
haha() {
if (!this.uiContext) {
console.info("no uiContext, keyframe failed");
return;
}
this.myScale = 1;
this.uiContext.keyframeAnimateTo({ iterations: 10 }, [
{
duration: 250,
event: () => {
this.myScale = 1.15;
}
},
{
duration: 250,
event: () => {
this.myScale = 1;
}
}
])
}
build() {
Column() {
Column() {
Column() {}
.width(90)
.height(90)
.borderRadius(45)
.backgroundColor(Color.Blue)
.linearGradient({
direction: GradientDirection.LeftBottom,
colors: [[this.flag ? 'red' : 'blue', 0.1], [this.flag ? 'FFF18888' : '#ff00aeff', 0.9]]
})
}
.width(100)
.height(100)
.borderRadius(50)
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
.scale({ x: this.myScale, y: this.myScale })
Column() {
Text(this.speed.toString()).fontWeight(FontWeight.Bold)
Text('km/h')
}
.width(80)
.height(80)
.borderRadius(40)
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
.offset({ x: 0, y: -90 })
}.width('100%').height('100%').backgroundColor(Color.Gray).justifyContent(FlexAlign.Center)
}
}
- 显示动画 还得调整下排版 自己来
@Entry
@Component
struct Index {
@State scaleX: number = 1
@State scaleY: number = 1
build() {
Text()
.width(50).height(50).backgroundColor(Color.Red).borderRadius(25)
.scale({ x: this.scaleX, y: this.scaleY }) // 1 who ✅
.onClick(()=>{ // 2 find&fixed 🔍 💕
animateTo({duration: 2000,curve: Curve.EaseOut,iterations: -1},()=>{
this.scaleX = 2
this.scaleY = 2
})
})
}
}

浙公网安备 33010602011771号