欢迎光临
我们一直在努力

HarmonyOS 动画实战:从「喵屿」看提醒与删除动效的三种实现

HarmonyOS 动画实战:从「喵屿」看提醒与删除动效的三种实现

    • 引言
    • 一、鸿蒙端属性动画简介
      • 1. 基本属性动画(animation)
      • 2. 显式动画(animateTo)
      • 3. 关键帧动画(keyframeAnimateTo)
    • 二、喵屿项目中三种动画实现
      • 2.1 删除模式抖动(setInterval + animation)
      • 2.2 飞入垃圾桶(animateTo)
      • 2.3 表单校验抖动(keyframeAnimateTo)
    • 三、总结与最佳实践
      • 核心设计原则
        • 1. 选型原则
        • 2. 多模态反馈增强
        • 3. 性能与可复用性
      • 结语

引言

在移动应用体验设计中,动画不仅仅是视觉点缀,更是用户与界面交互的情感纽带。恰当的动画能降低认知负担、增强操作反馈、提升产品质感,尤其在表单校验、删除确认等高频场景中,动画的缺失往往会让用户感到“卡顿”或“不确定”。然而,如何在不同平台上高效实现自然、连贯且可复用的动效,始终是前端开发的挑战之一。

「喵屿」App 作为一款宠物管理类应用,在 HarmonyOS 平台上深度实践了 ArkUI 动画体系,针对批量删除提示、单个删除确认、表单校验失败三种典型场景,分别采用持续抖动、飞入目标和关键帧抖动等动效方案。本文提炼自该 App 真实代码,展示动画实现细节,帮助开发者快速在 HarmonyOS 项目中落地高品质动效。


一、鸿蒙端属性动画简介

属性动画的核心是通过动画接口驱动组件属性变化,ArkUI 提供了三种核心动画接口——animation、animateTo、keyframeAnimateTo,分别适用于不同场景,可灵活实现各类属性动画效果。

动画接口作用域原理使用场景
animation 组件通过属性接口绑定的属性变化 自动识别组件的可动画属性变化,为其添加动画;组件接口调用自上而下执行,animation 仅作用于其上方的属性调用,支持为多个属性设置不同动画参数 对多个可动画属性配置不同动画参数的场景,需为单个组件的不同属性设置差异化动画
animateTo 闭包内改变属性引起的界面变化 通用函数,对比闭包执行前与闭包内状态变量引起的 UI 差异,为差异部分添加动画;支持多次调用、嵌套使用,闭包内所有属性变化遵循相同动画参数 对多个可动画属性配置相同动画参数的场景,需要嵌套动画的场景;多段动画循环可通过 playMode 和 iterations 实现
keyframeAnimateTo 多个闭包内改变属性引起的分段属性动画 通用函数,每一段闭包中的状态变量与前一次状态的差异,单独生成一段动画;支持多次调用,不推荐嵌套,每段动画可单独配置参数 同一属性需要实现连续多个动画的场景,避免多次创建动画导致的衔接卡顿

1. 基本属性动画(animation)

animation 是最简洁的属性动画实现方式,直接在组件上设置 animation 属性,即可为组件的可动画属性变化添加动画效果,无需额外复杂配置,适合简单场景的快速实现。

核心步骤:声明状态变量 → 将状态变量绑定到组件可动画属性 → 设置 animation 动画参数 → 触发状态变量变化,触发动画。

