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

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

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

引言

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

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


一、鸿蒙端属性动画简介

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

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

1. 基本属性动画(animation)

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

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

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

接口定义

typescript 复制代码
animateTo(value: AnimateParam, event: () => void): void

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

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

接口定义

typescript 复制代码
keyframeAnimateTo(param: KeyframeAnimateParam, keyframes: Array<KeyframeState>): void

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

typescript 复制代码
@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() 平滑插值。

typescript 复制代码
// 状态变量
@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,仅对非删除中的宠物生效。

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

工作原理

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

2.2 飞入垃圾桶(animateTo)

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

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

步骤 1:计算目标位置

typescript 复制代码
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)

typescript 复制代码
  // 初始化动画起始状态
  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,应用动画状态:

typescript 复制代码
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 }, [ ... ]) 两帧关键帧动画。

标准实现(水平抖动):

typescript 复制代码
@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 应用到需要抖动的元素:

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

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

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

typescript 复制代码
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 --- 多字段抖动:

typescript 复制代码
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 为例):

typescript 复制代码
// 校验:名称为空时触发抖动 + 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),也保留了强大的命令式控制能力(animateTokeyframeAnimateTo)。开发者可根据业务场景自由组合,在保证性能的前提下,为用户带来更具情感和确定性的交互体验。本文提炼的三种动画模式及配套的最佳实践,可直接复制到任何 HarmonyOS项目中,帮助快速实现高品质的提醒与删除动效。


相关推荐
weixin_604236672 小时前
华为二层交换机 企业完整正式版配置
运维·服务器·华为·华为交换机命令
互联网散修2 小时前
鸿蒙实战:基于Navigation自定义转场动画 —— 一镜到底
华为·harmonyos·转场动画·一镜到底
禁默2 小时前
[鸿蒙PC命令行移植适配]移植rust三方库tealdeer到鸿蒙PC的完整实践
华为·rust·harmonyos
不爱学英文的码字机器2 小时前
[鸿蒙PC命令行移植适配]移植rust三方库xh到鸿蒙PC的完整实
华为·rust·harmonyos
AI_零食2 小时前
鸿蒙PC Electron跨平台应用开发:辗转相除法计算器实现详解
前端·学习·华为·electron·开源·鸿蒙·鸿蒙系统
●VON3 小时前
AtomGit Flutter鸿蒙客户端:安全JSON解析
安全·flutter·华为·json·harmonyos·鸿蒙
●VON3 小时前
AtomGit Flutter鸿蒙客户端:项目架构概览
flutter·华为·架构·harmonyos·鸿蒙
独特的螺狮粉3 小时前
蛋鸡养护周期管理系统 - 鸿蒙PC Electron框架完整实现指南
前端·javascript·华为·electron·前端框架·开源·鸿蒙
小菜鸟学开发3 小时前
OpenHarmony 交叉编译深度解析
harmonyos