我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

先问你个扎心的问题:你做的 ArkUI 动画,是“动了”,还是“动人”?很多同学第一次上手动画,能把元素从 A 移到 B、能把透明度从 0 到 1,就觉得万事大吉。可一上真场景——卡顿、延迟、节拍不对、手势不跟手、交互动线不统一……用户看着就是“别扭”。别急,本文就带你系统拆解 ArkUI 动画系统:从显式/隐式动画的本体哲学,到AnimationController的“指挥棒”,再到Curve(曲线)与插值器如何“点睛”。全程都用可以直接拿去跑的 ArkTS 代码,配上我踩坑踩出来的心得体感,保证你读完能把动画做得“又丝滑又聪明”。😎

适读对象:已经在写 ArkUI(ArkTS)的同学,想把动画从“能跑”升级到“好用、好看、好维护”。

导航(按大纲来)

  • 显式动画 / 隐式动画:两种建模方式怎么选,什么时候各显神通?
  • 动画控制器(AnimationController):如何暂停、恢复、反向、跳转进度、和手势绑定?
  • 曲线与插值器(Curve):为什么“快—慢—停”才有生命力?自定义贝塞尔、弹簧曲线怎么玩?

一、显式动画 vs 隐式动画:一静一动,动静皆宜

1.1 隐式动画:写状态就给你“顺手带个动效”

先用一句人话总结隐式动画你只管改状态,ArkUI 帮你把变更用动画“抹平”。
在 ArkUI 中,隐式动画通常有两种常见写法:

  • 组件级修饰器 .animation({ ... }):给某个组件绑定一条“默认动画规则”;只要这个组件的可动画属性(如 opacityrotatescaletranslatewidth/height 等)发生变化,就按规则“动”。
  • animateTo(options, () => { ... }):把一批状态变更包起来,一次性“平滑过渡”。

什么时候用隐式?交互简单、只需要“随状态一起动”的场景:比如按钮按下的缩放、卡片展开、列表项淡入淡出、细微的位移修饰等。

示例:给卡片一个“呼吸感”

@Component
export struct BreathingCardDemo {
  @State private on = false;

  build() {
    Column() {
      // 卡片本体
      Column() {
        Text(this.on ? 'I am alive ✨' : 'Tap me!')
          .fontSize(18)
          .fontColor('#ffffff')
          .padding(16)
      }
      .width(220)
      .height(120)
      .borderRadius(16)
      .backgroundColor(this.on ? '#4B7BEC' : '#9B9B9B')
      // 注意:给组件绑一个隐式动画规则
      .animation({
        duration: 320,
        curve: Curve.EaseInOut
      })
      // 状态变化时,以下属性会被上述规则“抹平过渡”
      .scale(this.on ? 1.08 : 1.0)
      .opacity(this.on ? 1.0 : 0.85)
      .onClick(() => this.on = !this.on)

      // 控制按钮
      Button(this.on ? '收一收' : '展开呼吸')
        .margin({ top: 20 })
        .onClick(() => this.on = !this.on)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#121212')
  }
}

心得:单点轻动效走隐式最省脑,代码“语义洁净”,不会满屏都是控制器对象。

1.2 显式动画:你来掌舵,我只负责推帆

显式动画强调“我明确地告诉你什么时候开始、做什么、怎么做”。典型接口是 animateTo(),也可以配合动画控制器实现更细粒度控制(第二章展开)。

示例:一键展开/收起,多个状态联动

@Component
export struct ExpandPanelDemo {
  @State private expanded = false;
  @State private contentOpacity: number = 0.0;
  @State private contentHeight: number = 0;

  private targetHeight = 140;

  toggle() {
    // 使用 animateTo 将一批状态变更合在一段动画里
    animateTo({
      duration: 360,
      curve: Curve.EaseInOut,
      onFinish: () => console.info('[ExpandPanelDemo] transition done')
    }, () => {
      this.expanded = !this.expanded
      this.contentOpacity = this.expanded ? 1.0 : 0.0
      this.contentHeight  = this.expanded ? this.targetHeight : 0
    })
  }

