HarmonyOS6 PC 开发实战:手风琴式展开折叠动画,让页面告别“一坨文字“ 做PC端应用的时候有个问题一直困扰着我——屏幕大了内容反而更容易堆成一坨。用户看到满屏的文字第一反应就是关掉。后来我想明白了PC端的设置页、FAQ页、帮助文档这些场景其实特别需要一种按需展示的交互方式。用户点一下标题内容平滑地展开再点一下优雅地收回去。这就是我们常说的手风琴效果。今天我们就来聊聊在HarmonyOS6 PC端开发中怎么用ArkUI的动画能力实现一个体验不错的展开折叠效果。先看效果点击即展开再点即收起我们要做的效果是这样的页面上有几个卡片每个卡片有一个标题栏。点击标题栏下方的详细内容区域会以一个从上往下滑入淡入的组合动画展开。再点击内容区域以向下滑出淡出的动画收起。同时标题栏右侧会显示展开 ▼或收起 ▲的文字提示给用户明确的状态反馈。这个效果在PC端的设置页面里非常常见。比如Windows的设置页、macOS的系统偏好设置到处都是这种交互。HarmonyOS6 PC端的应用也该有。核心思路条件渲染 过渡动画说白了展开折叠的核心就两件事用一个布尔状态控制内容区域的显示/隐藏在内容出现和消失的时候加上过渡动画ArkUI里有个非常方便的机制——if条件渲染配合.transition()修饰器就能搞定这件事。当if条件从false变为true时组件会被创建并执行入场过渡动画从true变为false时组件执行出场过渡动画然后被销毁。我们再配合.animation()修饰器来让高度变化也有一个平滑过渡整个效果就出来了。状态管理每个折叠项一个布尔值先来看状态定义部分。EntryComponentstruct AccordionDemo{StateisExpanded1:booleanfalseStateisExpanded2:booleanfalseStateisExpanded3:booleanfalse// ...}三个折叠项三个State布尔值。简单粗暴但很管用。说实话如果折叠项很多比如FAQ页面有二三十个问答这种一个个定义的方式肯定不合适。那时候更好的做法是用一个数组来管理状态或者把每个折叠项封装成独立的子组件让它自己管理自己的展开状态。但作为学习示例三个状态变量已经足够把原理讲清楚了。我们还提供了两个辅助方法来读写状态_getState(index:number):boolean{if(index0)returnthis.isExpanded1if(index1)returnthis.isExpanded2returnthis.isExpanded3}_toggleState(index:number){if(index0)this.isExpanded1!this.isExpanded1elseif(index1)this.isExpanded2!this.isExpanded2elsethis.isExpanded3!this.isExpanded3}这样做的好处是把索引到状态的映射集中管理在Builder里调用起来很干净。标题栏点击切换的关键标题栏部分没什么黑魔法就是一个Row布局左边放标题文字右边放状态提示BuilderExpandItem(title:string,index:number){Column(){Row(){Text(title).fontSize(14).fontWeight(FontWeight.Medium).layoutWeight(1)Text(this._getState(index)?收起 ▲:展开 ▼).fontSize(12).fontColor(#007DFF)}.width(100%).padding(12).onClick((){this._toggleState(index)})// 内容区域在下面...}}几个值得注意的细节layoutWeight(1)让标题文字占据剩余空间状态提示自然靠右状态提示用三元表达式根据当前展开状态动态显示收起 ▲或展开 ▼onClick直接调用_toggleState切换布尔值ArkUI的响应式系统会自动触发UI更新重头戏TransitionEffect.asymmetric 非对称过渡这是今天最核心的知识点。我们来看看内容区域的代码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}))))}这段代码信息量挺大的我来拆解一下。什么是 TransitionEffectTransitionEffect是ArkUI专门用来定义组件出现和消失时的动画效果的。当组件被if条件创建出来时它会从 TransitionEffect 定义的初始状态过渡到正常状态当组件被销毁时从正常状态过渡到 TransitionEffect 定义的结束状态。为什么要用 asymmetricTransitionEffect.asymmetric()允许我们分别定义入场和出场的动画效果。这个设计非常合理——展开和收起本来就不该是完全相反的过程。你看我们的实现入场展开从上方20像素的位置滑下来同时从透明变为不透明耗时300ms出场收起向下方20像素的位置滑出去同时从不透明变为透明耗时200ms为什么出场时间更短这是一个UX细节。用户对收起的耐心比展开低——收起是个结束动作快一点会让用户觉得更干脆、不拖泥带水。展开稍微慢一点给用户一个内容正在呈现的感知过程。combine 的作用.combine()用来把多个过渡效果叠加在一起。我们的效果是淡入滑入同时进行所以需要把OPACITY和translate组合起来。如果不 combine你就只能定义单一的过渡效果比如只淡入不滑动视觉上会单调很多。.animation() 修饰器的配合你可能注意到了在.transition()之外我们还加了一个.animation({ duration: 300, curve: Curve.EaseInOut })。这个修饰器的作用是让组件的属性变化比如高度、宽度等布局属性的变化也有平滑过渡。坦白讲transition管的是组件的出现/消失animation管的是组件属性值的变化。两者配合起来才能保证展开的时候不仅有淡入滑入效果整个布局的重新排列也是平滑的而不是啪的一下跳过去。批量操作全部展开/全部折叠除了单个卡片的点击切换我们还加了两个按钮来做批量操作Button(全部展开).width(100%).margin({top:8}).onClick((){this.isExpanded1truethis.isExpanded2truethis.isExpanded3true})Button(全部折叠).width(100%).margin({top:6}).onClick((){this.isExpanded1falsethis.isExpanded2falsethis.isExpanded3false})点击全部展开三个布尔值同时置为true三个内容区域几乎同时执行入场动画。因为有Curve.EaseInOut缓动曲线视觉上看起来是整齐划一的展开效果。这个功能在PC端其实挺实用的。想象一下用户在设置页面想搜索某个选项如果所有分组都折叠了他得一个个点开找。给一个全部展开的入口体验会好很多。完整代码把上面的片段组合起来完整的页面结构如下EntryComponentstruct AccordionDemo{StateisExpanded1:booleanfalseStateisExpanded2:booleanfalseStateisExpanded3:booleanfalseBuilderExpandItem(title:string,index:number){Column(){Row(){Text(title).fontSize(14).fontWeight(FontWeight.Medium).layoutWeight(1)Text(this._getState(index)?收起 ▲:展开 ▼).fontSize(12).fontColor(#007DFF)}.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})}_getState(index:number):boolean{if(index0)returnthis.isExpanded1if(index1)returnthis.isExpanded2returnthis.isExpanded3}_toggleState(index:number){if(index0)this.isExpanded1!this.isExpanded1elseif(index1)this.isExpanded2!this.isExpanded2elsethis.isExpanded3!this.isExpanded3}build(){Column(){Scroll(){Column(){Text(展开折叠动画).fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){this.ExpandItem(功能介绍,0)this.ExpandItem(使用说明,1)this.ExpandItem(配置选项,2)Button(全部展开).width(100%).margin({top:8}).onClick((){this.isExpanded1truethis.isExpanded2truethis.isExpanded3true})Button(全部折叠).width(100%).margin({top:6}).onClick((){this.isExpanded1falsethis.isExpanded2falsethis.isExpanded3false})}.width(100%)}.width(100%)}.layoutWeight(1)}.width(100%).height(100%).backgroundColor(#F5F6FA).padding(16)}}扩展思考在PC端的实际应用场景展开折叠动画在PC端的应用场景比手机端多得多。设置页面是最典型的。PC端应用的设置项通常很多——显示设置、通知设置、隐私设置、账户设置等等。每个分类下面可能有十几个选项。用折叠面板把不同分类收纳起来用户需要时再展开页面整洁、查找高效。帮助文档/FAQ页面也很适合。用户在PC端看帮助文档的时候往往是带着具体问题来的。一个问题对应一个折叠项用户扫一眼标题就知道该不该点开。比把所有回答平铺在页面上强太多。代码编辑器的文件树也是类似思路。文件夹展开/收起本质上就是手风琴效果。HarmonyOS6 PC端如果要做一个文件管理器这个动画是基础中的基础。还有一个场景——数据面板。Dashboard 里经常有多个数据模块允许用户折叠暂时不关心的模块把注意力集中在当前关注的数据上。在PC端大屏幕上看数据报表的时候这个功能特别有用。进阶优化用数组管理折叠状态前面用了三个State变量来管理三个折叠项。如果折叠项是动态的比如从服务器拉取的FAQ列表我们可以把状态改成数组StateexpandStates:boolean[][false,false,false]// 切换某个项的展开状态_toggleState(index:number){// 注意直接修改数组元素不会触发UI更新// 需要创建新数组来触发响应式constnewStates[...this.expandStates]newStates[index]!newStates[index]this.expandStatesnewStates}这里有个坑要提醒一下。ArkUI的State对数组的监听是引用级别的直接修改this.expandStates[index]不会触发UI更新。必须替换整个数组引用才行。这个坑我踩过排查了好一会儿。用数组管理状态后全部展开和全部折叠也可以写得更简洁// 全部展开this.expandStatesthis.expandStates.map(()true)// 全部折叠this.expandStatesthis.expandStates.map(()false)踩坑记录过渡动画不生效的几个原因在调试过程中我遇到了几个过渡动画不生效的情况总结一下忘了加.animation()修饰器。如果你只写了.transition()但没有给父容器或相关组件加.animation()布局变化可能不会有平滑过渡直接跳过去了。if条件变化太快。如果在一个事件回调里连续多次切换状态框架可能会合并这些更新导致过渡动画根本没机会执行。translate的y值太小。如果你设的y: -5位移太小了肉眼根本看不出来。建议至少y: -15以上动画才明显。duration设太长。过渡动画最好不要超过400ms。太长会让用户觉得页面卡了。展开300ms、收起200ms是个比较舒服的区间。小结展开折叠动画是个看起来简单、做起来有细节的交互效果。核心就是三样东西State布尔值控制显隐if条件渲染配合.transition()实现出入场动画TransitionEffect.asymmetric()定义不同的入场和出场效果在HarmonyOS6 PC端开发中这个效果的使用频率会非常高。PC端屏幕大、内容多按需展示是信息架构的基本功。把这个模式吃透了设置页、FAQ页、帮助文档这些场景就都能搞定了。