大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

老实说,刚上手 ArkUI 的那会儿,我对动画的态度是:“能不用就不用,反正功能能跑就行。”
结果产品一句话把我教育了:

“页面是能用,但你这交互也太直男了吧,一点动效都没有,谁想点?”

后来我认真把 ArkUI 动画系统撸了一遍,才发现它其实不复杂,甚至可以说是很照顾开发者心情

  • 属性动画负责各种属性渐变;
  • 显式动画 / 动画控制器负责复杂编排;
  • 关键帧动画负责“花里胡哨”的高级动效;
  • 转场动画帮你搞定组件进出、页面切换;
  • 还支持跟手势做联动动画,比你自己算每一帧舒服多了。

今天这篇,就按你给的几个点,把 ArkUI Animation 从整体到实战撸一个“完全指南”,重点落在:

  • 常见动画类型怎么选
  • AnimatedProperty(可动画属性)到底是什么鬼
  • 动画控制器怎么玩
  • 手势联动怎么写得既跟手又不卡
  • 以及几个实用 UI 动效案例

一、动画类型:属性、关键帧、转场,谁负责哪摊事?

ArkUI 里,动画大致可以拆成三类维度来看:

  1. 属性动画(Property Animation)

    • 核心方式:

      • animateTo(...) 显式动画(函数)
      • .animation({...}) 属性动画(修饰器)
    • 用来让组件的各种“可动画属性”在一段时间内平滑变化,比如宽高、位置、透明度、圆角、缩放、旋转等等。

  2. 关键帧动画(Keyframe Animation)

    • 接口:UIContext.keyframeAnimateTo(param, keyframes)
    • 适合分段、多阶段变化,比如:放大 → 旋转 → 弹回 → 淡出这种复杂路径。
  3. 转场动画(Transition)

    • 组件级转场:.transition({...}) + animateTo
    • 页面级转场:导航 / router 的默认转场 & 自定义转场
    • 用来控制“出现 / 消失 / 切换时怎么动”,比如淡入淡出、位移进出、缩放、共享元素等。

