你真想把鸿蒙动画“卷”到手感上头、交互顺到停不下来吗?

举报
bug菌 发表于 2025/11/01 21:37:43 2025/11/01
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 前言动画不是“锦上添花”,是信息表达、结构引导和心流维持的低延迟语言。...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

动画不是“锦上添花”,是信息表达、结构引导和心流维持的低延迟语言。本文用 **ArkUI(Stage 模型 / ArkTS)**把四件事讲透:动画 API 与设计取舍、关键帧思路、手势系统、以及自定义组件的实战套路。不端术语汤,直接能跑的代码 + 设计心法 + 性能要点,一次打包。

一、动画的三层用法:从“省心”到“可控”

先搭好心智模型:隐式 → 过渡 → 显式,用最省事的能满足需求就别上高配。

  1. 隐式动画(Implicit):给组件挂一个 .animation({...}),当可动画属性(如 opacity/scale/translate/rotate 等)变化时自动过渡,最省心
  2. 过渡(Transition):用 .transition(TransitionEffect.xxx()) 指定出现/消失的动效(入场、退场),对页面/列表尤实用。
  3. 显式动画(animateTo):用 animateTo({ duration, curve, delay }, () => { /* 改状态 */ }) 手动控制,适合串联、时序、条件分支等复杂场景。

二、动画 API 快速手册(够用又清楚)

2.1 隐式动画:一行“挂件”

// 隐式动画:状态一变,动画就走
@Entry
@Component
struct ImplicitDemo {
  @State show: boolean = false

  build() {
    Column({ space: 16 }) {
      Button(this.show ? 'Hide' : 'Show')
        .onClick(() => this.show = !this.show)

      // 可动画属性:opacity/scale/rotate/translate/size 等
      Image($r('app.media.poster'))
        .width(220).height(140)
        .opacity(this.show ? 1 : 0)
        .scale(this.show ? { x: 1, y: 1 } : { x: 0.92, y: 0.92 })
        .animation({
          duration: 220,
          curve: Curve.EaseInOut,   // 选个中性的缓动
        })
        .borderRadius(16)
    }.padding(24)
  }
}

使用心法

  • 用隐式动画做局部过渡/微反馈(按钮、卡片 hover、like 点赞)。
  • 一个组件挂一个 .animation() 就够;避免里外多层叠加导致时间/曲线“打架”。

2.2 过渡动画:出场与退场的“门面担当”

// Transition:对列表插入/删除、页面入场/退场很友好
@Entry
@Component
struct TransitionDemo {
  @State items: number[] = [1,2,3]

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Button('Add').onClick(() => this.items.unshift(Date.now()))
        Button('Pop').onClick(() => this.items.shift())
      }

      List() {
        ForEach(this.items, (it) => {
          ListItem() {
            Text(`Item ${it}`).fontSize(18).padding(12)
              .backgroundColor('#181818').borderRadius(10)
              .transition(TransitionEffect
                .asymmetric(                         // 入场 / 退场分开设
                  TransitionEffect.move(TransitionEdge.Start).combined(TransitionEffect.opacity(0.0)),
                  TransitionEffect.scale({ x: 0.9, y: 0.9 }).combined(TransitionEffect.opacity(0.0))
                )
              )
          }.margin({ bottom: 8 })
        }, (it) => it) // key
      }
      .layoutWeight(1)
    }.padding(20)
  }
}

使用心法

  • 入退场分设asymmetric(appear, disappear) 让进入更有“引导感”,离开更“干净”。
  • 长列表,退场动画别太慢(≤200ms),否则会阻塞布局回收。

2.3 显式动画:可编排、可插值、可打点

// 显式动画:完整掌控时间线与状态切换
@Entry
@Component
struct AnimateToDemo {
  @State x: number = 0
  @State scaleVal: number = 1

  private async bounce() {
    // 串联式时序
    animateTo({ duration: 180, curve: Curve.EaseOut }, () => this.x = 36)
    await sleep(180)
    animateTo({ duration: 160, curve: Curve.EaseInOut }, () => this.x = 0)

    // 叠加一个缩放
    animateTo({ duration: 120 }, () => this.scaleVal = 1.06)
    await sleep(120)
    animateTo({ duration: 120 }, () => this.scaleVal = 1.0)
  }

  build() {
    Column({ space: 16 }) {
      Button('Bounce').onClick(() => this.bounce())

      Text('Tap me')
        .fontSize(20).padding(16)
        .translate({ x: this.x })
        .scale({ x: this.scaleVal, y: this.scaleVal })
        .backgroundColor('#202020').borderRadius(12)
    }.padding(24)
  }
}

function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }

使用心法

  • 可插入逻辑:显式动画里可以做条件判断、埋点、并行动画。
  • 统一曲线:多段动画尽量用相近的 curve,否则会有“抖音式断感”。

三、关键帧设计:不是死磕 API,是运动剧本

ArkUI 没有“必用 Keyframe 类才叫关键帧”的刚性要求。关键帧是设计方法:把运动拆成若干“转折点”,再用 animateTo 串起来;或把多个隐式动画参数“对齐到时间点”。