import { curves } from '@kit.ArkUI';
@Entry
@Component
struct AttrAnimationDemo3 {
// 控制动画状态的变量
@State animate: boolean = false;

// 声明相关状态变量,绑定组件可动画属性
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度

// 构建 UI 界面
build() {
Row() {
// 组件一:旋转+透明度动画
Column() {
}
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 设置 animation 属性,配置动画参数
.animation({
curve: curves.springMotion() // 使用弹簧动画曲线
})
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 切换动画状态
this.animate = !this.animate;

// 修改状态变量,触发属性变化,进而触发动画
this.rotateValue = this.animate ? 90 : 0; // 旋转动画:90度/0度
this.translateX = this.animate ? 50 : 0; // 组件二偏移动画:50px/0px
this.opacityValue = this.animate ? 0.6 : 1; // 透明度动画:0.6/1
})

// 组件二:平移+透明度动画
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
// 设置 animation 属性,配置动画参数
.animation({
curve: curves.springMotion() // 使用弹簧动画曲线
})
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

说明:点击组件一后,状态变量发生变化,组件一的 rotate 属性、组件二的 translate 和 opacity 属性随之变化,animation 接口自动为这些属性变化添加弹簧动画,实现平滑过渡。

2. 显式动画(animateTo)

animateTo 是显式动画接口,通过传入动画参数和闭包函数,闭包内所有属性变化将遵循统一的动画参数,适合需要为多个组件、多个属性设置相同动画效果的场景,支持嵌套使用,灵活性更高。

接口定义:

animateTo(value: AnimateParam, event: () => void): void

参数说明:value 为动画参数配置(AnimateParam 对象),event 为闭包函数,闭包内的状态变量变化所引发的 UI 差异,将自动应用该动画参数。

import { curves } from '@kit.ArkUI';
@Entry
@Component
struct AttrAnimateToDemo2 {
// 控制动画状态的变量
@State animate: boolean = false;

// 声明相关状态变量
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度

// 构建 UI 界面
build() {
Row() {
// 组件一
Column() {
}
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 调用 animateTo 接口,配置动画参数和闭包
this.getUIContext()?.animateTo({
curve: curves.springMotion() // 使用弹簧动画曲线
}, () => {
// 切换动画状态
this.animate = !this.animate;

// 闭包内修改状态变量,所有属性变化遵循相同动画参数
this.rotateValue = this.animate ? 90 : 0; // 组件一旋转:90度/0度
this.opacityValue = this.animate ? 0.6 : 1; // 组件二透明度:0.6/1
this.translateX = this.animate ? 50 : 0; // 组件二平移:50px/0px
})
})

// 组件二
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

说明:与 animation 接口不同,animateTo 将动画参数统一配置在接口中,闭包内所有属性变化(旋转、平移、透明度)均使用相同的弹簧曲线,无需为每个组件单独设置 animation,适合批量控制动画效果。

3. 关键帧动画(keyframeAnimateTo)

keyframeAnimateTo 用于实现分段动画,通过传入整体动画参数和关键帧数组,每一段关键帧可单独配置时长、曲线等参数,适合同一属性需要连续执行多个动画的场景,避免多次创建动画导致的衔接卡顿。

接口定义:

keyframeAnimateTo(param: KeyframeAnimateParam, keyframes: Array<KeyframeState>): void

参数说明:param 为关键帧动画整体参数(如重复次数、延迟、结束回调);keyframes 为关键帧数组,每一项包含当前关键帧的时长、曲线、闭包事件(属性变化逻辑)。

@Entry
@Component
struct KeyframeAnimateToDemo {
// 声明相关状态变量
@State rotateValue: number = 0; // 组件一旋转角度
@State translateX: number = 0; // 组件二水平偏移量
@State opacityValue: number = 1; // 组件透明度

// 构建 UI 界面
build() {
Row() {
// 组件一
Column() {
}
// 绑定旋转属性
.rotate({ angle: this.rotateValue })
// 样式设置
.backgroundColor('#317AF7')
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.borderRadius(30)
// 点击事件处理
.onClick(() => {
// 调用 keyframeAnimateTo 接口,配置整体参数和关键帧数组
this.getUIContext()?.keyframeAnimateTo({
iterations: 1 // 整体动画重复次数
}, [
{
// 第一段关键帧:800ms,弹簧曲线,属性变化
duration: 800, // 动画持续时间:800ms
curve: curves.springMotion(), // 动画曲线:弹簧曲线
event: () => {
// 第一段动画的属性变化
this.rotateValue = 90; // 组件一顺时针旋转90度
this.opacityValue = 0.6; // 组件二透明度降低
this.translateX = 50; // 组件二向右偏移50px
}
},
{
// 第二段关键帧:500ms,缓出曲线,属性恢复
duration: 500, // 动画持续时间:500ms
curve: Curve.EaseOut, // 动画曲线:缓出曲线
event: () => {
// 第二段动画的属性变化
this.rotateValue = 0; // 组件一恢复初始角度
this.opacityValue = 1; // 组件二透明度恢复
this.translateX = 0; // 组件二恢复初始位置
}
}
]);
})

// 组件二
Column() {
}
// 样式设置
.justifyContent(FlexAlign.Center)
.width(100)
.height(100)
.backgroundColor('#D94838')
.borderRadius(30)
// 绑定透明度属性
.opacity(this.opacityValue)
// 绑定平移属性
.translate({ x: this.translateX })
}
// 容器样式设置
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

说明:点击组件一后,将依次执行两段关键帧动画:第一段 800ms 实现组件旋转、偏移和透明度变化,第二段 500ms 实现属性恢复,两段动画衔接自然,无卡顿,适合需要多阶段动画的场景(如复杂的组件交互反馈)。


二、喵屿项目中三种动画实现

「喵屿」App 在宠物管理、表单校验等场景中大量使用了动画来提升交互体验。以下是三种核心动画模式的实现详解。

2.1 删除模式抖动(setInterval + animation)

场景:管理互动宠物页面进入删除模式时,网格中的宠物图标持续左右摆动,提示用户可点击删除。

核心技术:setInterval 定时切换角度状态 + .rotate() + .animation() 平滑插值。

// 状态变量
@State shakeAngle: number = 0;
private shakeTimer: number = 1;

// 启动抖动:每 100ms 在 +1.5° 和 -1.5° 之间交替
startShake(): void {
if (this.shakeTimer !== 1) return; // 防重入
let direction = 1;
this.shakeTimer = setInterval(() => {
this.shakeAngle = direction * 1.5; // 交替正负角度
direction *= 1;
}, 100);
}

// 停止抖动:清除定时器,复位角度
stopShake(): void {
if (this.shakeTimer !== 1) {
clearInterval(this.shakeTimer);
this.shakeTimer = 1;
}
this.shakeAngle = 0;
}

UI 绑定:在 GridItem 上应用 rotate + animation,仅对非删除中的宠物生效。

GridItem() {
// …宠物图片…
}
.rotate({
angle: (this.isDeleteMode && this.deletingUri !== pet.uri) ? this.shakeAngle : 0
})
.animation({ duration: 80, curve: Curve.Linear })

工作原理:

  • setInterval 每 100ms 将 shakeAngle 设置为 +1.5 或 -1.5
  • .animation({ duration: 80 }) 在状态变化间进行 80ms 的匀速插值
  • 80ms 动画 + 20ms 静置 = 100ms 完整周期,产生快速左右摆动效果
  • Curve.Linear 保证往返速度一致,视觉上均匀抖动

  • 2.2 飞入垃圾桶(animateTo)

    场景:用户确认删除宠物后,宠物图片从原位置飞向垃圾桶图标,同时缩小到 0.08 倍并淡出,500ms 后执行实际删除。

    核心技术:getUIContext().animateTo() 命令式动画,同时驱动 scale、opacity、translate。

    步骤 1:计算目标位置

    async animateDelete(index: number): Promise<void> {
    const pet = this.customPets[index];
    this.deletingUri = pet.uri; // 标记为"正在删除",在原网格中隐藏

    // 获取被删宠物在屏幕上的位置
    const itemPos = this.itemPositions[pet.uri];
    // 计算垃圾桶图标的中心坐标
    let targetX = 0;
    let targetY = 120; // 默认向上偏移
    if (itemPos && this.trashIconX > 0) {
    const trashCX = this.trashIconX + 12; // 垃圾桶中心 X
    const trashCY = this.trashIconY + 12; // 垃圾桶中心 Y
    const itemCX = itemPos.x + itemPos.w / 2; // 宠物中心 X
    const itemCY = itemPos.y + itemPos.h / 2; // 宠物中心 Y
    targetX = trashCX itemCX; // 水平位移
    targetY = trashCY itemCY; // 垂直位移
    }

    步骤 2:执行动画(scale + opacity + translate)

    // 初始化动画起始状态
    this.deleteAnimTranslateX = 0;
    this.deleteAnimTranslateY = 0;
    this.deleteAnimScale = 1;
    this.deleteAnimOpacity = 1;

    // animateTo:500ms EaseOut 驱动三个属性同时变化
    this.getUIContext().animateTo(
    { duration: 500, curve: Curve.EaseOut },
    () => {
    this.deleteAnimScale = 0.08; // 缩小到 8%
    this.deleteAnimOpacity = 0; // 完全透明
    this.deleteAnimTranslateX = targetX; // 水平移动到垃圾桶
    this.deleteAnimTranslateY = targetY; // 垂直移动到垃圾桶
    }
    );

    // 500ms 后执行实际的数据删除
    setTimeout(() => {
    this.executeDelete(index);
    }, 500);
    }

    步骤 3:Overlay 层渲染 — 在页面最上层放置一个与原始位置相同的 Image,应用动画状态:

    if (this.deletingUri) {
    Image(this.deletingUri)
    .width(this.deleteOverlayW)
    .height(this.deleteOverlayH)
    .objectFit(ImageFit.Cover)
    .position({ x: this.deleteOverlayX, y: this.deleteOverlayY })
    .scale({ x: this.deleteAnimScale, y: this.deleteAnimScale }) // 缩放到 0.08
    .opacity(this.deleteAnimOpacity) // 淡出到 0
    .translate({ x: this.deleteAnimTranslateX, y: this.deleteAnimTranslateY })
    .hitTestBehavior(HitTestMode.None) // 不拦截其他手势
    }

    设计要点:

    • 在原始网格中将宠物设为 Visibility.Hidden,同时在 overlay 层渲染相同的图片
    • animateTo 闭包内一次修改多个属性,框架自动并行驱动
    • Curve.EaseOut(快入慢出)让动画结束时有"落入"垃圾桶的视觉感受
    • 动画持续 500ms,setTimeout 同步等待 500ms 后执行 executeDelete()

    具体效果: 在这里插入图片描述


    2.3 表单校验抖动(keyframeAnimateTo)

    场景:用户在添加/编辑表单中未填写必填字段点击保存时,输入框或提示文字左右抖动,视觉提示用户。

    核心技术:uiContext.keyframeAnimateTo({ iterations: 2 }, [ … ]) 两帧关键帧动画。

    标准实现(水平抖动):

    @State nameTranslateX: number = 0;

    startAnimation(): void {
    const uiContext = this.getUIContext?.();
    if (!uiContext) return;

    this.nameTranslateX = 0;
    // iterations: 2 = 两帧 × 2 次 = 完整来回
    uiContext.keyframeAnimateTo({ iterations: 2 }, [
    {
    // 帧 1:100ms 内 translateX 从 0 到 5
    duration: 100,
    event: () => { this.nameTranslateX = 5; }
    },
    {
    // 帧 2:100ms 内 translateX 从 5 回到 0
    duration: 100,
    event: () => { this.nameTranslateX = 0; }
    }
    ]);
    }

    UI 绑定:将 translateX 应用到需要抖动的元素:

    TextInput({ placeholder: "请输入宠物名称", text: $$this.name })
    .translate({ x: this.nameTranslateX })
    // …
    .position({ x: this.nameTranslateX, y: 0 })

    变体 1 — 垂直抖动(疫苗/驱虫列表项):

    仅方向不同(translateY 代替 translateX),时长加倍(200ms),曲线更平滑:

    startAnimation(): void {
    const uiContext = this.getUIContext?.();
    if (!uiContext) return;
    this.translateY = 0;

    uiContext.keyframeAnimateTo({ iterations: 2 }, [
    {
    duration: 200,
    curve: Curve.Smooth, // 平滑过渡,比默认更柔和
    event: () => { this.translateY = 5; } // 向上抖动
    },
    {
    duration: 200,
    curve: Curve.Smooth,
    event: () => { this.translateY = 0; }
    }
    ]);
    }

    变体 2 — 多字段抖动:

    startAnimation(name: boolean = true, total: boolean = true): void {
    const uiContext = this.getUIContext?.();
    if (!uiContext) return;
    this.nameTranslateX = 0;
    this.totalTranslateX = 0;

    uiContext.keyframeAnimateTo({ iterations: 2 }, [
    {
    duration: 100,
    event: () => {
    if (name) this.nameTranslateX = 5;
    if (total) this.totalTranslateX = 5;
    }
    },
    {
    duration: 100,
    event: () => {
    if (name) this.nameTranslateX = 0;
    if (total) this.totalTranslateX = 0;
    }
    }
    ]);
    }

    调用方式(以 CatManagement.ets 为例):

    // 校验:名称为空时触发抖动 + Toast 提示
    if (StringUtil.isEmpty(this.name)) {
    VibrateUtil.alarmVibrate(); // 触觉反馈:警告振动
    this.startAnimation(); // 视觉反馈:抖动动画
    this.promptAction.showToast({ // 文字反馈:Toast 提示
    message: '请输入宠物名称'
    });
    return;
    }

    在这里插入图片描述


    三、总结与最佳实践

    「喵屿」App 在提醒与删除场景中成功落地了三种 HarmonyOS 动画模式,下表总结了各自的核心技术、适用场景与关键参数:

    模式核心技术适用场景参数要点
    持续抖动 setInterval + .rotate().animation() 编辑模式提示(批量删除) 幅度 1.5°、间隔 100ms、Curve.Linear
    飞入目标 animateTo() 删除确认动画(单个删除) duration 500ms、Curve.EaseOut、scale 0.08
    校验抖动 keyframeAnimateTo() 表单校验失败提示 iterations 2、每帧 100ms、位移 5vp

    核心设计原则

    1. 选型原则
    动画接口核心选择原则典型使用场景
    animation 为组件的不同属性设置不同的动画参数时使用。 按钮点击时,背景色渐变与缩放使用不同曲线。
    animateTo 为多个属性(可能跨组件)设置相同的动画参数,或需要动画嵌套时使用。 页面布局切换时,多个组件同时以相同方式移动和淡入淡出。
    keyframeAnimateTo 为同一属性创建连续、多段动画时使用。 实现类似“呼吸灯”效果(缩放-暂停-缩放),或复杂的路径动画。

    通用指导原则:

    • 优先使用animation:当只需为单个组件的属性添加动画,且参数各异时,这是最简洁的方式。
    • 需要同步控制多个动画时用animateTo:当多个属性或组件需要以相同的动画参数协同变化时,使用animateTo可以避免代码重复,并确保动画同步。
    • 需要复杂序列动画时用keyframeAnimateTo:当单个属性需要经历一系列不同的状态变化(如移动、停顿、再移动)时,关键帧动画是最佳选择,它能确保动画流畅衔接。
    • 性能考虑:属性动画(以上三种)的性能通常优于帧动画(ohos.animator),因此在满足需求的前提下应优先选择属性动画接口。帧动画仅在需要逐帧精确控制或可暂停能力时考虑。
    2. 多模态反馈增强

    动画不应孤立存在,与触觉反馈(VibrateUtil.buttonVibrate / alarmVibrate)结合使用,能显著提升用户对操作结果的感知强度。例如表单校验失败时,“抖动动画 + 警告振动 + Toast 提示”三管齐下,形成完整的错误反馈闭环。

    3. 性能与可复用性
    • 飞入垃圾桶动画采用 Overlay 层独立渲染,避免原网格组件重新布局,性能开销最小化。
    • 将抖动启动/停止、关键帧构建等逻辑封装为 AnimationUtil 工具类,可被任意页面调用,降低重复代码。
    • 注意 keyframeAnimateTo 每次调用前需重置状态变量,否则可能产生意外的跳帧。

    结语

    通过「喵屿」App 的实践可以看出,HarmonyOS 的 ArkUI 动画体系既提供了简洁的声明式接口(animation),也保留了强大的命令式控制能力(animateTo、keyframeAnimateTo)。开发者可根据业务场景自由组合,在保证性能的前提下,为用户带来更具情感和确定性的交互体验。本文提炼的三种动画模式及配套的最佳实践,可直接复制到任何 HarmonyOS项目中,帮助快速实现高品质的提醒与删除动效。


    赞(0)
    未经允许不得转载:171主机测评 » HarmonyOS 动画实战:从「喵屿」看提醒与删除动效的三种实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址