文章目录
- 前言
- 应用场景
- 方案设计
- 编码实现
- 状态管理
- Builder 封装折叠项
- asymmetric 过渡详解
- 状态管理辅助方法
- 全部展开/折叠
- 踩坑总结
- 坑1:transition 不加 animation 参数不生效
- 坑2:Builder 里的状态响应
- 坑3:折叠时内容闪烁
- 写在最后
前言
FAQ 页面、设置页面、手风琴菜单……这些地方都需要"展开/折叠"的交互。点一下标题,详细内容从下方展开;再点一下,内容收起消失。
ArkUI 实现这个效果有两种路子:一种是手动控制高度动画,另一种是用transition配合if-else条件渲染。第二种更简洁,今天就来聊聊,特别是asymmetric不对称过渡在这里的应用。
应用场景
展开折叠在 App 里的应用场景比你想的多:
- FAQ 问答页面
- 设置选项的详细说明
- 手风琴菜单
- 评论区的"展开更多"
- 表单的高级选项
核心需求都一样:点击触发展开,再点击触发折叠,展开和折叠都要有过渡动画。
方案设计
每个折叠项需要:
- 一个标题栏(始终可见,可点击)
- 一个内容区域(展开时显示,折叠时隐藏)
- 展开时的进入动画
- 折叠时的退出动画
关键难点在于:展开和折叠的动画效果应该不同。展开时内容从上方向下滑出,折叠时内容向上方收回。这就需要用到TransitionEffect.asymmetric。
编码实现
状态管理
三个折叠项,需要三个独立的状态变量:
@Entry@Componentstruct ExpandCollapsePanel{@StateisExpanded1:boolean=false@StateisExpanded2:boolean=false@StateisExpanded3:boolean=falsebuild(){Column(){// ...}}}Builder 封装折叠项
用@Builder把折叠项的 UI 抽出来,避免重复代码:
@BuilderExpandItem(title:string,index:number){Column(){// 标题栏Row(){Text(title).fontSize(14).fontWeight(FontWeight.Medium).layoutWeight(1)Text(this._getState(index)?'收起 ▲':'展开 ▼').fontSize(12).fontColor('#4D96FF')}.width('100%').padding(12).onClick(()=>{this._toggleState(index)})// 内容区域(条件渲染)if(this._getState(index)){Column(){Text(`这是${title}的详细内容区域。\n展开折叠动画可以让页面更具交互性,\n用户体验更加流畅。`).fontSize(12).fontColor('#999999').padding(12)}.width('100%').backgroundColor('#F5F6FA').borderRadius(8).animation({duration:300,curve:Curve.EaseInOut}).transition(TransitionEffect.asymmetric(TransitionEffect.OPACITY.animation({duration:300}).combine(TransitionEffect.translate({y:-20}).animation({duration:300})),TransitionEffect.OPACITY.animation({duration:200}).combine(TransitionEffect.translate({y:20}).animation({duration:200}))))}}.width('100%').backgroundColor('#FFFFFF').borderRadius(8).margin({bottom:8})}asymmetric 过渡详解
这段transition代码是整个案例的核心:
.transition(TransitionEffect.asymmetric(// 第一个参数:出现时的效果TransitionEffect.OPACITY.animation({duration:300}).combine(TransitionEffect.translate({y:-20}).animation({duration:300})),// 第二个参数:消失时的效果TransitionEffect.OPACITY.animation({duration:200}).combine(TransitionEffect.translate({y:20}).animation({duration:200}))))出现时(展开):透明度从 0→1 + 从上方 20px 处向下滑入,持续 300ms
消失时(折叠):透明度从 1→0 + 向下方 20px 处滑出,持续 200ms
注意消失动画比出现动画短(200ms vs 300ms),这是有意为之。用户点击"折叠"时希望内容赶紧收起来,所以消失要快一些。
状态管理辅助方法
因为用了@Builder,需要通过辅助方法来管理状态:
_getState(index:number):boolean{if(index===0)returnthis.isExpanded1if(index===1)returnthis.isExpanded2returnthis.isExpanded3}_toggleState(index:number){if(index===0)this.isExpanded1=!this.isExpanded1elseif(index===1)this.isExpanded2=!this.isExpanded2elsethis.isExpanded3=!this.isExpanded3}全部展开/折叠
Button('全部展开').width('100%').margin({top:8}).onClick(()=>{this.isExpanded1=truethis.isExpanded2=truethis.isExpanded3=true})Button('全部折叠').width('100%').margin({top:6}).onClick(()=>{this.isExpanded1=falsethis.isExpanded2=falsethis.isExpanded3=false})踩坑总结
坑1:transition 不加 animation 参数不生效
TransitionEffect后面必须跟.animation()来指定时长和曲线,否则可能使用默认参数或者直接没有动画效果。
坑2:Builder 里的状态响应
@Builder方法内部访问@State变量是可以正常响应变化的,但前提是这些变量属于调用@Builder的组件本身。如果@Builder被另一个组件调用,状态响应可能会有问题。
坑3:折叠时内容闪烁
有时候折叠动画开始的一瞬间,内容会"闪"一下。这通常是因为内容区域的高度变化太快。解决办法是给外层容器也加上.animation():
.animation({duration:300,curve:Curve.EaseInOut})写在最后
展开折叠动画的关键就是TransitionEffect.asymmetric——让出现和消失用不同的动画效果。这个小技巧能让交互变得更自然,因为现实世界中"展开"和"折叠"本来就不是完全对称的动作。
如果你的 App 里有 FAQ、设置项、手风琴菜单这类场景,不妨加上这个效果,开发成本低但体验提升明显。