3.1 关键帧脚本化(可维护)

type KF = { t: number; state: () => void } // t: 相对时间(ms)

function runKeyframes(frames: KF[]) {
  let t = 0
  frames.forEach(f => t = t + f.t)
  // 顺序执行
  let elapsed = 0
  frames.reduce((p, f) => {
    return p.then(() => {
      return new Promise<void>(resolve => {
        animateTo({ duration: f.t, curve: Curve.EaseInOut }, () => f.state())
        setTimeout(resolve, f.t)
      })
    })
  }, Promise.resolve())
}

// 使用
@Entry
@Component
struct KeyframeDemo {
  @State opacity: number = 0.0
  @State y: number = 24
  @State blur: number = 12

  play() {
    runKeyframes([
      { t: 120, state: () => { this.opacity = 1; this.y = 0 } },      // 0%→40%
      { t: 80,  state: () => { this.blur = 6 } },                      // 40%→67%
      { t: 60,  state: () => { this.blur = 0 } },                      // 67%→100%
    ])
  }

  build() {
    Column({ space: 12 }) {
      Button('Play Keyframes').onClick(() => this.play())
      Text('Keyframe Card')
        .opacity(this.opacity)
        .translate({ y: this.y })
        .blur(this.blur)
        .padding(18).backgroundColor('#1d1d1d').borderRadius(12)
    }.padding(24)
  }
}

设计心法

  • 先写剧本,再码参数:先画 0%/40%/60%/100% 的姿态草图(位移、缩放、透明),再落到数值。
  • 关键帧越少越纯粹:推荐 2–4 个转折点;多了容易“累赘”。
  • 节律一致:同一页面里的不同元素对齐时间点(例如共同在 120ms 时到位),层级自然。

四、手势系统:把“手上意图”翻译成 UI 的语言

4.1 常用手势与事件

  • TapGesture():点击/双击。
  • LongPressGesture({ duration }):长按。
  • PanGesture({ direction, distance }):拖拽,可取速度
  • PinchGesture() / RotationGesture():捏合/旋转。
  • GestureGroup(Parallel/Sequence)并行/串行组合(比如按住再拖)。
  • 事件回调:.onActionStart(e) / .onActionUpdate(e) / .onActionEnd(e)

4.2 手势 + 动画 = “流畅且可控”的本体

@Entry
@Component
struct PanCard {
  @State offsetX: number = 0
  @State offsetY: number = 0
  @State scaleVal: number = 1

  private settle() {
    // 回弹:显式动画做“自然落座”
    animateTo({ duration: 180, curve: Curve.EaseOut }, () => {
      this.offsetX = 0; this.offsetY = 0; this.scaleVal = 1
    })
  }

  build() {
    let pan = PanGesture({ direction: PanDirection.All })
      .onActionStart(() => {
        animateTo({ duration: 100 }, () => this.scaleVal = 1.02) // 轻微提起
      })
      .onActionUpdate((e) => {
        this.offsetX += e.offsetX
        this.offsetY += e.offsetY
      })
      .onActionEnd((e) => {
        // 速度阈值:快甩出去就“解散”,否则回弹
        const v = Math.hypot(e.velocityX ?? 0, e.velocityY ?? 0)
        if (v > 1400 || Math.abs(this.offsetX) > 140) {
          animateTo({ duration: 220, curve: Curve.EaseOut }, () => {
            this.offsetX += Math.sign(this.offsetX || 1) * 240
            this.scaleVal = 0.86
          })
          // 结束:模拟消失
          setTimeout(() => { this.offsetX = 0; this.offsetY = 0; this.scaleVal = 1 }, 260)
        } else {
          this.settle()
        }
      })

    Column() {
      Text('Drag me')
        .translate({ x: this.offsetX, y: this.offsetY })
        .scale({ x: this.scaleVal, y: this.scaleVal })
        .padding(24)
        .borderRadius(16)
        .backgroundColor('#222')
        .gesture(pan) // 将手势“挂”到组件
    }.padding(24)
  }
}

手势心法

  • 开始就给“起势”(放大 1–2%/提高海拔感),结束要“落座”(回弹或拆分两段动画)。
  • 速度优先于位移:甩出删除、回弹收起,用速度阈值更贴近人手感。

五、自定义组件动画实践:把通用动效封装成“乐高积木”

5.1 例 1:可复用的“弹簧卡片”🧩

目标:任意子内容都能套上“弹簧式按压 + 回弹”手感(按下缩小、释放反弹)。

// SpringCard.ets —— 简化弹簧手感(用显式动画近似)
@Component
export struct SpringCard {
  @State private scaleVal: number = 1
  @Prop contentBuilder: () => any    // 透传子内容

  build() {
    GestureListener()
      .onDown(() => animateTo({ duration: 80, curve: Curve.EaseIn }, () => this.scaleVal = 0.96))
      .onUp(() => animateTo({ duration: 160, curve: Curve.EaseOut }, () => this.scaleVal = 1.0))

    Column() {
      this.contentBuilder()
    }
    .scale({ x: this.scaleVal, y: this.scaleVal })
    .borderRadius(16).backgroundColor('#1a1a1a')
    .padding(12)
    .gesture(
      TapGesture().onAction(() => {
        // 可选:点击事件透传/反馈
      })
    )
  }
}

