鸿蒙 ArkUI 收尾篇:动画体系,animateTo、animation、CustomDialog 三件套让界面「动起来」

写在前面

如果你写 ArkUI 写过交互稍丰富的页面,大概率遇到过这三个场景:

场景一:点「放大」按钮,@State 字段从 1.0 改到 1.5,方块瞬间硬切变大——毫无过渡,用户感觉像被掐了一下。你想加动画,查文档发现 animateTo,但不知道咋挂。

场景二:你给方块挂了 .scale({ x: this.boxScale }),每次改 boxScale 都是硬切。你希望「只要这个绑的字段变,就自动跑 300ms 动画」,不想每次套 animateTo,文档说有 animation 隐式动画但没讲清咋用。

场景三:你写个「确认操作」弹层,用 if (this.showDialog) 硬切显示,出现/消失毫无动画,像突然蹦出来又突然消失。设计说「要像原生弹层那样淡入淡出」,你查到 CustomDialog 但一脸懵。

这是「状态硬切」和「动画过渡」的分水岭。鸿蒙 ArkUI 给的答案是三件套:animateTo 显式动画、animation 隐式动画、CustomDialog 入场动画——前两个管状态变的过渡,第三个管弹层的原生入场。

本文就用一个真机可跑的 demo,把这三件套从「听名字一脸懵」讲到「下个项目直接抄」。代码托管在 AtomGit,文末有链接,真机实拍截图作证。这是 ArkUI 系列第十篇,凑齐十篇收尾。

适合人群:写过 ArkUI、被「状态硬切毫无动画」折磨过的同学。
不适合人群:还在学 @State 的同学——出门左转看我的入门篇。


一、先讲清楚:三件套到底是啥

一句话:animateTo 显式包一段状态改带动画,animation 隐式装组件上自动过渡,CustomDialog 自带原生入场。

装饰器/类 解决什么 怎么用 何时用
animateTo 显式包一段状态改 animateTo({...}, () => { 改状态 }) 要精确控时长/曲线的显式动画
animation 隐式装组件自动过渡 .animation({ duration: 300 }) 状态绑的字段变就自动过渡
CustomDialog 弹层原生入场 CustomDialogController + @Builder 自定义弹层带淡入淡出

新手最容易混淆的决策:「要精确控这段改带动画」用 animateTo,「要绑字段变自动过渡」用 animation,「要弹层带原生入场」用 CustomDialog


二、animateTo:显式包一段状态改带动画

最小例子:

Button('放大').onClick(() => {
  // animateTo 闭合一段状态改,这段改带动画过渡(不套 animateTo 就是硬切)
  animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
    this.boxScale = 1.5
    this.boxColor = '#FF4D4F'
    this.rotation = 15
  })
})

animateTo 两个参数:

  1. AnimateParam:动画参数(duration 时长、curve 曲线、playMode 播放模式、iterations 迭代次数)
  2. event:闭合回调,里面写要带动画的状态改

回调里改的所有状态字段都会带动画过渡——一次改三个字段(scale/color/rotation),三个一起过渡,不需要各自套 animateTo

AnimateParam 常用字段

字段 类型 默认 作用
duration number 1000 动画时长(ms)
curve Curve Linear 过渡曲线(EaseInOut/EaseOut/EaseIn/Spring 等)
playMode PlayMode Normal 播放模式(Normal/Reverse/Alternate)
iterations number 1 迭代次数(-1 = 无限循环)
onFinish callback - 动画完成回调

注:鸿蒙 6.1 API 23 里 animateTo 已 deprecated(建议改 animateTo 的 V2 版 animateToImmediately 或新 API),但 V1 仍可用,本 demo 用 V1 演示。


三、animation:隐式装组件上自动过渡

animateTo 每次改状态都要套一层闭合,繁琐。如果只要某字段变就自动跑动画,用 animation 隐式动画——装在组件上,绑的字段变就自动过渡。

Stack({ alignContent: Alignment.Center }) {
  Text('B').fontSize(36).fontColor('#fff').fontWeight(FontWeight.Bold)
}
.width(80).height(80).backgroundColor('#27AE60').borderRadius(40)
// animation 装在组件上:绑的 boxScale 变就自动跑 300ms 动画
.animation({ duration: 300, curve: Curve.EaseOut })
.scale({ x: this.boxScale, y: this.boxScale })

Button('切换大小').onClick(() => {
  // 改 boxScale 自动触发 animation 装的动画(不用套 animateTo)
  this.boxScale = this.boxScale === 1.0 ? 0.6 : 1.0
})

.animation({...}) 装在组件上,绑定的状态字段变就自动跑动画——不用每次套 animateTo,改字段即可。