  build() {
    Column() {
      Row() {
        Text('订单详情')
          .fontSize(20).fontWeight(FontWeight.Bold)
        Blank()
        Button(this.expanded ? '收起' : '展开')
          .onClick(() => this.toggle())
      }
      .height(48).padding({ left: 16, right: 16 }).alignItems(VerticalAlign.Center)

      // 内容区:高度与透明度在 animateTo 中联动
      Column() {
        Text('· 商品名称:ArkUI Phone')
        Text('· 价格:¥3999')
        Text('· 配送:当日达(首发专享)')
      }
      .height(this.contentHeight)
      .opacity(this.contentOpacity)
      .padding(16)
      .clip(true) // 重要:收起时不露馅
      .backgroundColor('#202020')
      .borderRadius(12)
      .margin({ top: 8 })
      .animation({ duration: 360, curve: Curve.EaseInOut }) // 也可不写,靠 animateTo 就行;写了会更“顺”
    }
    .padding(16)
    .backgroundColor('#121212')
    .width('100%').height('100%')
  }
}

心得:多属性联动需要 onFinish 回调需要一次性控制多个组件时,animateTo 非常趁手。它像个“事务”(transaction):把变化集中交给动画系统,一起“平滑提交”。

二、AnimationController:让动画听你的指挥棒

隐式/显式能解决大半需求,但当你要做可暂停、可反向、可拖拽、可预览,甚至多段串联的复杂动画,控制器(AnimationController)就登场了。简单说,它是把动画时间轴抽象成一个对象,你可以:

  • play / pause / resume / stop / finish:控制生命周期
  • reverse:反向播放(常见于返回动效、切换动线)
  • setProgress / progress:把手势位移 → 动画进度,做“跟手”
  • onFrame / onFinish:订阅帧事件,用于同步其他状态或触发后续逻辑

不同版本模板 API 略有差异,下面示例采用通用写法语义;若你的工程模板方法名略有不同(如 forward()/backward()),替换即可,思路完全一致。

2.1 基础用法:从 0 到 1 的时间轴

@Component
export struct ControllerBasics {
  private controller: AnimationController = new AnimationController({
    duration: 500,
    curve: Curve.EaseInOut
  })

  @State private x = 0
  @State private opacity = 1.0

  aboutToAppear() {
    // 每一帧回调(可选):把进度同步成你想要的数值
    this.controller.onFrame((progress: number) => {
      // progress 通常是 0~1
      this.x = 40 * progress
      this.opacity = 1.0 - 0.3 * progress
    })
    this.controller.onFinish(() => console.info('[ControllerBasics] finished'))
  }

  build() {
    Column() {
      Row() {
        Button('Play').onClick(() => this.controller.play())
        Button('Pause').margin({ left: 8 }).onClick(() => this.controller.pause())
        Button('Resume').margin({ left: 8 }).onClick(() => this.controller.resume())
        Button('Reverse').margin({ left: 8 }).onClick(() => this.controller.reverse())
      }
      .margin(16)

      // 被控制的“盒子”
      Row() {
        Text('🐦')
          .fontSize(22)
      }
      .width(100).height(56)
      .backgroundColor('#2D7D46')
      .borderRadius(16)
      .translate({ x: this.x, y: 0 })
      .opacity(this.opacity)
    }
    .width('100%').height('100%')
    .backgroundColor('#121212')
  }
}

要点:把“UI属性 = f(progress)” 这种关系写清楚,你的动画就变成了“可编排的时间函数”。控制器只是负责推进 progress

2.2 跟手动画:滑多少,动多少(手势 ←→ 进度)

真正让动效“活起来”的,是它和手势的配合。比如一个“底部抽屉”需要跟着手指拖动,并在松手时按速度/距离决定“展开还是收起”。

@Component
export struct BottomSheetDemo {
  private controller: AnimationController = new AnimationController({ duration: 360, curve: Curve.EaseOut })
  @State private progress: number = 0 // 0=收起, 1=完全展开
  private minY = 0
  private maxY = 320 // sheet 最大拖动高度

