组件显示和隐藏的优雅过渡:TransitionEffect 在 HarmonyOS6 PC 端的实战
做 HarmonyOS6 PC 端开发的时候,"显示/隐藏"可能是最常见的 UI 交互之一。
展开一个折叠面板、弹出一个通知条、切换一个详情区域——这些场景都需要组件从"不存在"变成"存在"(或者反过来)。如果你直接用if条件渲染,组件就是"砰"地一下出现、"砰"地一下消失,体验非常生硬。
ArkUI 提供了一个专门针对这种场景的方案:TransitionEffect。它跟if/else条件渲染配合使用,让组件在创建和销毁时自带过渡动画。
这篇文章就来把 TransitionEffect 彻底讲清楚,顺便聊聊它跟.transition()的区别——这个问题我当初也搞了好一阵。
先看效果
这个 Demo 实现了一个可显示/隐藏的内容区域,用到了TransitionEffect.OPACITY和TransitionEffect.scale的组合过渡。
@Entry@Componentstruct TransitionDemo{@Statevisible:boolean=true@StateselectedIndex:number=0build(){Column(){Text('显示隐藏过渡动画').fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){if(this.visible){Column().width('100%').height(120).backgroundColor(this.getColor(this.selectedIndex%8)).borderRadius(16).transition(TransitionEffect.OPACITY.animation({duration:400,curve:Curve.EaseInOut}).combine(TransitionEffect.scale({x:0,y:0}).animation({duration:400})))}else{Column().width('100%').height(120).backgroundColor('#E8E8E8').borderRadius(16).justifyContent(FlexAlign.Center).transition(TransitionEffect.OPACITY.animation({duration:300}))}Row({space:10}){Button(this.visible?'隐藏':'显示').onClick(()=>{this.visible=!this.visible})Button('切换内容').onClick(()=>{this.selectedIndex++this.visible=true})Button('滑入显示').onClick(()=>{this.visible=true})Button('淡出隐藏').onClick(()=>{this.visible=false})}.width('100%').justifyContent(FlexAlign.SpaceEvenly).margin({top:16})}.width('100%').backgroundColor('#FFFFFF').borderRadius(12).padding(16)}.width('100%').height('100%').backgroundColor('#F5F6FA').padding(16)}getColor(index:number):string{constcolors=['#FF6B6B','#FFA500','#FFD93D','#6BCB77','#4ECDC4','#4D96FF','#9B59B6','#FF6B9D']returncolors[index]}}TransitionEffect 的工作原理
TransitionEffect 的触发条件非常简单:组件因为 if/else 条件变化而被创建或销毁时。
当this.visible从true变成false:
if (this.visible)分支下的组件要被销毁了- 框架检测到这个组件上有
.transition()修饰器 - 不立即销毁,而是先执行"离场"过渡动画
- 动画结束后才真正移除组件
当this.visible从false变成true:
if (this.visible)分支下的组件要被创建了- 组件创建后立即执行"进场"过渡动画
- 从"过渡起始状态"动画变化到"正常状态"
这个机制跟animateTo()完全不同。animateTo 是"组件始终存在,只是属性在变";TransitionEffect 是"组件本身在创建和销毁,过渡动画是这个生命周期的附带效果"。
TransitionEffect 有哪些可用效果?
ArkUI 内置了这些 TransitionEffect 类型:
// 基础效果TransitionEffect.OPACITY// 透明度:0 → 1(进场)/ 1 → 0(离场)TransitionEffect.scale(value)// 缩放:指定值 → 1(进场)/ 1 → 指定值(离场)TransitionEffect.translate(value)// 位移:指定偏移 → 0(进场)/ 0 → 指定偏移(离场)TransitionEffect.rotate(value)// 旋转:指定角度 → 0(进场)/ 0 → 指定角度(离场)// 组合方法.combine(otherEffect)// 组合两个效果,同时执行.asymmetric(appear,disappear)// 进场和离场使用不同的效果说实话,光这几个基础效果加上组合,已经能覆盖 90% 的显示/隐藏动画需求了。
组合过渡:OPACITY + scale
Demo 里的核心代码:
.transition(TransitionEffect.OPACITY.animation({duration:400,curve:Curve.EaseInOut}).combine(TransitionEffect.scale({x:0,y:0}).animation({duration:400})))这段代码做了什么?
进场动画(visible 从 false 变 true):组件从"完全透明 + 缩放为零"过渡到"完全不透明 + 正常大小"。视觉上就是一个从无到有、从小到大弹出来的效果。
离场动画(visible 从 true 变 false):反过来,从"正常大小 + 不透明"过渡到"缩放为零 + 完全透明"。组件缩小到消失。
.combine()方法让两个效果同时执行。每个效果可以有自己的.animation()配置,这意味着你可以让透明度和缩放的时长不一样:
// 透明度 300ms 就完成,缩放 500ms 慢慢来TransitionEffect.OPACITY.animation({duration:300}).combine(TransitionEffect.scale({x:0.5,y:0.5}).animation({duration:500}))这种"不同步"的组合过渡有时候反而更有设计感。
非对称过渡:进场和离场不同效果
有些场景下,你希望组件"飞进来"但"淡出去"——进场和离场不是简单的反向关系。
TransitionEffect.asymmetric()就是为这个设计的:
.transition(TransitionEffect.asymmetric(// 进场效果:从左侧滑入TransitionEffect.translate({x:-200}).animation({duration:400,curve:Curve.EaseOut}),// 离场效果:淡出TransitionEffect.OPACITY.animation({duration:300,curve:Curve.EaseIn})))进场时组件从左边 200px 的位置滑入,离场时原地淡出。两个方向的效果完全不同。
这种非对称过渡在 HarmonyOS6 PC 端的侧边栏、抽屉菜单等场景中特别实用。侧边栏从左侧滑入(进场),但关闭时可能是淡出+滑回左侧(离场可以跟进场反向,也可以完全不同)。
Demo 里的两个分支都有 transition
注意 Demo 里if和else两个分支的组件上都有.transition():
if(this.visible){Column()// ... 彩色内容区域.transition(TransitionEffect.OPACITY.animation({duration:400,curve:Curve.EaseInOut}).combine(TransitionEffect.scale({x:0,y:0}).animation({duration:400})))}else{Column()// ... 灰色占位区域.transition(TransitionEffect.OPACITY.animation({duration:300}))}这是为什么呢?
因为if/else条件渲染在切换时,旧的组件被销毁,新的组件被创建。两个组件都有独立的 transition 配置。
当 visible 从 true 变 false:
- 彩色组件开始执行离场动画(缩小+淡出,400ms)
- 同时灰色组件被创建,执行进场动画(淡入,300ms)
当 visible 从 false 变 true:
- 灰色组件开始执行离场动画(淡出,300ms)
- 同时彩色组件被创建,执行进场动画(放大+淡入,400ms)
两个组件的过渡动画是并行执行的——一个在消失,另一个在出现。这就形成了流畅的交叉淡入淡出效果。
.transition() vs .transitionEffect():到底有什么区别?
这个问题在社区里被问过无数次了,我来理清楚。
.transition()
这是一个组件修饰器,直接挂在组件上。它接收一个 TransitionEffect 参数:
Column().transition(TransitionEffect.OPACITY.animation({duration:400}))当组件因为if/else被创建/销毁时,这个 transition 自动生效。
.transitionEffect()
这是一个更新的 API(API 11+),功能更强大。它也可以挂在组件上,但支持更细粒度的控制:
Column().transitionEffect(TransitionEffect.OPACITY.animation({duration:400}))两者的核心区别:
- 触发方式:
.transition()只能由 if/else 条件渲染触发;.transitionEffect()除了 if/else 外,还支持通过transition()全局函数主动触发 - 组合能力:
.transitionEffect()支持更灵活的链式调用和组合 - API 版本:
.transition()从 API 7 就有了;.transitionEffect()是 API 11 新增的
对于 HarmonyOS6 PC 开发,API 版本通常不是问题(HarmonyOS6 的 API 版本足够高)。建议新项目统一用.transitionEffect(),老项目如果已经在用.transition()也不用急着换。
实战:用 TransitionEffect 实现几种常见的 PC 端过渡
下面扩展几种在 HarmonyOS6 PC 端项目中常用的过渡效果。
顶部通知条
从顶部滑入的通知条,适合用在系统消息、操作反馈等场景:
if(this.showNotification){Row(){Text('操作成功!').fontColor('#FFFFFF').fontSize(14)}.width('100%').height(48).backgroundColor('#6BCB77').borderRadius(8).transition(TransitionEffect.translate({y:-60}).animation({duration:350,curve:Curve.EaseOut}))}进场时从上方 60px 的位置滑下来,离场时原路滑回去。
底部操作栏
从底部弹出的操作栏,适合用在浮动工具栏、批量操作面板:
if(this.showActionBar){Row(){Button('删除').backgroundColor('#FF6B6B')Button('标记').backgroundColor('#4D96FF')Button('取消')}.width('100%').height(56).justifyContent(FlexAlign.SpaceEvenly).backgroundColor('#FFFFFF').shadow(ShadowStyle.OUTER_DEFAULT_SM).transition(TransitionEffect.translate({y:80}).combine(TransitionEffect.OPACITY).animation({duration:300,curve:Curve.EaseOut}))}从底部 80px + 完全透明 → 原位 + 不透明。
右侧详情面板
HarmonyOS6 PC 端常见的右侧详情面板,从右滑入:
if(this.showDetail){Column(){// 详情内容...}.width(320).height('100%').backgroundColor('#FFFFFF').transition(TransitionEffect.asymmetric(TransitionEffect.translate({x:320}).animation({duration:400,curve:Curve.EaseOut}),TransitionEffect.translate({x:320}).combine(TransitionEffect.OPACITY).animation({duration:300,curve:Curve.EaseIn})))}进场时从右侧滑入(EaseOut,快入慢出),离场时滑出+淡出(EaseIn,慢入快出)。进场 400ms 离场 300ms,离场更快是因为用户已经决定关闭了,不想等太久。
缩放弹出的对话框
if(this.showDialog){Column(){Text('确认删除?').fontSize(18).fontWeight(FontWeight.Bold)Text('此操作不可恢复').fontSize(14).fontColor('#999999')Row({space:12}){Button('取消').onClick(()=>{this.showDialog=false})Button('确认').backgroundColor('#FF6B6B').onClick(()=>{this.showDialog=false})}.margin({top:16})}.width(300).padding(24).backgroundColor('#FFFFFF').borderRadius(16).shadow(ShadowStyle.OUTER_DEFAULT_SM).transition(TransitionEffect.scale({x:0.8,y:0.8}).combine(TransitionEffect.OPACITY).animation({duration:300,curve:Curve.EaseOut}))}经典的弹窗效果——从 80% 大小 + 透明弹出到正常大小 + 不透明。scale 起始值用 0.8 而不是 0,是因为 0 的话弹窗会从一个点开始放大,看着很突兀。0.8 只是稍微小一点,弹出来更自然。
一个容易踩的坑:TransitionEffect 不触发的情况
用 TransitionEffect 最常见的坑就是——动画没触发,组件直接闪现/闪消。
原因通常有这么几个:
1. 没有用 if/else 条件渲染
TransitionEffect 只在组件被创建/销毁时触发。如果你用的是.opacity(0)或.visibility(Visibility.None)来"隐藏"组件,组件本身一直在组件树里,transition 不会触发。
// 错误:组件始终存在,只是透明度变了Column().opacity(this.visible?1:0).transition(TransitionEffect.OPACITY)// 不会触发!// 正确:用 if 条件渲染if(this.visible){Column().transition(TransitionEffect.OPACITY)// 会触发}2. 状态变更在 animateTo 闭包内
如果改变 visible 的赋值操作在 animateTo 闭包里,可能跟 TransitionEffect 的动画机制冲突。建议直接赋值,不要包在 animateTo 里:
// 可能有问题animateTo({duration:400},()=>{this.visible=true// 在 animateTo 里改 visible})// 推荐this.visible=true// 直接赋值,transition 自己处理动画3. ForEach 中的组件
在 ForEach 循环里使用 TransitionEffect,需要确保 key 生成器是正确的。如果 key 不稳定(比如用 index 做 key),框架可能认为组件没变而不触发过渡。
HarmonyOS6 PC 端的过渡动画设计建议
PC 端和手机端在过渡动画上有几个关键差异:
1. 过渡距离更长
PC 端屏幕大,组件滑入/滑出的距离要相应增加。手机上 translate 60px 就够了,PC 端可能需要 100-200px 才有"从屏幕外飞入"的感觉。
2. 阴影和层级的配合
PC 端用户更习惯多层级 UI(面板叠面板)。TransitionEffect 配合.shadow()使用效果更好——面板滑入时自带阴影,层级感更明确。
3. 不要滥用过渡
PC 端的操作频率比手机端高(鼠标点击比手指触摸快得多)。如果每次显示/隐藏都有 400ms 的过渡动画,用户会觉得"怎么这么慢"。高频操作的组件(比如 tooltip、hover 弹出层)建议把过渡时长压到 150-200ms,甚至直接不用过渡。
4. 键盘用户的体验
PC 端有很多键盘操作(Tab 切换、Enter 确认、Esc 关闭)。这些操作的响应预期比鼠标点击更快,对应的过渡动画也应该更短更快。
小结
TransitionEffect 是 ArkUI 里做"组件显示/隐藏过渡"的标准方案。核心用法就三步:
- 用
if/else条件渲染控制组件的存在与否 - 在组件上用
.transition()或.transitionEffect()配置过渡效果 - 用
.combine()组合多个效果,用.asymmetric()实现进场/离场不同效果
记住一句话:animateTo 管"属性变化",TransitionEffect 管"生死过渡"。两者的适用场景不同,别搞混了。