animateTo vs animation:啥时候用哪个

维度 animateTo animation
用法 显式闭合一段改 隐式装组件上
触发 animateTo 那一刻 绑字段变自动触发
控粒度 精确控这段改的参数 装一次,所有绑字段变都跑
何时用 要精确控某次改的动画 要绑字段变自动过渡

一句话:要精确控某次改用 animateTo,要绑字段变自动过渡用 animation


四、CustomDialog:自定义弹层带原生入场

CustomDialog 不是属性,是一套弹层机制——CustomDialogController 控制弹层的 open/close,弹层内容用 @Builder 写,自带原生淡入淡出入场动画。

4.1 @Builder 写弹层内容

@Builder
function confirmDialogContent(controller: CustomDialogController, title: string, msg: string) {
  Column({ space: 14 }) {
    Text(title).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#222')
    Text(msg).fontSize(14).fontColor('#555')
    Row({ space: 12 }) {
      Button('取消').backgroundColor('#eee').fontColor('#333').height(38)
        .onClick(() => { controller.close() })
      Button('确认').backgroundColor('#007DFF').fontColor('#fff').height(38)
        .onClick(() => { controller.close() })
    }
    .width('100%').justifyContent(FlexAlign.End)
  }
  .width('80%').padding(20).backgroundColor('#fff').borderRadius(14)
}

弹层内容就是个 @Builder 函数,第一参数接 CustomDialogController(用于内部 close),后面是自定义参数。

4.2 CustomDialogController 控制入场

private dialogController: CustomDialogController = new CustomDialogController({
  builder: confirmDialogContent.bind(this, this, '确认操作', '这是自定义弹层,带原生入场动画'),
  autoCancel: true,                        // 点遮罩取消
  alignment: DialogAlignment.Center,       // 居中显示
  offset: { dx: 0, dy: -20 },              // 微偏移
  customStyle: false,                      // false = 带原生样式包裹
  maskColor: '#66000000'                   // 遮罩色(半透明黑)
})

// 用的时候:
this.dialogController.open()    // 打开(带淡入)
// ...
this.dialogController.close()   // 关闭(带淡出)

CustomDialogControllerOptions 常用字段:

字段 作用
builder 弹层内容 builder(用 .bindthis + 参数)
autoCancel 点遮罩/按返回是否取消
alignment 弹层位置(Center/Top/Bottom)
offset 位置微调
customStyle false = 带原生样式包裹,true = 裸内容
maskColor 遮罩色

注:builder 字段用 Function.bindthis 和参数,ArkTS WARN「Function.bind 不支持」但仍可运行。生产项目可改用 @Builder 方法形式避开 WARN。


五、动手:一个 demo 同台演示三件套

5.1 主页面:三件套就位

@Entry
@Component
struct Index {
  @State boxScale: number = 1.0
  @State boxColor: string = '#007DFF'
  @State rotation: number = 0

  private dialogController: CustomDialogController = new CustomDialogController({
    builder: confirmDialogContent.bind(this, this, '确认操作', '这是自定义弹层,带原生入场动画'),
    autoCancel: true,
    alignment: DialogAlignment.Center,
    offset: { dx: 0, dy: -20 },
    customStyle: false,
    maskColor: '#66000000'
  })

  build() {
    Column({ space: 14 }) {
      Text('动画体系 Demo').fontSize(22).fontWeight(FontWeight.Bold).margin({ top: 16 })

      // ① animateTo 显式动画
      Column({ space: 10 }) {
        Stack({ alignContent: Alignment.Center }) {
          Text('A').fontSize(36).fontColor('#fff').fontWeight(FontWeight.Bold)
        }
        .width(80).height(80).backgroundColor(this.boxColor)
        .borderRadius(20 * this.boxScale)
        .rotate({ angle: this.rotation })
        .scale({ x: this.boxScale, y: this.boxScale })

        Row({ space: 12 }) {
          Button('放大').onClick(() => {
            animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
              this.boxScale = 1.5; this.boxColor = '#FF4D4F'; this.rotation = 15
            })
          })
          Button('还原').onClick(() => {
            animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
              this.boxScale = 1.0; this.boxColor = '#007DFF'; this.rotation = 0
            })
          })
        }
      }

      // ② animation 隐式动画
      Column({ space: 10 }) {
        Stack({ alignContent: Alignment.Center }) {
          Text('B').fontSize(36).fontColor('#fff').fontWeight(FontWeight.Bold)
        }
        .width(80).height(80).backgroundColor('#27AE60').borderRadius(40)
        .animation({ duration: 300, curve: Curve.EaseOut })
        .scale({ x: this.boxScale, y: this.boxScale })

        Button('切换大小').onClick(() => {
          this.boxScale = this.boxScale === 1.0 ? 0.6 : 1.0
        })
      }

      // ③ CustomDialog 自定义弹层
      Column({ space: 10 }) {
        Button('打开确认弹层')
          .onClick(() => { this.dialogController.open() })
      }
    }
    .padding(16).backgroundColor('#F5F6F8').height('100%').width('100%')
  }
}