  aboutToAppear() {
    this.controller.onFrame((p: number) => this.progress = p)
  }

  private toY(p: number) { return this.maxY * (1 - p) } // p=1 → y=0(完全展开)

  build() {
    Stack() {
      // 模拟内容区
      Column() { Text('Content').fontSize(20).fontColor('#fff') }
        .width('100%').height('100%').backgroundColor('#1E1E1E')

      // 底部抽屉
      Column() {
        Row() { Text('Drag me ↑').fontSize(16) }.height(56).alignItems(VerticalAlign.Center)
        // ... 放更多内容
      }
      .width('100%')
      .height(this.maxY + 80)
      .backgroundColor('#2B2B2B')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .translate({ x: 0, y: this.toY(this.progress) })
      .gesture(
        PanGesture({ direction: PanDirection.Vertical, distance: 1 })
          .onActionStart(() => this.controller.pause())
          .onActionUpdate((event) => {
            // 根据手指位移更新进度
            const dy = -event.offsetY // 向上为正
            const travelled = (this.progress * this.maxY) + dy
            const next = Math.min(Math.max(travelled / this.maxY, 0), 1)
            this.progress = next
            this.controller.setProgress(next) // 关键:用控制器写回进度
          })
          .onActionEnd((event) => {
            // 松手后根据速度/位置决定去向
            const goingUp = event.velocityY < 0
            const shouldOpen = goingUp ? true : (this.progress > 0.5)
            if (shouldOpen) this.controller.play() // 朝1播放
            else this.controller.reverse() // 朝0播放
          })
      )
    }
    .width('100%').height('100%')
  }
}

关键点:

  • 进度是“一等公民”,UI 属性基于进度派生。
  • 手势期间 pause() 冻住时间轴,用 setProgress() 直接推进;松手后 play()/reverse() 让动画自己“收尾”。
  • 这套套路也非常适合轮播图、卡片叠层、转场过度等场景。

2.3 串联与并联:一段接一段、一起走一起停

实际效果往往需要多段动画组装:比如“淡入 + 位移 + 旋转”先后执行,或“头像和昵称同时进场”。

并联(together):多个属性同时基于同一 controller

@Component
export struct TogetherDemo {
  private c = new AnimationController({ duration: 460, curve: Curve.FastOutSlowIn })
  @State private p = 0

  aboutToAppear() { this.c.onFrame((x) => this.p = x) }

  build() {
    Column() {
      Button('Play').onClick(() => this.c.play())

      Row() {
        // 同一个 progress,多个属性并联
        Image($r('app.media.avatar'))
          .width(80).height(80).borderRadius(40)
          .opacity(this.p)
          .scale(0.8 + 0.2 * this.p)
          .rotate({ angle: 12 * (1 - this.p) })
          .translate({ x: -40 * (1 - this.p), y: 0 })

        Text('Hello ArkUI')
          .fontSize(18).fontColor('#fff')
          .opacity(this.p)
          .translate({ x: 12 * (1 - this.p), y: 0 })
      }
      .margin(20)
    }
    .backgroundColor('#121212')
    .width('100%').height('100%')
  }
}

串联(sequence):分段推进
通常做法是:第一段 onFinish 里接第二段,或者使用多个控制器按顺序 play()。也可以自己封装一个**“时间切片”**的 Sequence,把全球时间进度映射到局部 0~1,再各自驱动。

// 一个简易 Sequence 工具,把全局 progress 分片给多段
function sliceProgress(global: number, start: number, end: number) {
  if (global <= start) return 0
  if (global >= end) return 1
  return (global - start) / (end - start)
}

@Component
export struct SequenceDemo {
  private c = new AnimationController({ duration: 800, curve: Curve.EaseInOut })
  @State private p = 0

