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
        })
      })
  }
}

鸿蒙开发者班级

posted @ 2025-11-27 12:57  神龙教主  阅读(1)  评论(0)    收藏  举报