鸿蒙APP开发:如果你想在鸿蒙App里做属性动画,@ohos.animator怎么用

如果你需要一个视力训练工具,可以去鸿蒙应用市场搜一下**「明眸集」**,下载下来体验体验。眼球跟随运动、焦点切换训练、视野扩展练习------这些训练都需要流畅的属性动画来引导视线移动。体验完了再回来看这篇文章,你会更清楚属性动画的实现方式。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易------都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • 属性动画 :Web里CSS transition直接声明"哪些属性需要动画";鸿蒙里animateTo也是声明式的,但它是把状态变更包裹在回调函数里,框架自动识别哪些属性变了。
  • 动画控制 :Web的CSS动画控制粒度有限;鸿蒙的@ohos.animator提供更精细的帧级控制。
  • 组合动画 :Web可以用animation同时应用多个动画;鸿蒙里可以在一个animatorframe回调里同时更新多个属性。

但别担心,核心思想是一样的:都是基于时间的状态驱动。你之前积累的前端经验,在鸿蒙里依然是你的核心竞争力。


这篇文章聊什么

明眸集这个App需要做多种属性动画:

  1. 眼球跟随:一个点从屏幕一端移动到另一端,引导用户视线跟随
  2. 焦点切换:多个点依次出现和消失,训练焦点切换能力
  3. 缩放动画:物体从小变大再变小,训练调节能力

这些动画的核心都是属性动画------控制物体的位置、大小、透明度等属性随时间变化。


第一步:理解两种动画方式

鸿蒙里有两种做属性动画的方式:

方式一:animateTo(声明式)

typescript 复制代码
@Entry
@Component
struct AnimateToDemo {
  @State offsetX: number = 0

  build() {
    Column() {
      Circle()
        .width(50)
        .height(50)
        .fill('#10B981')
        .translate({ x: this.offsetX })

      Button('移动')
        .onClick(() => {
          // animateTo:把状态变更包裹在回调里
          animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
            this.offsetX = this.offsetX === 0 ? 200 : 0
          })
        })
    }
  }
}

animateTo的特点:

  • 声明式:你只管改状态,框架自动做过渡动画
  • 简单:不需要手动管理动画实例
  • 适合:简单的属性过渡,比如位置、大小、透明度

方式二:@ohos.animator(命令式)

typescript 复制代码
import { animator, AnimatorResult } from '@ohos.animator'

@Entry
@Component
struct AnimatorDemo {
  @State offsetX: number = 0
  private anim: AnimatorResult | null = null

  aboutToDisappear() {
    if (this.anim) {
      this.anim.cancel()
      this.anim = null
    }
  }

  startAnimation() {
    this.anim = animator.create({
      duration: 2000,
      iterations: -1,
      direction: 'Alternate',
      curve: 'EaseInOut',
      frame: (value: number) => {
        // value从0到1,每2秒一个周期
        this.offsetX = value * 200
      }
    })
    this.anim.play()
  }

  build() {
    Column() {
      Circle()
        .width(50)
        .height(50)
        .fill('#10B981')
        .translate({ x: this.offsetX })

      Button('开始')
        .onClick(() => this.startAnimation())
    }
  }
}

animator的特点:

  • 命令式:你在frame回调里手动计算每一帧的状态
  • 灵活:可以做复杂的非线性动画、多属性联动
  • 适合:需要精细控制的动画,比如循环、交替、多阶段

第二步:animateTo详解

animateTo是鸿蒙推荐的属性动画方式,用法类似CSS的transition

typescript 复制代码
animateTo({
  duration: 500,           // 动画时长(毫秒)
  curve: 'EaseInOut',      // 缓动曲线
  delay: 0,                // 延迟(毫秒)
  iterations: 1,           // 播放次数
  playMode: PlayMode.Normal, // 播放模式
  onFinish: () => {
    // 动画完成回调
  }
}, () => {
  // 状态变更回调
  this.offsetX = 200
  this.scale = 1.5
  this.opacity = 0.5
})

在这个回调里修改的任何@State属性,只要前后值不同,都会自动产生过渡动画。