  aboutToAppear() { this.c.onFrame(x => this.p = x) }

  build() {
    Column() {
      Button('Play').onClick(() => this.c.play())
      // 段1:0~0.4 透明度
      const p1 = sliceProgress(this.p, 0.0, 0.4)
      // 段2:0.4~0.7 位移
      const p2 = sliceProgress(this.p, 0.4, 0.7)
      // 段3:0.7~1.0 旋转
      const p3 = sliceProgress(this.p, 0.7, 1.0)

      Image($r('app.media.logo'))
        .width(96).height(96).borderRadius(12)
        .opacity(p1)
        .translate({ x: 0, y: (1 - p2) * 24 })
        .rotate({ angle: p3 * 180 })
    }
    .padding(24)
    .backgroundColor('#121212')
    .width('100%').height('100%')
  }
}

亮点:串并联统一到“进度映射”这个抽象,你的动画就具备“可编排性”和“可维护性”。写复杂动效不再靠复制粘贴时间参数。

三、Curve(曲线)与插值器:让动画有“呼吸和肌肉”

“曲线”是动画气质的灵魂。同一个位移 40px,用 Linear 和用 EaseInOut,给人的感觉完全不是一个东西。ArkUI 的 Curve 提供常用枚举(如 LinearEaseEaseInEaseOutEaseInOutFastOutSlowIn 等),同时支持自定义贝塞尔。此外,很多同学喜欢的“弹簧感”,也可以通过参数化曲线/插值器来实现。

3.1 你真的了解常见曲线吗?

  • Linear:匀速,机械。适合加载条闪烁这类“不要情绪”的地方。
  • EaseIn:先慢后快。适合入场(像“加速进入画面”)。
  • EaseOut:先快后慢。适合离场(像“刹车停靠”)。
  • EaseInOut:慢→快→慢。通用好用,大多数 UI 过渡都挺优雅。
  • FastOutSlowIn:更“急促地出发、更柔和地停下”,有鲜明张力,适配卡片升起/按钮按下回弹等。

对比示例:同一段位移,不同曲线体验

@Component
export struct CurveCompare {
  @State private xLinear = 0
  @State private xEase = 0
  @State private xFosi = 0  // FastOutSlowIn

  play(curve: Curve, setter: (v:number) => void) {
    const c = new AnimationController({ duration: 600, curve })
    c.onFrame((p) => setter(p * 180))
    c.play()
  }

  build() {
    Column() {
      Row() {
        Button('Linear').onClick(() => this.play(Curve.Linear, v => this.xLinear = v))
        Button('EaseInOut').margin({ left: 8 }).onClick(() => this.play(Curve.EaseInOut, v => this.xEase = v))
        Button('FastOutSlowIn').margin({ left: 8 }).onClick(() => this.play(Curve.FastOutSlowIn, v => this.xFosi = v))
      }.margin(16)

      // 三条小条同时比较
      Column() {
        Row().width(200).height(2).backgroundColor('#333')
        Row().width(12).height(12).borderRadius(6)
          .backgroundColor('#5AD') .translate({ x: this.xLinear, y: -5 })

        Row().width(200).height(2).backgroundColor('#333').margin({ top: 22 })
        Row().width(12).height(12).borderRadius(6)
          .backgroundColor('#7ED') .translate({ x: this.xEase, y: 17 })

        Row().width(200).height(2).backgroundColor('#333').margin({ top: 22 })
        Row().width(12).height(12).borderRadius(6)
          .backgroundColor('#9F8') .translate({ x: this.xFosi, y: 39 })
      }
    }
    .width('100%').height('100%').backgroundColor('#121212')
  }
}

看着它们跑一遍,你就会对“曲线带来的气质变化”更敏感。

3.2 自定义贝塞尔:把美术同学的“动效规范”落地到代码

设计稿里常出现这样的描述:“cubic-bezier(0.2, 0.8, 0.2, 1)”。你完全可以在 ArkUI 里还原:

const EaseBrand = Curve.cubicBezier(0.2, 0.8, 0.2, 1.0)

@Component
export struct BrandMotionDemo {
  private c = new AnimationController({ duration: 520, curve: EaseBrand })
  @State private p = 0