六、真机实拍:三件套跑起来长这样

我把这个 demo 装到真机上跑(鸿蒙 6.1.1.125, API 24),下面这张是真机实拍,没有任何 P 图。

整体效果:三区块依次演示「① animateTo 显式动画(蓝色 A 圆角方块 + 放大/还原按钮)」「② animation 隐式动画(绿色 B 圆方块 + 切换大小按钮)」「③ CustomDialog 自定义弹层入场(打开确认弹层按钮)」:

动画体系 demo 真机整体效果

重点看画面:第一区块是蓝色 A 圆角方块(点「放大」会带 400ms 动画变大变红旋转 15°,「还原」带动画回初);第二区块是绿色 B 圆方块(点「切换大小」自动跑 300ms 动画,不用套 animateTo);第三区块是「打开确认弹层」按钮(点开有原生淡入遮罩 + 弹层居中)。三件套各司其职,界面动起来。


七、三件套 vs:啥时候用哪个

维度 animateTo animation CustomDialog
管啥 显式一段改 绑字段变自动 弹层入场
用法 闭合回调 装组件上 Controller.open
控粒度 精确控这次改 装一次绑字段都跑 原生入场不可控
何时用 要精确控动画参数 要绑字段自动过渡 自定义弹层

一句话决策:精确控用 animateTo,绑字段自动用 animation,弹层入场用 CustomDialog


八、常见坑(都是血泪)

症状 解法
改状态忘套 animateTo 硬切无动画 要动画的改套 animateTo 闭合回调
animation 装错位置 不触发 .animation() 装在绑状态的组件上,装父级无效
CustomDialogControllerFunction.bind WARN 但能跑 可改 @Builder 方法形式避开,或忽略 WARN
animateTo 回调里改非绑字段 动画不跑 回调里改的必须是组件绑的状态字段
animation 期望立即跳 仍有 300ms 过渡 要立即跳用 animateToImmediately(不带动画)
CustomDialog.open() 弹层不显示 Controller 只声明不打开,要调 .open()

九、完整代码仓库

本文所有代码都已托管到 AtomGit,欢迎 clone、提 issue、点 star:

🔗 仓库地址:https://atomgit.com/JaneConan/arkui-animation

仓库包含:

  • 完整的「三件套同台演示」demo 工程
  • Index.ets 主页面(animateTo + animation + CustomDialogController
  • confirmDialogContent @Builder 弹层内容
  • 可直接用 DevEco Studio 打开运行

十、ArkUI 系列十篇脉络(完结)

这是 ArkUI 系列第十篇,凑齐十篇收尾。十篇脉络由浅入深:

主题 深度
1 入门:@State/@Link/@Prop + ForEach 列表渲染
2 进阶:Tabs 嵌套滚动
3 进阶:@Builder/@BuilderParam 组件复用
4 深水区:Navigation 多级路由
5 进阶:@Extend/@Styles 样式复用
6 深水区:Stage 模型四级状态
7 深水区:@Watch/@Observed 深层状态响应
8 进阶:@Provide/@Consume 跨层传递
9 深水区:LazyForEach 大列表虚拟滚动
10 收尾:动画体系 animateTo/animation/CustomDialog

十篇读完,你的 ArkUI UI + 状态 + 性能 + 动画就齐了基础覆盖。


写在最后

animateTo/animation/CustomDialog 的关系,本质是**「显式控动画」/「隐式自动过渡」/「原生入场」的分离**——前两个管状态变的过渡策略,第三个管弹层的原生入场。这思想在前端圈叫 transition/animation/dialog modal,在鸿蒙圈叫三件套,名字不同灵魂相通。

一旦你开始用动画思维写交互,你会发现大部分「状态硬切毫无动画」的需求,都是三件套声明的自然结果。代码量少一半,交互顺滑九成,设计逮不住你硬切。

代码已经给你了,仓库链接在上面。现在关掉这篇文章,打开 DevEco Studio,把 demo 跑起来,亲手点「放大」感受下 400ms 动画过渡。

跑通了,回来评论区打个「1」,我看看有多少人真的动手了。🚀


作者:JaneConan
仓库:https://atomgit.com/JaneConan/arkui-animation
协议:Apache-2.0,随便用,别告我

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