使用

@Entry
@Component
struct SpringUse {
  build() {
    SpringCard({ contentBuilder: () => Text('Buy Now').fontSize(18).padding(10) })
      .margin(20)

    SpringCard({ contentBuilder: () => Image($r('app.media.poster')).width(260).height(150) })
      .margin(20)
  }
}

封装心法

  • 交互与动画参数(时长、曲线、缩放幅度)暴露为 @Param,即可在不同场景“拧旋钮”。
  • 统一“起/落”曲线,按压快、回弹慢更接近物理直觉。

5.2 例 2:关键帧 Banner(进场→强调→稳定)

目标:页面打开时 Banner 先出现轻微强调稳定,同时配合手势左右滑。

@Entry
@Component
struct BannerKF {
  @State x: number = 18     // 初始右移
  @State blur: number = 10
  @State op: number = 0.0
  @State scale: number = 0.98

  aboutToAppear() { this.play() }

  async play() {
    // 0%→60%:入场
    animateTo({ duration: 180, curve: Curve.EaseOut }, () => {
      this.x = 0; this.op = 1; this.blur = 0
    })
    await sleep(180)
    // 60%→80%:强调(轻微放大)
    animateTo({ duration: 100, curve: Curve.EaseInOut }, () => this.scale = 1.02)
    await sleep(100)
    // 80%→100%:稳定回落
    animateTo({ duration: 120, curve: Curve.EaseInOut }, () => this.scale = 1.0)
  }

  build() {
    let pan = PanGesture({ direction: PanDirection.Horizontal })
      .onActionUpdate(e => this.x += e.offsetX)
      .onActionEnd(() => animateTo({ duration: 140 }, () => this.x = 0))

    Column() {
      Image($r('app.media.banner'))
        .width('100%').height(160)
        .translate({ x: this.x })
        .scale({ x: this.scale, y: this.scale })
        .opacity(this.op)
        .blur(this.blur)
        .borderRadius(16)
        .gesture(pan)
    }.padding(16)
  }
}

六、设计与工程双清单(做完这 12 条再下班)

设计侧(动效稿)

  1. 定义目的:引导/反馈/层级/情绪?不同目的决定时长与曲线。
  2. 关键帧脚本(0/60/100%)与延迟关系(谁先动、谁跟随)。
  3. 统一曲线家族:同一页面尽量只用 2–3 种 curve。

工程侧(ArkUI 落地)
4. 优先用隐式/过渡,复杂再上 animateTo
5. 手势与动画合拍onActionStart 起势、onActionEnd 落座。
6. 列表退场 ≤200ms;入场 ≤260ms,二者节律一致。
7. 封装可复用组件(SpringCard、BannerKF 等),参数化。
8. 动画与状态最小耦合:只让 @State 管控需要过渡的字段。
9. 避免多层 .animation() 叠加;用一个“总动画”覆盖。
10. 并行动画合并触发,减少多次 re-layout。
11. 弱设备降级:图片滤镜/模糊优先关;形变代替阴影。
12. 埋点:动效开始/结束记录,方便 A/B 与卡顿排查。


七、常见坑位与解法

  • 首帧闪烁aboutToAppear() 中先把初始状态设置到“起点”,再触发动画。
  • 卡顿:把 IO/网络从生命周期挪走;动画只改数字型属性(位移/缩放/透明),尽量避开频繁重排。
  • 手势与滚动冲突:横向 PanGesture 避免与纵向 List 抢手,必要时用 GestureGroup.Sequence 或在阈值前不拦截。
  • 曲线乱用:一个页面混入多种夸张弹性曲线,主观“花却不顺”。同一族的 EaseIn/EaseOut/EaseInOut 足矣。

八、可复制的“动画样式表”(拿去即用)

export const Motion = {
  fast: { duration: 120, curve: Curve.EaseOut },
  base: { duration: 180, curve: Curve.EaseInOut },
  slow: { duration: 260, curve: Curve.EaseInOut },
  pressIn: { duration: 80, curve: Curve.EaseIn },
  pressOut: { duration: 160, curve: Curve.EaseOut },
}

export const Transitions = {
  listAppear: TransitionEffect.opacity(0.0)
    .combined(TransitionEffect.move(TransitionEdge.Start)),
  listDisappear: TransitionEffect.opacity(0.0)
    .combined(TransitionEffect.scale({ x: 0.92, y: 0.92 }))
}

把它当“动效 Design Token”。项目统一调用,体验可控、维护省心。

结语:动画不是“秀”,是信息结构与情绪的放大器

当你用隐式抹平小摩擦、用过渡梳理结构、用显式表达节奏,再把手势接进来让用户“像拉一根橡皮筋”那样操作——你的界面就会有生命力
  现在的问题是:你打算先把哪个页面“动”起来?是首页 Banner 的“进→强调→稳”,还是商品卡片的“按→弹→落”?😉

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。