  aboutToAppear() { this.c.onFrame(x => this.p = x) }

  build() {
    Column() {
      Button('Play').onClick(() => this.c.play())
      Text('Brand Motion').fontSize(22).fontColor('#fff')
        .opacity(this.p)
        .translate({ x: 0, y: (1 - this.p) * 16 })
    }.padding(24)
    .backgroundColor('#121212')
    .width('100%').height('100%')
  }
}

小技巧:把品牌曲线抽象成常量,形成“动效规范”。产品动效统一、体验“像一家人”。

3.3 春天里最会“弹”的那位:弹簧/阻尼曲线(Spring)

很多时候你要的是物理感:比如按钮按下有个轻微回弹,卡片被拉出后有弹性的回归。这时可以用弹簧曲线(不同模板名称可能略有差异,通常在 Curve 下提供 spring 参数 / 或有诸如 SpringSpringMotion 的接口),核心参数是刚度(stiffness)阻尼(damping)

下面用一种常见写法:自行实现一个简化版弹簧插值(实战可换成内置 Spring 接口/曲线)。

// 一个简单的弹簧插值器:给定 t∈[0,1],返回带弹性的过渡
function springLerp(t: number, damping = 10, stiffness = 180): number {
  // 近似:阻尼振动解(可替换成项目内置的 Spring 曲线)
  // 这里只为了展示效果,公式做轻量化处理
  const w0 = Math.sqrt(stiffness)
  const zeta = damping / (2 * Math.sqrt(stiffness))
  if (zeta < 1) {
    const wd = w0 * Math.sqrt(1 - zeta * zeta)
    return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + (zeta / Math.sqrt(1 - zeta * zeta)) * Math.sin(wd * t))
  } else {
    // 过阻尼近似
    return 1 - Math.exp(-w0 * t)
  }
}

@Component
export struct SpringFeelDemo {
  private c = new AnimationController({ duration: 700, curve: Curve.Linear })
  @State private p = 0

  aboutToAppear() { this.c.onFrame(x => this.p = x) }

  build() {
    const sp = springLerp(this.p, 12, 180) // 弹簧后的“进度”
    Column() {
      Button('Boing!').onClick(() => this.c.play())
      Row()
        .width(100).height(56).borderRadius(28)
        .backgroundColor('#3E9')
        .scale(0.8 + 0.3 * sp)
        .shadow({ radius: 12 * sp, color: '#3E9' })
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width('100%').height('100%').backgroundColor('#121212')
  }
}

真实项目里优先用内置 Spring 曲线 API(如果你的模板版本提供),因为它对帧率、数值稳定性更友好。自己写则记得限制 overshoot,避免“弹过头挡住东西”。

四、把“动画工程化”:规范、解耦、复用、可调试

好看的动画不少见,好维护的动画才稀缺。下面给你一套“工程化小抄”。

4.1 抽象“可复用动效组件”

把常见动效抽象成组件或函数,让调用方只关心“想要的感觉”,而不是每次从头配时间、曲线。

例:通用“淡入上移”动效

type MotionPreset = {
  duration?: number
  curve?: Curve
  distance?: number
}

function fadeUp(controller: AnimationController, p: number, opt?: MotionPreset) {
  const d = opt?.distance ?? 12
  const opacity = p
  const y = (1 - p) * d
  return { opacity, y }
}

@Component
export struct MotionPresetDemo {
  private c = new AnimationController({ duration: 420, curve: Curve.EaseInOut })
  @State private p = 0

  aboutToAppear() { this.c.onFrame(x => this.p = x) }

