transition不生效?先搞清鸿蒙动画的三种类型
transition 不生效?先搞清鸿蒙动画的三种类型
开发环境:DevEco Studio、ArkTS、API 12+
前置知识:会写条件渲染(if)、知道组件和状态是什么
引子:一段完全没反应的动画代码
我在聊天页想给一条提示横幅加个滑入效果,照着文档写了:
Row() {
Text('正在给「小明」发群内私信')
}
.visibility(this.showBanner ? Visibility.Visible : Visibility.None)
.transition(TransitionEffect.translate({ y: -20 }).animation({ duration: 250 }))
一行效果都没有。 加动画、删动画、换参数,怎么试都是硬切。
后来才想明白:transition 只在组件被插入或删除组件树的那一刻触发,而我用 visibility 控制显隐——组件一直在树上,只是不画。根本没有"插入",哪来的转场动画。
这个坑逼着我把鸿蒙的动画体系重新理了一遍,也才分清那三个长得像的 API:animateTo、animation、transition。
一、先讲本质:动画不是逐帧画,是补间
如果你用 C 写过动画,脑子里的模型大概是这样:
for (int i = 0; i <= 100; i++) {
x = start + (end - start) * i / 100;
draw(x);
sleep(16);
}
每帧自己算值、自己画。ArkUI 完全不用这么干。
你只给起点和终点,中间那 100 帧由框架插值算出来。这叫补间动画(tween)。
所以写鸿蒙动画的思路是反过来的:不是"怎么画动画",而是"改状态的时候顺手说一句:这个变化要过渡"。
理解这一点,三种动画的区别就顺了——它们只是"在哪一句话上说这句"。
二、三种动画的分工
| 类型 | 写法 | 管什么 | 谁触发 |
|---|---|---|---|
| 显式动画 | animateTo(param, () => { 改状态 }) | 闭包里所有状态变化一起过渡 | 你主动包一段代码 |
| 属性动画 | 组件上加 .animation(param) | 挂在这个组件上,它的可动画属性一变就过渡 | 被动,属性变了自动动 |
| 转场动画 | 组件上加 .transition(...) | 组件出现和消失的那一刻 | if 条件变化导致插入/删除 |
用拍戏打比方:
- 显式动画:导演喊"这一段慢放",这一段里所有演员的动作一起慢下来
- 属性动画:给某个演员定了个规矩——他上场之后每次换装都放慢
- 转场动画:只管演员上场和下场那两个动作,戏中间怎么演它不管
这个比喻有个边界要说清:属性动画不是"装了就一直生效"。
.animation()只对挂载之后发生的属性变化起作用——组件第一次渲染时属性本来就是那个值,没有"变化",所以不播动画。它更像"上场之后的每一次换装都放慢",而不是"站在那儿就自动慢动作"。
最小对照:
// 显式动画:把"改状态"这件事包起来
this.getUIContext().animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
this.isExpanded = !this.isExpanded
})
// 属性动画:挂在组件上,之后它变就自动动
Text('内容')
.width(this.isExpanded ? 200 : 100)
.animation({ duration: 300 })
// 转场动画:管出现和消失
if (this.showTip) {
Text('提示')
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
}
一个版本坑
全局的 animateTo 从 API 18 起已废弃,要改用 UIContext.animateTo(或者组件里的 this.getUIContext().animateTo)。
废弃的原因不是它坏了,而是全局函数在多窗口、多实例场景下分不清该作用于哪个 UI 实例。网上很多教程还是老写法,照抄会踩这个。
三、为什么"放大"该用 scale,不该用 width
这一段是我查文档才彻底想清的。
一个界面画到屏幕上要分三步:
① 布局(Measure 算尺寸 / Layout 算位置) → ② 绘制(生成绘制指令) → ③ 合成(GPU 上屏)
改 width 会触发全部三步。因为宽变了,里面的子元素位置要重算、同一行的兄弟节点可能被挤走、父容器可能被撑大,整棵子树都可能重排。
改 scale 只跑 ②③——布局尺寸没变,只是在绘制时乘一个变换矩阵。
打个比方:改 width 是把房子扩建,得重画图纸、动承重墙、连邻居都得跟着挪;改 scale 是拿放大镜看房子,房子本身没动。
一帧只有 16.7ms(60fps 下的总预算)。布局这一步如果占了大头,剩下的时间就不够绘制和合成,掉帧就发生了——而且掉帧是连锁的:这一帧没画完,下一帧的数据又来了。
还有个更直观的问题:内容会"跳"
官方文档在属性动画那一节明确写了:
对于改变布局类属性(如宽高)的动画,内容通常会直接跳变到最终状态,例如文字或 Canvas 中的内容。
意思是你把 Column 的宽度从 100 动到 200,边框是平滑变宽的,但里面的文字位置会瞬间跳到新位置。
原因很直接:补间补的是属性值,不是布局结果。width 变了之后文字应该在哪,要重新测算,这个过程不参与补间。
结论:
// ❌ 贵,且内容会跳
.width(this.zoom ? 200 : 100)
// ✅ 便宜,内容跟着一起缩放
.scale({ x: this.zoom ? 2 : 1, y: this.zoom ? 2 : 1 })
判断口诀:视觉上的"放大缩小"用 scale,"移动"用 translate,"淡入淡出"用 opacity,只有真的需要占位变大才用 width/height。
四、transition 为什么不生效
回到开头那个坑。要讲清楚,得先问一个问题:
怎么让一个组件不显示?
鸿蒙里有两种方式,它们完全不同:
| 方式 | 组件还在组件树上吗 | 效果 |
|---|---|---|
if (条件) | 不在了,被销毁 | 条件 false 时组件从树上摘掉 |
.visibility(Visibility.None) | 还在,只是不画 | 组件仍占着位置,只是不显示 |
transition 挂载的位置,是组件被插入 / 删除的那一瞬间:
- 用
if控制 → 条件一变,组件被插入或删除 → transition 触发 ✅ - 用
visibility控制 → 组件一直在树上,只是不画 → 没有插入删除 → transition 永远不触发 ❌
这就是它必须配 if 的原因。
很多人写了 .transition(...) 完全没反应,九成是这三种情况:
- 用
visibility或opacity(0)控制显隐 - 组件一开始就在(初始渲染不播入场动画)
- 父容器尺寸自适应,组件插入时被挤得看不出位移
我项目里的实际改造
聊天页有个私信模式横幅:进入群内私信时,输入框上方会出现一条"正在给「某某」发群内私信"的提示。
if (this.conversation.type === 'group' && this.privateTarget !== null) {
Row() {
Image(avatar).width(20).height(20).borderRadius(10)
Text(`正在给「${this.privateTarget.username}」发群内私信`).fontSize(12)
Text('取消').fontSize(12).onClick(() => { this.cancelPrivateTarget() })
}
.width('100%')
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor('#FFF8E1')
}
这段原来是硬切:点好友头像,黄色横条"啪"地一下出现;点取消,又"啪"地消失。
它用的是 if,所以 transition 能生效。加上这段:
.transition(
TransitionEffect.translate({ y: -20 }) // 从上方滑入
.combine(TransitionEffect.OPACITY) // 同时淡入
.animation({ duration: 250, curve: Curve.EaseOut })
)
出现时从 -20 滑到位、透明度 0→1;消失时原路返回。
五、还有几种不常用但该知道的
| 类型 | 用途 |
|---|---|
关键帧动画 keyframeAnimateTo | 分段动画:先放大、再回弹。给若干个关键帧状态,框架在每段之间补间 |
路径动画 motionPath | 让组件沿一条自定义路径运动(贝塞尔曲线) |
组件动画 @ohos.animator | 命令式地控制播放、暂停、反向播放 |
关键帧动画
普通补间是"从 A 到 B",中间是单调的。关键帧能做成"放大 → 回弹"这种多段效果:
// 点一下先放大到 1.2,再弹回 1.0
this.getUIContext().keyframeAnimateTo({ iterations: 1 }, [
{ duration: 200, event: () => { this.scaleVal = 1.2 } },
{ duration: 150, event: () => { this.scaleVal = 1.0 } }
])
比起"嵌套两个 animateTo 回调",它的好处是整段动效一次描述完,参数(次数、延迟、完成回调)统一在第一个参数里配。
路径动画
让组件沿一条曲线走,path 用 SVG 路径语法:
Image($r('app.media.dot'))
.motionPath({ path: 'M0 0 C 100 0, 100 100, 200 100', from: 0, to: 1 })
from / to 是路径上的起止比例(0 到 1),可以用来做"只走前半段"这种效果。
日常 90% 的场景,前面三种(显式 / 属性 / 转场)就够了。这两种属于"知道有这么个东西,遇到具体需求再查"。
六、踩坑清单
- 用全局
animateTo(API 18 已废弃)→ 改用UIContext.animateTo - 用
width/height做放大缩小 → 每帧重排 + 内容跳变,应该用scale .transition()配visibility控制显隐 → 完全不生效,必须用if- 同一个组件既加
.animation()又用animateTo包 → 参数互相覆盖,效果不可预期 - 动画时长随手写 800ms 以上 → 用户感觉"卡",实际是做得太慢。交互类动画 200~300ms 比较合适
- 在
animateTo闭包里做异步操作 → 闭包必须同步改完状态,异步的改动不在动画范围内 - 忘了
curve→ 默认是线性,看起来"机械"。交互类用Curve.EaseInOut或Curve.Friction更自然
七、小结
| 问题 | 答案 |
|---|---|
| 加不加动画 | 状态变化频繁且细微(展开、切换、滑入)→ 加;一次性的大变化(换页面)→ 交给转场 |
| 用哪种 | 一次改多个状态 → 显式;组件自己属性变 → 属性;出现消失 → 转场 |
transition 不生效 | 先看显隐是不是 if 控制的 |
| 放大怎么做 | 用 scale,别用 width |
最后一句:动画这件事过度做反而扣分。每个点击都加弹跳、每个页面都塞转场,用户会觉得浮夸,代码也难维护。知道什么时候不该加,比知道怎么加更值钱。
相关文章
更多推荐

所有评论(0)