typescript 复制代码
@Entry
@Component
struct AnimateToFullDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0
  @State scale: number = 1
  @State opacity: number = 1
  @State rotation: number = 0
  @State color: string = '#10B981'

  moveRight() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.offsetX = 200
    })
  }

  moveDown() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.offsetY = 200
    })
  }

  scaleUp() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.scale = 2
    })
  }

  fadeOut() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.opacity = 0.3
    })
  }

  rotate() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.rotation = 90
    })
  }

  changeColor() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.color = '#EF4444'
    })
  }

  reset() {
    animateTo({ duration: 500, curve: 'EaseInOut' }, () => {
      this.offsetX = 0
      this.offsetY = 0
      this.scale = 1
      this.opacity = 1
      this.rotation = 0
      this.color = '#10B981'
    })
  }

  build() {
    Column() {
      // 动画对象
      Circle()
        .width(80)
        .height(80)
        .fill(this.color)
        .opacity(this.opacity)
        .scale({ x: this.scale, y: this.scale })
        .translate({ x: this.offsetX, y: this.offsetY })
        .rotate({ angle: this.rotation })

      // 控制按钮
      Column() {
        Row() {
          Button('右移')
            .onClick(() => this.moveRight())
          Button('下移')
            .onClick(() => this.moveDown())
            .margin({ left: 8 })
        }

        Row() {
          Button('放大')
            .onClick(() => this.scaleUp())
          Button('淡出')
            .onClick(() => this.fadeOut())
            .margin({ left: 8 })
        }
        .margin({ top: 8 })

        Row() {
          Button('旋转')
            .onClick(() => this.rotate())
          Button('变色')
            .onClick(() => this.changeColor())
            .margin({ left: 8 })
        }
        .margin({ top: 8 })

        Button('重置')
          .onClick(() => this.reset())
          .margin({ top: 8 })
      }
      .margin({ top: 40 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#111827')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }
}

第三步:用animateTo做眼球跟随动画

明眸集的核心训练之一是眼球跟随------一个点从屏幕一端移动到另一端,引导用户视线跟随。

typescript 复制代码
@Entry
@Component
struct EyeFollowPage {
  @State dotX: number = 0
  @State dotY: number = 0
  @State isRunning: boolean = false

  private screenWidth: number = 360
  private screenHeight: number = 600

  startFollow() {
    this.isRunning = true
    this.moveRight()
  }

  moveRight() {
    animateTo({
      duration: 2000,
      curve: 'EaseInOut',
      onFinish: () => {
        if (this.isRunning) this.moveLeft()
      }
    }, () => {
      this.dotX = this.screenWidth - 50
    })
  }

  moveLeft() {
    animateTo({
      duration: 2000,
      curve: 'EaseInOut',
      onFinish: () => {
        if (this.isRunning) this.moveDown()
      }
    }, () => {
      this.dotX = 0
    })
  }

  moveDown() {
    animateTo({
      duration: 1000,
      curve: 'EaseInOut',
      onFinish: () => {
        if (this.isRunning) this.moveRight()
      }
    }, () => {
      this.dotY = this.dotY + 100 > this.screenHeight - 50 ? 0 : this.dotY + 100
    })
  }

  stopFollow() {
    this.isRunning = false
    animateTo({ duration: 300 }, () => {
      this.dotX = 0
      this.dotY = 0
    })
  }

  build() {
    Stack() {
      // 跟随点
      Circle()
        .width(40)
        .height(40)
        .fill('#10B981')
        .translate({ x: this.dotX, y: this.dotY })
        .shadow({ radius: 20, color: 'rgba(16, 185, 129, 0.5)' })

      // 控制按钮
      Column() {
        Button(this.isRunning ? '停止' : '开始')
          .onClick(() => {
            if (this.isRunning) {
              this.stopFollow()
            } else {
              this.startFollow()
            }
          })
      }
      .width('100%')
      .position({ x: 0, y: this.screenHeight - 80 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#111827')
  }
}

这个动画的逻辑:

  1. 点从左向右移动(2秒)
  2. 到达右端后从右向左移动(2秒)
  3. 到达左端后向下移动一行(1秒)
  4. 重复以上步骤

animateToonFinish回调串联多个动画。


第四步:用animator做更复杂的动画

如果动画逻辑更复杂,比如需要根据时间计算非线性运动,用animator更合适:

typescript 复制代码
@Entry
@Component
struct ComplexAnimationPage {
  @State dotX: number = 0
  @State dotY: number = 0
  @State dotScale: number = 1
  @State trail: Array<{ x: number, y: number }> = []

  private anim: AnimatorResult | null = null

  aboutToDisappear() {
    if (this.anim) {
      this.anim.cancel()
      this.anim = null
    }
  }

  startAnimation() {
    this.anim = animator.create({
      duration: 4000,
      iterations: -1,
      curve: 'Linear',
      frame: (value: number) => {
        // 螺旋运动
        const angle = value * Math.PI * 4  // 两圈
        const radius = 50 + value * 100     // 半径逐渐增大

        this.dotX = 180 + radius * Math.cos(angle)
        this.dotY = 300 + radius * Math.sin(angle)

        // 呼吸效果
        this.dotScale = 1 + 0.3 * Math.sin(value * Math.PI * 8)

        // 记录轨迹
        if (this.trail.length > 50) {
          this.trail.shift()
        }
        this.trail.push({ x: this.dotX, y: this.dotY })
      }
    })
    this.anim.play()
  }

  build() {
    Stack() {
      // 轨迹
      ForEach(this.trail, (point: { x: number, y: number }, index: number) => {
        Circle()
          .width(4)
          .height(4)
          .fill('#10B981')
          .opacity(index / this.trail.length * 0.5)
          .translate({ x: point.x - 2, y: point.y - 2 })
      })

      // 主点
      Circle()
        .width(30 * this.dotScale)
        .height(30 * this.dotScale)
        .fill('#10B981')
        .translate({ x: this.dotX - 15, y: this.dotY - 15 })
        .shadow({ radius: 10, color: 'rgba(16, 185, 129, 0.5)' })

      Button('开始')
        .onClick(() => this.startAnimation())
        .position({ x: 130, y: 550 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#111827')
  }
}

这个动画用animatorframe回调里用三角函数计算螺旋运动轨迹,同时驱动位置和大小两个属性。


第五步:animateTo vs animator的选择

场景 推荐方式 原因
简单的属性过渡 animateTo 代码简洁,声明式
循环动画 animator iterations: -1
多属性联动 animator 一个回调控制所有属性
非线性运动 animator 可以用数学函数计算
暂停/恢复 animator 提供pause()/resume()
链式动画 animateTo onFinish回调串联

总结

这篇文章围绕"明眸集"的视力训练需求,讲了鸿蒙里两种属性动画方式:

animateTo(声明式)

  • 把状态变更包裹在回调里,框架自动做过渡动画
  • 适合简单的属性过渡
  • onFinish回调串联多个动画

animator(命令式)

  • frame回调里手动计算每一帧的状态
  • 适合复杂的、需要精细控制的动画
  • 提供play()/pause()/resume()/cancel()控制方法

注意事项

  • animateTo只对回调里修改的状态生效
  • animatorframe回调里修改的状态不会自动做过渡
  • 两种方式可以混用
  • 动画实例必须在aboutToDisappear里取消/释放

下一篇文章我会讲明眸集的路径运动------物体沿指定路径移动的实现。

相关推荐
陈_杨1 小时前
鸿蒙APP开发:篮球App怎么画球场?鸿蒙Canvas绘图实战
前端
colofullove1 小时前
前端工程搭建与用户访问流程设计
前端
广州华水科技2 小时前
如何利用单北斗GNSS系统实现大坝的变形监测?
前端
代码小库2 小时前
【2026前端最新面试题——day10】JavaScript 高频面试题
开发语言·前端·javascript
zzz_23682 小时前
【Spring】面试突击系列(三):Spring Web MVC 深度解析
前端·spring·面试
colofullove2 小时前
小说上传中心与异步处理进度展示设计
前端
Marst Code3 小时前
⚙️ 2026 年推荐技术方案
前端
qq_366086223 小时前
测试接口传参数时,放在Header和Body中后台接收参数的区别
java·开发语言·前端
whatever who cares3 小时前
Vue3中vue文件和composables的分工
前端·javascript·vue.js