  build() {
    const m = fadeUp(this.c, this.p, { distance: 18 })
    Column() {
      Button('Play').onClick(() => this.c.play())
      Text('Preset Motion')
        .fontSize(20).fontColor('#fff')
        .opacity(m.opacity)
        .translate({ x: 0, y: m.y })
    }.padding(24).backgroundColor('#121212').width('100%').height('100%')
  }
}

收益:形成团队动效词汇表。当你说“这个地方给我 fadeUp,300ms,EaseInOut”,大家都懂,不用来回调参。

4.2 统一时间与曲线:别让页面变“菜市场”

  • 规范化:把常用 duration 定成变量,比如 T.Fast=160, T.Mid=320, T.Slow=560;曲线也命名,如 Ease.Brand, Ease.Overlay
  • 分层:交互类(按钮反馈)用快节奏,布局类(页面进出)用中等,沉浸式(全屏过渡)允许稍慢。
  • 一致性:同类场景统一曲线与时长,用户的肌肉记忆会点赞。

4.3 调试与性能:别用动效“装饰卡顿”

  • 避免过度动画:列表长项每个都开 300ms 淡入?慎重。首屏要紧,优先关键节点
  • 合成层友好:尽量用不触发布局回流的属性(opacity、transform 族),减少 width/height 硬变更。
  • 分段加载:复杂页面先完成骨架,再让“装饰型动效”慢半拍上场,感知更流畅
  • 低端机兜底:给动效挂“功耗档位”(例如全局开关或时长缩短),在性能吃紧的设备走轻量版。

五、真实案例:从按钮按压到卡片分层出场

5.1 按钮按压反馈(隐式动画 + 弹簧味)

@Component
export struct PressButton {
  @State private down = false

  build() {
    Row()
      .width(160).height(46)
      .borderRadius(23)
      .backgroundColor('#3A85FF')
      .scale(this.down ? 0.95 : 1)
      .shadow({ radius: this.down ? 6 : 12, color: '#3A85FF' })
      .animation({ duration: 120, curve: Curve.FastOutSlowIn })
      .onTouch((e) => {
        if (e.type === TouchType.Down) this.down = true
        if (e.type === TouchType.Up || e.type === TouchType.Cancel) this.down = false
      })
      .justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center)
      .onClick(() => console.info('clicked'))
      .bindContent(
        Text('加入购物车').fontSize(16).fontColor('#fff')
      )
  }
}

小而美:短时长 + 快出慢收,就是舒服。

5.2 卡片分层进场(Sequence:标题→图片→按钮)

@Component
export struct CardStaggerIn {
  private c = new AnimationController({ duration: 900, curve: Curve.EaseInOut })
  @State private p = 0

  aboutToAppear() { this.c.onFrame(x => this.p = x) }

  build() {
    const titleP  = sliceProgress(this.p, 0.00, 0.35)
    const imageP  = sliceProgress(this.p, 0.25, 0.70)
    const actionP = sliceProgress(this.p, 0.60, 1.00)

    Column() {
      Button('进入').onClick(() => this.c.play())

      Column() {
        Text('ArkUI 动画系统')
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#fff')
          .opacity(titleP)
          .translate({ x: 0, y: (1 - titleP) * 10 })

        Image($r('app.media.banner'))
          .width('100%').height(160).borderRadius(12).margin({ top: 12 })
          .opacity(imageP)
          .scale(0.9 + 0.1 * imageP)

        Row() {
          Button('开始体验').onClick(() => console.info('start'))
            .opacity(actionP).translate({ x: 0, y: (1 - actionP) * 12 })
        }.margin({ top: 18 })
      }
      .padding(16)
      .backgroundColor('#232323').borderRadius(16).width('100%')
    }
    .padding(16).backgroundColor('#121212').width('100%').height('100%')
  }
}

一眼就“叙事化”了:标题告诉你我是谁,图片告诉你我做什么,按钮告诉你来不来

