摘要

ArkUI 内置属性动画、显式动画、页面路由转场动画三类动画能力,用于按钮点击反馈、卡片展开收起、弹窗淡入淡出、页面左右切换、共享元素过渡等交互效果,大幅提升 APP 视觉质感,是课程设计、毕设加分核心模块。API Version23 重构动画调度引擎、动画插值、帧同步、资源自动销毁逻辑,修复低版本动画卡顿、多次触发叠加错乱、页面退出动画残留、高帧率机型掉帧、循环动画内存泄漏等兼容缺陷。旧项目升级 API23 后常出现:动画重复执行无法终止、页面返回动画黑屏、循环闪烁动画占用大量 CPU、弹窗动画不流畅等问题。本文基于 DevEco Studio API23 及以上,封装通用动画工具,覆盖属性动画、显式动画、路由转场三大类,搭配卡片折叠、弹窗淡入、页面滑动切换、共享元素动画实战代码,输出动画性能优化、生命周期销毁规范,汇总版本升级故障解决方案,为鸿蒙动画交互开发提供标准化模板。

关键词

OpenHarmony;ArkUI;API Version23;动画;animateTo;属性动画;页面转场;共享元素动画;插值动画

一、引言

1.1 动画开发背景

静态页面交互生硬,动画用于反馈用户操作、引导视觉层级:点击按钮缩放、列表卡片展开收起、弹窗淡入、页面左右滑入切换、图片点击放大共享元素过渡。ArkUI 提供三套原生动画体系,无需引入第三方库,原生渲染性能更强。

  1. 属性动画(内置 animate 属性):组件 width/height/opacity/scale/rotate 等属性绑定动画,数据变更自动过渡;
  2. 显式动画 animateTo:手动触发一段连续过渡动画,支持延时、循环、缓动曲线;
  3. 页面路由转场动画:router 跳转时自定义进出动画,包含普通切换、共享元素过渡。

OpenHarmony API Version23 动画引擎核心升级点:

  1. 统一动画生命周期管理,页面销毁自动终止所有绑定动画,杜绝内存泄漏;
  2. 新增高精度缓动插值曲线,修复低版本动画先快后慢失真问题;
  3. 优化 60/120 高帧率设备帧同步,消除滑动动画掉帧撕裂;
  4. 标准化共享元素匹配规则,跨页面图片无缝过渡无闪烁;
  5. 限制后台闲置循环动画自动暂停,降低 CPU 耗电;
  6. 区分同步 / 异步动画调度,主线程动画不阻塞 UI 渲染。

大量旧项目升级后动画叠加错乱、退出页面动画持续后台运行耗电,根源是未管控动画生命周期、未设置动画终止条件,因此掌握 API23 全套动画规范是优质 UI 项目必备技能。

1.2 开发环境与测试场景

开发工具:DevEco Studio 5.0+ 适配系统:OpenHarmony API23、HarmonyOS NEXT 核心 API:animateTo、animation 属性、router 转场参数、sharedTransition 共享元素 测试场景:按钮点击缩放动画、卡片展开折叠、弹窗淡入淡出、页面左右滑入、图片共享元素过渡、循环加载动画

二、API23+ 三大动画体系核心 API 说明

2.1 内置属性 animation(自动过渡动画)

绑定到组件,当绑定的 @State 变量发生变化时,自动执行过渡动画,无需手动调用方法。

ets

.animation({
  duration: 300,       // 动画时长ms
  curve: Curve.EaseOut,// 缓动曲线
  delay: 0,            // 延时启动
  iterations: 1,       // 循环次数,Infinity无限循环
  playMode: PlayMode.Normal // 播放模式
})

可动画属性:opacity 透明度、scale 缩放、rotate 旋转、translate 位移、width/height 尺寸、margin、borderRadius、backgroundColor。

2.2 显式动画 animateTo(手动触发)

主动调用,在回调内修改状态变量,统一执行一段连贯动画,支持完成回调。

ets

animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
  // 此处修改所有需要过渡的状态
}, () => {
  // 动画执行完成回调
})

2.3 页面路由转场动画

router.pushUrl/replaceUrl 第二个参数配置转场:

  1. 普通页面滑动:transition 动画进出参数
  2. 共享元素:sharedTransition (id) 标记同名组件,跨页面无缝过渡

ets

router.pushUrl({url:"pages/detail/detail"}, {
  transition: RouterTransition.Slide,
  duration: 350
})

2.4 API23 强制约束与废弃规则

  1. 页面 aboutToDisappear 阶段,无限循环动画必须手动置终止,系统不会自动全部回收;
  2. 共享元素两个页面组件必须设置完全相同 sharedTransition 唯一 id;
  3. 动画回调内禁止大量网络、数据库同步阻塞代码,会造成掉帧;
  4. 废弃无时长超长循环动画,后台会强制冻结降低性能;
  5. 多动画同时执行建议统一 animateTo 批量修改状态,减少多次重绘;
  6. 弹窗、临时组件销毁时同步终止内部循环加载动画。

三、基础极简示例代码

3.1 属性动画:点击缩放按钮