一句粗暴总结:

  • 想让“某个属性”动:用属性动画(animateTo / .animation
  • 想让“分段动,从 A → B → C”:用关键帧动画
  • 想让“出现消失、列表增删、页面切换更顺滑”:用转场动画

二、属性动画:animateTo vs animation,怎么选?

属性动画基本是 ArkUI 动画的“地基”,先把这个玩明白,剩下的都是加特效。

2.1 显式动画 animateTo —— 闭包里的变化都做动画

核心形式:

this.getUIContext()?.animateTo(
  {
    duration: 300,
    curve: Curve.EaseInOut
  },
  () => {
    // 这里面所有引起 UI 变化的状态修改,都会按上面的参数做动画
  }
);

比如一个按钮点击后移动 + 变宽:

@Entry
@Component
struct ExplicitAnimationDemo {
  @State offsetX: number = 0;
  @State btnWidth: number = 120;

  build() {
    Column() {
      Button('点我动一下')
        .width(this.btnWidth)
        .translate({ x: this.offsetX })
        .onClick(() => {
          this.getUIContext()?.animateTo(
            { duration: 400, curve: Curve.EaseInOut },
            () => {
              this.offsetX = this.offsetX === 0 ? 80 : 0;
              this.btnWidth = this.btnWidth === 120 ? 200 : 120;
            }
          );
        })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

适用场景:

  • 需要对多个属性 / 多个组件统一用一组动画参数
  • 动画逻辑比较集中,一次性触发

2.2 属性动画 .animation() —— 声明式绑定,谁变谁动

核心形式:

Image($rawfile('xxx.png'))
  .opacity(this.opacity)
  .scale({ x: this.scale, y: this.scale })
  .animation({
    duration: 500,
    curve: Curve.Friction
  })

只要绑定在 .animation() 之前的“可动画属性”发生变化,就自动按参数做动画。

举个常见的卡片动效:

@Entry
@Component
struct CardAnimationDemo {
  @State cardSize: number = 200;
  @State radius: number = 8;
  @State angle: number = 0;

  build() {
    Column() {
      Column() {
        Text('Hello ArkUI')
      }
      .width(this.cardSize)
      .height(this.cardSize)
      .borderRadius(this.radius)
      .rotate({ angle: this.angle })
      .backgroundColor('#FFCCDD')
      .animation({
        duration: 600,
        curve: Curve.EaseOut
      })
      .onClick(() => {
        // 只管改状态,动画系统自动帮你补帧
        this.cardSize = this.cardSize === 200 ? 260 : 200;
        this.radius = this.radius === 8 ? 32 : 8;
        this.angle = this.angle === 0 ? 5 : 0;
      })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

对比一下:

场景 推荐
一段逻辑里控制多处 UI 差异 animateTo
同一个组件不同属性用不同动画参数 多个 .animation()
简单状态驱动动画(少改代码) .animation() 优先

三、关键帧动画:keyframeAnimateTo 把动画拆成几段玩

当你想实现这种动效:

卡片先轻轻放大 → 稍微旋转一下 → 再弹回原位

普通动画也能勉强写,但会写一堆 setTimeout + animateTo,既丑又不好维护。

ArkUI 的关键帧动画就是为这种场景准备的:UIContext.keyframeAnimateTo(...)

3.1 基本用法结构

this.uiContext?.keyframeAnimateTo(
  { iterations: 1 },   // 整体参数
  [
    { duration: 400, curve: Curve.EaseInOut, event: () => { ... } },
    { duration: 300, curve: Curve.Linear, event: () => { ... } },
    // 还可以继续加
  ]
);

3.2 实战例子:弹跳徽标

import { UIContext } from '@kit.ArkUI';

@Entry
@Component
struct KeyframeDemo {
  @State scale: number = 1;
  @State offsetY: number = 0;
  uiContext?: UIContext;

  aboutToAppear() {
    this.uiContext = this.getUIContext?.();
  }

  play() {
    if (!this.uiContext) return;
    this.uiContext.keyframeAnimateTo(
      { iterations: 1 },
      [
        {
          duration: 200,
          curve: Curve.EaseOut,
          event: () => {
            this.scale = 1.3;
            this.offsetY = -10;
          }
        },
        {
          duration: 150,
          curve: Curve.EaseIn,
          event: () => {
            this.scale = 0.9;
            this.offsetY = 5;
          }
        },
        {
          duration: 200,
          curve: Curve.EaseOut,
          event: () => {
            this.scale = 1.0;
            this.offsetY = 0;
          }
        }
      ]
    );
  }

  build() {
    Column() {
      Text('⭐')
        .fontSize(50)
        .scale({ x: this.scale, y: this.scale })
        .translate({ y: this.offsetY })
        .animation({ duration: 50 }) // 每段之间的小过渡更顺
      Button('播放关键帧动画').onClick(() => this.play())
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

重点:

  • 每个关键帧 duration 是该段动画的时长
  • event 里只负责“目标状态”
  • 系统自动帮你补插值和过渡

四、AnimatedProperty & 动画控制器:想精细控制,就别只用 @State

4.1 “可动画属性”本质是啥?

ArkUI 把属性分成两类:可动画 / 不可动画。

  • 可动画属性

    • 改变会引发 UI 变化
    • 变化“适合”用动画过渡
    • 包括:宽高、位置、缩放、旋转、透明度、圆角、阴影、背景色、文字大小等等
  • 不可动画属性

    • 例如:focusablezIndex 等,不适合慢慢变,那就不要加动画。

更高级的玩法是:

@AnimatableExtend 给自定义绘制内容抽象出“可动画属性”,比如音量柱高度之类。

4.2 用 @AnimatableExtend 扩展可动画属性(概念级)

@AnimatableExtend
class GaugeAnim {
  value: number = 0; // 0 ~ 1 表示进度
}

然后你就可以把 GaugeAnim 作为一个可动画属性,在每一帧里用它来绘制自定义 Canvas——详细实现这里不展开,只要你知道 ArkUI 支持自定义可动画属性就够了。

4.3 低层动画控制器:Animator

.animation()animateTo 不够用了(比如需要暂停 / 继续 / 倒放),可以下探到 @ohos.animator

import { Animator, AnimatorOptions, AnimatorResult } from '@ohos.animator';

@Entry
@Component
struct AnimatorDemo {
  @State value: number = 0;
  anim?: AnimatorResult;

  aboutToAppear() {
    const opt: AnimatorOptions = {
      duration: 1000,
      easing: 'friction',
      begin: 0,
      end: 100,
      iterations: -1
    };
    this.anim = Animator.create(opt);
    this.anim?.onframe = (v: number) => {
      this.value = v;
    };
  }

  build() {
    Column() {
      Text(`当前值:${this.value.toFixed(0)}`)
        .fontSize(30)
      Row() {
        Button('Play').onClick(() => this.anim?.play());
        Button('Pause').onClick(() => this.anim?.pause());
        Button('Stop').onClick(() => this.anim?.finish());
      }.margin({ top: 20 }).space(12)
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

这类 Animator 更偏“时间轴驱动”,适合:

  • 某些不直接绑定组件属性的动画(比如数值变化、音频可视化)
  • 需要精细播放控制(暂停、继续、加速、倒放等)

五、组合动画:一个交互往往不只一个属性在动

一个“看起来高级”的动画,往往是多个属性 / 多个组件的组合。

5.1 同一组件多个属性组合

示例:卡片点击 → 放大 + 提升阴影 + 轻微上移

@Entry
@Component
struct ComboCard {
  @State scale: number = 1;
  @State offsetY: number = 0;
  @State shadowBlur: number = 4;

  build() {
    Column() {
      Column() {
        Text('组合动画卡片').fontSize(20)
      }
      .scale({ x: this.scale, y: this.scale })
      .translate({ y: this.offsetY })
      .shadow({
        radius: this.shadowBlur,
        color: Color.fromARGB(80, 0, 0, 0)
      })
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(20)
      .animation({ duration: 250, curve: Curve.EaseOut })
      .onTouch((e) => {
        if (e.type === TouchType.Down) {
          this.scale = 1.03;
          this.offsetY = -4;
          this.shadowBlur = 12;
        } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
          this.scale = 1;
          this.offsetY = 0;
          this.shadowBlur = 4;
        }
      })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#F5F5F5')
  }
}

整个交互没有显式 animateTo,只靠 .animation() + 改状态就能完成。


5.2 多组件组合:按钮点击,多个元素一起动

这种玩法很适合做“结果反馈”:

  • 按钮缩小 / 变色
  • 图标飞出去
  • 背景淡入遮罩

可以用一个 animateTo 把所有涉及状态改掉,在闭包外各自绑定动画。


六、手势联动动画:让动画“跟着手走”

没有什么比“跟手动画”更能提升高级感了。

6.1 Bottom Sheet 联动:拖动高度 + 松手自动收 / 展

@Entry
@Component
struct BottomSheetDemo {
  private readonly MIN_HEIGHT: number = 80;
  private readonly MAX_HEIGHT: number = 400;

  @State sheetHeight: number = 80;

  build() {
    Stack() {
      // 背景内容
      Column() {
        Text('主内容区').fontSize(24)
      }.width('100%').height('100%').backgroundColor('#EEEEEE')

      // Bottom Sheet
      Column() {
        Row() {
          Text('上拉查看更多').fontSize(16)
        }
        .height(40).justifyContent(FlexAlign.Center)

        // 这里放列表、操作区域等
        Text('这里是 Sheet 内容').margin(16)
      }
      .width('100%')
      .height(this.sheetHeight)
      .backgroundColor(Color.White)
      .borderRadius({ topLeft: 16, topRight: 16 })
      .align(Alignment.Bottom)
      .gesture(
        PanGesture()
          .onActionUpdate((e) => {
            // 跟手拖动:手指向上,sheetHeight 变大
            let next = this.sheetHeight - e.offsetY; // offsetY 向上为负
            this.sheetHeight = Math.min(this.MAX_HEIGHT, Math.max(this.MIN_HEIGHT, next));
          })
          .onActionEnd(() => {
            // 松手后自动吸附到“展开 / 收起”
            const mid = (this.MAX_HEIGHT + this.MIN_HEIGHT) / 2;
            const target = this.sheetHeight > mid ? this.MAX_HEIGHT : this.MIN_HEIGHT;
            this.getUIContext()?.animateTo(
              { duration: 250, curve: Curve.EaseOut },
              () => this.sheetHeight = target
            );
          })
      )
    }
    .width('100%')
    .height('100%')
  }
}

这里有几个点可以记一下:

  • 手势更新时 不要用 animateTo,直接改状态,保证“跟手”;
  • 手势结束时再用 animateTo 做一个收尾弹回或吸附;
  • 限制最小 / 最大高度防止拖“飞”。

6.2 手势 + 动画:卡片滑动删除(侧滑 + 离场动效)

思路是:

  1. PanGesture 更新 offsetX
  2. 超过阈值后,animateTo 把整个卡片位移出屏幕,配合 transition 做删除

这里就不把全代码铺开了,但你可以联想一下:

  • 水平拖动 → translateX
  • 松手时判断 offsetX
  • 超过 1/3 宽度就认为删除

七、常见 UI 动效案例:这些是项目里真会用到的

最后,用几个“非常接地气”的动效做收尾。

7.1 按钮点击压缩反馈

@Component
struct PressButton {
  @State scale: number = 1;

  build() {
    Button('提交')
      .scale({ x: this.scale, y: this.scale })
      .animation({ duration: 80, curve: Curve.EaseOut })
      .onTouch((e) => {
        if (e.type === TouchType.Down) {
          this.scale = 0.95;
        } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
          this.scale = 1;
        }
      })
  }
}

用户会下意识觉得:“这个按钮是活的”


7.2 点赞 / 收藏心形动效(放大 + 弹回)

@Component
struct LikeIcon {
  @State liked: boolean = false;
  @State scale: number = 1;

  play() {
    this.getUIContext()?.keyframeAnimateTo(
      {},
      [
        { duration: 120, event: () => this.scale = 1.4 },
        { duration: 120, event: () => this.scale = 0.9 },
        { duration: 120, event: () => this.scale = 1.0 }
      ]
    );
  }

  build() {
    Text(this.liked ? '❤️' : '🤍')
      .fontSize(32)
      .scale({ x: this.scale, y: this.scale })
      .animation({ duration: 80 })
      .onClick(() => {
        this.liked = !this.liked;
        this.play();
      })
  }
}

比单纯变色手感好太多。


7.3 列表新增 / 删除项:transition 组件转场

根据性能建议,组件插入 / 删除优先用 transition,少用手动 animateTo 重写布局

@Entry
@Component
struct ListTransitionDemo {
  @State items: number[] = [1, 2, 3, 4];

  build() {
    Column() {
      Button('添加一行').onClick(() => {
        this.getUIContext()?.animateTo({ duration: 200 }, () => {
          this.items.push(Date.now());
        });
      })
      Button('删除最后一行').onClick(() => {
        this.getUIContext()?.animateTo({ duration: 200 }, () => {
          this.items.pop();
        });
      }).margin({ top: 8 })

      ForEach(this.items, (it) => {
        Row() {
          Text(`Item ${it}`).fontSize(18)
        }
        .height(40)
        .width('90%')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .margin({ top: 6 })
        .transition({
          type: TransitionType.All,
          opacity: 0,
          translate: { x: 30, y: 0 }
        })
      }, (it) => it.toString())
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F2F2F2')
  }
}

插入时:从右侧淡入;
删除时:淡出 + 左移(是逆过程,ArkUI 会自动反向补帧)。


最后:动效不是“越多越炫”,而是“刚刚好”

写到这里,其实你已经掌握了 ArkUI 动画系统 80% 的实战能力:

  • 知道什么时候用 animateTo,什么时候用 .animation()
  • 知道复杂多段动画可以交给 keyframeAnimateTo
  • 知道需要暂停 / 播放 / 调速时,可以下探到 Animator;
  • 知道转场动画要用 transition,别自己造轮子;
  • 知道手势联动不要每一帧都 animateTo,而是跟手修改 + 松手收尾

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