六、FAQ:你可能卡的那些点

Q:animateTo.animation() 同时写,会不会“打架”?
A:不会“打架”,但要理解二者关系。.animation()组件级默认,而 animateTo包裹一批状态改变。在实际项目中,要么统一走 animateTo 做一次性过渡,要么.animation() 处理碎片化微动效。同一属性反复切换来源时,注意时长和曲线保持一致。

Q:控制器的 onFrame 会不会太频繁?
A:它就是逐帧,别滥用。推荐只在需要“多属性同步”的地方用;简单场景交给隐式/显式动画即可。记得解绑或关闭不需要的监听,避免长生命周期里“忘关水龙头”。

Q:手势动画为什么偶尔“跟不齐手”?
A:通常是把布局类属性也挂上了跟手(比如频繁改 height),导致重排。优先用 translate/scale/opacity,并在关键时刻 clip 裁切,让视觉正确但布局不抖。

Q:为什么我用 Linear 看起来“假”?
A:人的感知对加速度更敏感。线性匀速没有“力学”味道,显得机械。EaseOut 用在结束落座、EaseIn 用在起步,EaseInOut 通用。有条件就用品牌贝塞尔弹簧做润色。

七、Checklist:上线前动效质检表

  • 统一时间/曲线:是否使用动效规范里的预设?
  • 关键路径轻量:首屏、切页是否避免堆叠过多动画?
  • 低性能兜底:是否支持全局开关/降级?
  • 无障碍考虑:是否尊重“减少动态效果(Reduce Motion)”的系统偏好?
  • 与手势一致:拖拽类动效是否跟手、松手回弹是否合理?
  • 不破布局:优先 transform/opacity,必要时 clip,避免频繁回流。
  • 可测试:关键动线是否有演示页或 story,方便回归?

八、总结:动画不是“装饰”,它是“语言”

把动画理解成“装饰”,就容易走向“哪里都想动一下”的歧途;把动画当作语言,你会思考节奏、重音、语法

  • 进入时要铺陈(EaseIn),
  • 停靠时要落座(EaseOut),
  • 重要信息要分层叙事(Sequence/Stagger),
  • 与手指要同频共振(Controller + setProgress),
  • 品牌要统一口音(预设时长 + 曲线规范)。

当这些成为你的下意识选择,你做出来的界面,就不是简单的“有动画”,而是“有呼吸、有情绪、有态度”。

现在就挑一个页面:把开场的卡片进场改成 Sequence,把按钮反馈改成“快出慢收”,把抽屉改成“手势跟手+松手收尾”。你会惊讶:同一套 UI,气质立马不一样。

九、附录:常用 API 速查卡(按本文语义)

不同工程模板的具体签名可能略有差异,以下为通用语义与常用写法,迁移时替换为你项目的实际导入路径/枚举即可。

  • 隐式动画(组件修饰)
    .animation({ duration, curve, delay?, iterations?, playMode?, onFinish? })
    对当前组件的可动画属性(如 translate/scale/rotate/opacity)的变化生效。

  • 显式动画(事务式)
    animateTo(options, () => { /* 这里批量改状态 */ })
    optionsduration, curve, delay?, onFinish? 等。

  • 动画控制器
    new AnimationController({ duration, curve })
    方法:play() / pause() / resume() / reverse() / finish() / setProgress(v:0~1)
    事件:onFrame(cb: (progress)=>void), onFinish(cb)

  • 曲线(Curve)
    Curve.Linear / Ease / EaseIn / EaseOut / EaseInOut / FastOutSlowIn / ...
    自定义贝塞尔:Curve.cubicBezier(x1, y1, x2, y2)
    (如果你的模板支持)弹簧:Curve.Spring(...)SpringMotion(...)

  • 手势配合
    PanGesture({...}).onActionStart/Update/End
    常见逻辑:pause()setProgress()(跟手)→ play()/reverse()(收尾)。

(未完待续)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