ets

@Entry
@Component
struct AnimDemo {
  @State scaleVal: number = 1
  build() {
    Button("点击缩放")
      .width(240).height(48)
      .scale({x:this.scaleVal, y:this.scaleVal})
      .animation({duration:200, curve:Curve.EaseOut})
      .onClick(()=>{
        this.scaleVal = 0.8
        animateTo({duration:200},()=>{this.scaleVal=1})
      })
  }
}

3.2 显式动画:卡片展开折叠

ets

@Entry
@Component
struct CardAnim {
  @State expand: boolean = false
  build() {
    Column() {
      Text("折叠卡片")
        .width("100%")
        .height(this.expand ? 200 : 80)
        .backgroundColor(Color.White)
        .animation({duration:300})
    }
    .onClick(()=>{
      animateTo({},()=>{this.expand = !this.expand})
    })
  }
}

3.3 路由滑动转场跳转

ets

// 跳转页面时配置滑动动画
RouterUtil.push(PAGE_ROUTE.DETAIL, {}, {
  transition: RouterTransition.SlideRight,
  duration: 350
})

四、四大完整实战案例(全兼容 API23)

4.1 实战一:通用动画工具封装 utils/anim_util.ets

ets

import animateTo from '@ohos.arkui.animateTo'

type AnimOption = {
  duration?: number
  curve?: Curve
  delay?: number
  iterations?: number
}

class AnimUtil {
  private static instance: AnimUtil
  static getInstance(): AnimUtil {
    if (!AnimUtil.instance) AnimUtil.instance = new AnimUtil()
    return AnimUtil.instance
  }

  // 通用显式动画封装
  runAnim(opt: AnimOption, updateFunc: ()=>void, finish?: ()=>void) {
    const defOpt = {
      duration: opt.duration ?? 300,
      curve: opt.curve ?? Curve.EaseInOut,
      delay: opt.delay ?? 0,
      iterations: opt.iterations ?? 1
    }
    animateTo(defOpt, updateFunc, finish)
  }

  // 弹窗淡入快捷动画
  fadeIn(update: ()=>void, finish?:()=>void) {
    this.runAnim({duration:250, curve:Curve.EaseOut}, update, finish)
  }

  // 弹出缩放动画
  scalePop(update: ()=>void, finish?:()=>void) {
    this.runAnim({duration:300, curve:Curve.Elastic}, update, finish)
  }
}

export default AnimUtil.getInstance()

4.2 实战二:卡片展开 + 弹窗淡入动画页面

ets

import AnimUtil from '../utils/anim_util'
import CustomDialogController from '@ohos.arkui.dialog.CustomDialogController'

@CustomDialog
struct FadeDialog {
  controller: CustomDialogController
  @State opacityVal: number = 0
  @State scaleVal: number = 0.9

  aboutToAppear() {
    // 弹窗打开淡入缩放动画
    AnimUtil.scalePop(()=>{
      this.opacityVal = 1
      this.scaleVal = 1
    })
  }

  build() {
    Column() {
      Text("动画弹窗")
        .fontSize(20).margin({bottom:20})
      Button("关闭弹窗").onClick(()=>{
        AnimUtil.fadeIn(()=>{
          this.opacityVal = 0
          this.scaleVal = 0.9
        }, ()=>{
          this.controller.close()
        })
      })
    }
    .width(300).height(220)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .opacity(this.opacityVal)
    .scale({x:this.scaleVal, y:this.scaleVal})
    .animation({duration:250})
  }
}

@Entry
@Component
struct AnimCardPage {
  @State isExpand: boolean = false
  dialogCtrl: CustomDialogController | null = null

  openDialog() {
    this.dialogCtrl = new CustomDialogController({builder:FadeDialog()})
    this.dialogCtrl.open()
  }

  toggleCard() {
    AnimUtil.runAnim({duration:350}, ()=>{
      this.isExpand = !this.isExpand
    })
  }

  build() {
    Column({space:20}) {
      // 可折叠卡片
      Column() {
        Text("点击展开/折叠卡片")
        if(this.isExpand) {
          Text("卡片扩展内容\n动画过渡展示更多文字信息")
            .fontColor("#666").margin({top:10})
        }
      }
      .width("90%")
      .padding(16)
      .height(this.isExpand ? 180 : 90)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .onClick(()=>this.toggleCard())

      Button("打开淡入动画弹窗")
        .width(280).height(48)
        .backgroundColor("#007DFF")
        .onClick(()=>this.openDialog())
    }
    .width("100%")
    .height("100%")
    .padding(20)
    .backgroundColor("#F5F5F5")
  }
}

4.3 实战三:页面共享元素过渡动画(图片跨页面无缝切换)

列表首页页面

ets

import RouterUtil from '../utils/router_util'
import { PAGE_ROUTE } from '../model/Constant'

@Entry
@Component
struct ShareAnimList {
  @State imgSrc: ResourceStr = $r("sys.media.ohos_ic_public_camera")

  jumpDetail() {
    RouterUtil.push(PAGE_ROUTE.DETAIL, {img:this.imgSrc}, {
      transition: RouterTransition.None
    })
  }

