HarmonyOS 属性动画.animation()

基础使用

- 1. 属性动画响应式(确定谁加动画)

@State scaleX: number = 1
@State scaleY: number = 1

Text().width(50).height(50).backgroundColor(Color.Red)
  .scale({ x: this.scaleX,  y: this.scaleY })

- 2. 触发改变 (发现没有动画)

//未使用动画属性,缩放变化生硬
Text().width(50).height(50).backgroundColor(Color.Red)
  .scale({ x: this.scaleX,  y: this.scaleY })
  .onClick(()=>{
    this.scaleX = 2
    this.scaleY = 2
  }) 

- 3. 增加 animation 动画属性 (让动画生效)

//使用动画过渡,属性值变化丝滑、流畅
Text()
  .width(50).height(50).backgroundColor(Color.Red)
  .scale({ x: this.scaleX,  y: this.scaleY })
  .animation({
    duration: 2000,						//动画持续时间(ms),默认为1000
    curve: Curve.EaseOut,			//动画速率曲线(参数值详见官方文档),默认为匀速linear
    iterations: 3,						//动画播放次数,-1是无限次,默认为1次
    playMode: PlayMode.Normal	//动画播放模式,默认播放完成后重头开始播放
  })
  .onClick(()=>{
    this.scaleX = 2
    this.scaleY = 2
  })

实战案例1: 基础案例

@Entry
@Component
struct Index {
  @State widthSize: number = 250
  @State heightSize: number = 100
  @State rotateAngle: number = 0
  @State flag: boolean = true

  build() {
    Column() {
      Button('change size').margin(30)
        .width(this.widthSize)   // 1 谁 ✅
        .height(this.heightSize)  // 1 谁 ✅
        .animation({ // 3 fixed💕
          duration: 2000,
          curve: Curve.EaseOut,
          iterations: 3,             // 重复次数
          playMode: PlayMode.Normal  // 播放模式
        })
        .onClick(() => {  // 2 find 🔍
          if (this.flag) {
            this.widthSize = 150
            this.heightSize = 60
          } else {
            this.widthSize = 250
            this.heightSize = 100
          }
          this.flag = !this.flag
        })
      Button('change rotate angle').margin(50)
        .rotate({ angle: this.rotateAngle })  // 1 谁 ✅
        .animation({ // 3 fixed💕
          duration: 1200,
          curve: Curve.Friction,
          delay: 500,
          iterations: -1, // 设置-1表示动画无限循环
          playMode: PlayMode.Alternate,
          expectedFrameRateRange: { // 开发者可以为不同的动画配置不同的期望绘制帧率,从而优化动画的流畅度和性能
            min: 20,
            max: 120,
            expected: 90,
          }
          // 绘制帧率和手机刷新率的关系
          // 绘制帧率(Frame Rate)是指在一段时间内屏幕上显示的图像帧数,通常以每秒钟显示的帧数(FPS, Frames Per Second)来衡量。
          // 而手机刷新率是指屏幕每秒钟更新的次数,通常以Hz(赫兹)为单位。
          // 例如,60FPS表示每秒钟屏幕上显示60张图片,而120Hz的手机屏幕每秒钟会更新120次。
          // 为了获得更好的用户体验,绘制帧率应该尽量接近或高于手机的刷新率,否则可能会出现卡顿、掉帧等不流畅的现象‌
        })
        .onClick(() => { // 2 find 🔍
          this.rotateAngle = 90
        })
    }.width('100%').margin({ top: 20 })
  }
}

实战案例2: 启动页案例

http://tmp00002.zhaodashen.cn/1128df4079ce0a78b6c279cca86a31d4.jpg

@Entry
@Component
struct Index {
  @State top: number = 0
  @State state: number = 1

  onPageShow() {  // 2 find 🔍
    this.state = 0
    this.top = -200
  }

  build() {
    Column() {

      Column() {
        Text('不').fontSize(30).fontColor('###fff').margin({ top: 10 })
        Text('背').fontSize(30).fontColor('###fff').margin({ top: 10 })
        Column() {
          Text('单').fontSize(30).fontColor('###fff').margin({ top: 10 })
          Text('词').fontSize(30).fontColor('###fff').margin({ top: 10 })
        }
        .opacity(this.state)  // 1 who ✅   单词透明度慢慢变弱
        .animation({ duration: 1000, curve: Curve.EaseIn })  // 先慢后快  3 fixed💕
      }.width(80)
      .translate({ x: 0, y: this.top })  // 1 who ✅   不背单词一起 负数 向上
      .animation({ duration: 1000, curve: Curve.EaseIn })  // 先慢后快 3 fixed💕
    }.width('100%').height('100%').backgroundColor('###000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  }
}

实战案例3: 标签页案例

@Entry
@Component
struct Index {
  @State scrollLeft: number = 0
  @State tabsIndex: number = 0
  @State tabsLeft: number[] = [5, 80, 160, 240, 330]

  build() {
    Column() {
      Row() {
        ForEach(['全部', '待付款', '待发货', '待收货', '待评价'], (item: string, index: number) => {
          Text(item).height('100%')
            .fontWeight(this.tabsIndex === index ? 600 : 400)
            .onClick(() => {  // 2 find 🔍
              this.tabsIndex = index
              this.scrollLeft = this.tabsLeft[this.tabsIndex]
            })
        })
        // 文字下方横杠,点击后发生位移
        Text().width(24).height(2).backgroundColor('#191919').borderRadius(100000).position({x: 0,y: '100%'})
          .translate({  x: this.scrollLeft,  y: '-100%' })  // 1 who ✅
          .animation({ duration: 300, curve: Curve.Linear })  // 3 fixed 💕
      }
      .height(60).width('100%').justifyContent(FlexAlign.SpaceBetween).backgroundColor('#fff')
    }.height('100%').width('100%').backgroundColor('#f5f5f5')
  }
}

实战案例4: 回到顶部案例

@Entry
@Component
struct Index {
  private scroller: Scroller = new Scroller()
  @State scaleY: number = 0
  @State offsetY:number = -50

  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      Scroll(this.scroller) {
        Column() {
          ForEach(new Array(100).fill(6), (item: number, index) => {
            Text(index.toString()).width('100%').height(50).margin({ bottom: 50 }).backgroundColor(Color.Black).fontColor(Color.White).fontSize(30)
          })
        }
      }
      //滚动时触发
      .onDidScroll(() => {
        const y = this.scroller.currentOffset().yOffset
        console.log('位置:', JSON.stringify(this.scroller.currentOffset()))
        console.log('位置:', y)
        if (y >= 1000) {   // 2 find 🔍
          this.offsetY = -70
          this.scaleY = 1
        } else {
          this.offsetY = -10
          this.scaleY = 0
        }
      })


      Text('↑').fontSize(18).fontColor(Color.White).width(40).height(40).borderRadius(20).backgroundColor(Color.Green).textAlign(TextAlign.Center).offset({ x: -10, y: this.offsetY })
        .scale({ x: 1, y: this.scaleY })  // 1 who ✅
        .opacity(this.scaleY)  // 1 who ✅
        .animation({   // 3 fixed 💕
          duration: 500,
          curve: Curve.Friction,
        })
        .onClick(() => {
          //回到顶部动画设为true时,所有属性为默认值
          //this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: true })
          this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 2000, curve: Curve.Friction } })
        })
    }

  }
}