  build() {
    Column({space:30}) {
      Text("共享元素动画-点击图片跳转详情")
        .fontSize(22)
      Image(this.imgSrc)
        .width(120).height(120)
        .sharedTransition("share_img") // 统一标记ID
        .onClick(()=>this.jumpDetail())
    }
    .width("100%")
    .height("100%")
    .justifyContent(Center)
  }
}
详情页页面

ets

import router from '@ohos.router'

@Entry
@Component
struct ShareDetailPage {
  @State src: ResourceStr = ""

  aboutToAppear() {
    const params = router.getParams() as {img:ResourceStr}
    this.src = params.img
  }

  build() {
    Column({space:40}) {
      Image(this.src)
        .width("100%").height(300)
        .sharedTransition("share_img") // 和首页ID完全一致
      Text("图片详情页,共享元素无缝过渡")
        .fontSize(20)
      Button("返回")
        .onClick(()=>router.back())
    }
    .width("100%").height("100%")
    .backgroundColor("#111")
  }
}

4.4 实战四:无限循环加载旋转动画(加载 loading)

ets

@Entry
@Component
struct LoadingAnimPage {
  @State rotateDeg: number = 0

  aboutToAppear() {
    // 无限循环旋转动画
    animateTo({
      duration: 1200,
      iterations: Infinity,
      curve: Curve.Linear
    }, ()=>{
      this.rotateDeg = this.rotateDeg + 360
    })
  }

  aboutToDisappear() {
    // 页面退出重置角度,终止循环动画
    this.rotateDeg = 0
  }

  build() {
    Column() {
      Text("加载中...")
        .fontSize(18).margin({bottom:20})
      Text("⬇")
        .fontSize(40)
        .rotate({angle:this.rotateDeg})
        .animation({duration:1200, iterations:Infinity, curve:Curve.Linear})
    }
    .width("100%").height("100%")
    .justifyContent(Center)
  }
}

五、API23 动画性能与交互优化规范

5.1 性能渲染规范

  1. 动画优先操作 transform(scale/rotate/translate)、opacity,避免动态修改 width/height/margin 触发页面重排;
  2. 无限循环动画页面销毁必须重置状态终止循环,防止持续占用 CPU;
  3. 单次 animateTo 批量修改多个状态,不拆分多次调用,减少页面重绘次数;
  4. 复杂长页面避免同时开启 3 个以上循环动画,防止帧率下降。

5.2 共享元素规范

  1. 跨页面共享组件必须使用完全相同的 sharedTransition 字符串 ID;
  2. 共享组件尺寸差距不宜过大,否则过渡会出现拉伸闪烁;
  3. 跳转时关闭默认 slide 转场,搭配共享元素效果更顺滑。

5.3 生命周期资源规范

  1. 弹窗、临时卡片内部循环动画,关闭弹窗时重置动画状态;
  2. 页面 aboutToDisappear 清空所有无限循环动画的迭代数值;
  3. 后台切页时暂停页面内加载动画,切前台再恢复,节省电量。

5.4 交互体验规范

  1. 按钮点击反馈动画时长控制 200~300ms,不宜过长拖沓;
  2. 弹窗淡入淡出 250~350ms,页面转场 300~400ms 为最佳视觉区间;
  3. 缓动曲线区分场景:弹窗弹出用 Elastic 弹性,页面切换用 EaseInOut,旋转加载用 Linear 匀速。

六、API23 升级高频兼容问题与解决方案

问题 1:页面退出后旋转加载动画仍在后台运行,耗电严重 解决:aboutToDisappear 重置动画角度数值,终止无限迭代循环。

问题 2:共享元素跳转画面闪烁、过渡断裂 解决:两个页面组件 sharedTransition 使用完全一致 ID,跳转关闭默认 slide 转场。

问题 3:多次快速点击按钮,动画层层叠加错乱 解决:增加防抖锁,动画执行期间拦截重复点击。

问题 4:高刷新率设备动画卡顿、画面撕裂掉帧 解决:统一使用 API23 标准 animateTo 批量更新状态,不分散单属性独立 animation。

问题 5:弹窗关闭动画不执行,直接瞬间消失 解决:先执行淡出动画 finish 回调内再调用 controller.close,不要直接关闭弹窗。

问题 6:修改 width/height 带动画,页面滑动明显卡顿 解决:改用 scale 缩放替代固定宽高变化,仅修改 transform 属性,减少重排。

七、总结

ArkUI 三套原生动画体系覆盖基础交互、弹窗、页面切换、跨页面共享过渡全场景,API Version23 重构动画底层调度、帧同步、生命周期自动回收机制,解决旧版动画叠加错乱、后台循环耗电、高帧率掉帧、共享元素闪烁等核心问题。 项目封装通用动画工具类统一管理淡入、缩放、自定义过渡动画,配套折叠卡片、弹窗、共享元素、加载旋转四大实战页面,所有代码兼容 API23 与 HarmonyOS NEXT,可直接用于课程设计 UI 美化、毕设高分视觉交互模块。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