实战案例5:表单科技感

@Entry
@Component
export struct Index {

  @State loadingOpacity: number = 0

  @State isLogin: boolean = false
  @State inputWidth: number = 300
  @State inputRadius: number = 8
  @State inputVisibility: Visibility = Visibility.Visible

  build() {
      Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
        Image($r('app.media.startIcon')).width(60).height(60)

        Column() {
          Column() {
            LoadingProgress()
              .width(60)
              .height(60)
          }
          .width(80)
          .height(80)
          .borderRadius(40)
          .backgroundColor(Color.Transparent)
          .justifyContent(FlexAlign.Center)
          .position({ x: 90, y: 0 })
          .opacity(this.loadingOpacity)
          .animation({
            duration: 300,
            playMode: PlayMode.Alternate,
            expectedFrameRateRange: {
              min: 20,
              max: 120,
              expected: 90,
            }
          })
          TextInput({ placeholder: '请输入用户名' })
            .width(this.inputWidth)
            .height(50)
            .visibility(this.inputVisibility)
            .border({
              width: {
                left: 0,
                right: 0,
                top: 0,
                bottom: 1
              },
              color: { bottom: Color.Gray },
              radius: { topLeft: this.inputRadius, topRight: this.inputRadius },
              style: { bottom: BorderStyle.Solid }
            })
            .id('username')
            .defaultFocus(true)
            .animation({
              duration: 300,
              playMode: PlayMode.Alternate,
              onFinish: () => {
                if (this.isLogin) {
                  this.inputVisibility = Visibility.Hidden
                  this.loadingOpacity = 1
                  this.isLogin = false
                }
              },
              expectedFrameRateRange: {
                min: 20,
                max: 120,
                expected: 90,
              }
            })
          TextInput({ placeholder: '请输入密码' })
            .placeholderColor('#D4D3D1')
            .type(InputType.Password)
            .showPasswordIcon(false)
            .backgroundColor('#ffffff')
            .width(this.inputWidth)
            .height(50)
            .visibility(this.inputVisibility)
            .width(this.inputWidth)
            .borderRadius({ bottomLeft: this.inputRadius, bottomRight: this.inputRadius })
            .id('password')
            .animation({
              duration: 300,
              playMode: PlayMode.Alternate,
              expectedFrameRateRange: {
                min: 20,
                max: 120,
                expected: 90,
              }
            })
        }
        .width(260)
        .height(121)
        Row() {
          Checkbox().unselectedColor('#ffffff').width(16).borderRadius(8).backgroundColor(Color.White)
          Text('aaa').fontColor('#ffffff')
        }
        .width(300)
        Button('立即登录')
          .width(200).height(45).fontSize(28).type(ButtonType.Normal).backgroundColor('#30FFFFFF')
          .border({ width: 1, color: Color.White, radius: 8 }).margin({ top: 50, bottom: 60 })
          .onClick(() => {
            this.isLogin = true
            this.inputWidth = 80
            this.inputRadius = 40
            return
          })

        Row() {
          Text('bbb').fontColor(Color.White)
          Text('ccc').fontColor(Color.White)
        }.justifyContent(FlexAlign.SpaceBetween).width(260)
      }
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Pink)
      .backgroundImageSize(ImageSize.Cover)
  }
}

实战案例6:下拉loading

https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-AnimateRefresh

@State scaleX:number = 1

Row() {
	Image().scale({ x: this.scaleX,  y: this.scaleY })
	  .animation({
	  延迟1s/2s/3s
      duration: 2000,						//动画持续时间(ms),默认为1000
      curve: Curve.EaseOut,			//动画速率曲线(参数值详见官方文档),默认为匀速linear
      iterations: ,						//动画播放次数,-1是无限次,默认为1次
      playMode: PlayMode.Normal	//动画播放模式,默认播放完成后重头开始播放
    })
	Image()
	Image()
	Image()
	Image()
}

下拉也可以是页面打开

this.scaleX = 1.3 

鸿蒙开发者班级

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