ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ArkUI动画实战:属性动画与转场动画从入门到进阶

2026/10/2 8:06:57 拓冰建站 浏览量
ArkUI动画实战:属性动画与转场动画从入门到进阶 这是我自己在整理鸿蒙中级课程笔记时一直想写的一篇——ArkUI 进阶的第一课属性动画和转场动画。学鸿蒙开发有一段时间的朋友大概都有这种感觉基础组件、布局写顺手之后界面总差点意思点击按钮没有反馈页面切换硬邦邦列表增删像在刷 Excel。问题就出在动画上。ArkUI 的动画不只是“好看”它直接关系到交互反馈的清晰度和界面的层级表达。这篇笔记我把属性动画和转场动画从 API 用法到实战案例、从参数调优到坑点排查完整梳理了一遍适合已经能独立写页面、但还没系统玩过动画的开发者。1. ArkUI 动画体系先搞明白再动手写1.1 动画的本质是状态变化的“解释器”很多人在 ArkUI 里写动画第一反应是去查 API、抄参数结果 animation 写了一大堆动画就是不生效。我刚开始也是这样后来才意识到问题出在理解上ArkUI 是声明式框架UI 是由状态驱动的。你改变了一个State变量的值组件会重新渲染而动画不过是这个“重新渲染”过程的插值器。说白了动画不是在“动组件”而是在“解释状态变化”。当opacity从 1 变成 0系统不可能瞬间完成它会在你设定的时长内把每一帧的透明度都计算出来逐帧渲染这个计算过程就是动画引擎的工作。所以你看 ArkUI 动画相关 API全部围绕“属性值的变化”展开没有哪个动画 API 是让你直接操作“移动这个组件到那个位置”而是让你声明“这个组件的位置属性变了请用动画过渡”。理解了这一点后面所有用法都顺理成章动画要生效必须有属性值的变化而且这个变化必须发生在“可被动画引擎捕获”的上下文里。ArkUI 提供了两种上下文分别是animation隐式动画和animateTo显式动画它们和状态管理、组件渲染的关系完全不同。1.2 属性动画与转场动画的分工ArkUI 的动画体系大致可以分成三类我习惯这样划分属性动画Attribute Animation同一个组件在不同状态之间切换时对可动画属性做插值过渡。典型的如点击按钮后透明度变化、卡片旋转、位移等。转场动画Transition Animation组件从无到有出现、从有到无消失时的整体过渡效果。典型的如 if 条件渲染切换、路由页面跳转。绘制与手势动画偏底层和交互比如Canvas绘制动画、手势animationCoordinator等通常不作为第一条学习路径。属性动画解决的是“怎么变”的问题转场动画解决的是“怎么来、怎么走”的问题。两者的实现机制也不一样属性动画是让组件自身的某个属性在一段时间内连续变化转场动画则是给组件的“生命周期瞬间”附加一个过渡效果比如透明度从 0 到 1、从下往上位移、缩放进场等。1.3 学习动画前的两个前置能力写动画之前有两块基础能力建议先打牢否则后面排查问题会很痛苦。第一状态管理的基本功。你要清楚地知道哪些变量的变化会触发 UI 更新State和Prop、Link的区别是什么因为动画的起点和终点都来自状态值。有几个常见问题比如外部传入的值变化了但 UI 没刷新或者子组件值改了父组件不知道根上都是状态管理没吃透。第二布局和渲染的基础理解。动画动的是渲染层的结果但影响动画的有布局参数、绘制参数和变换参数。有些属性动了会影响布局重新计算有些不会。这部分知识在优化动画性能时尤其重要比如width变化会引起父容器重排而scale变化只作用于绘制阶段性能开销差很多。2. 属性动画animation 与 animateTo 是使用频率最高的两个 API2.1 隐式动画与显式动画的代码差异ArkUI 属性动画的两个核心 APIanimation是隐式动画animateTo是显式动画。它们的触发方式不一样实际场景里的用法差异也很大。animation的写法是挂在组件上的指定这个组件的属性变化要用什么样的动画参数来过渡。一旦这个组件的某个可动画属性因为状态变化而改变就会自动触发。Entry Component struct ImplicitAnimationDemo { State scaleValue: number 1; State opacityValue: number 1; build() { Column({ space: 20 }) { Button(点击缩放/透明变化) .scale({ x: this.scaleValue, y: this.scaleValue }) .opacity(this.opacityValue) .animation({ duration: 300, curve: Curve.EaseOut, delay: 0, iterations: 1, playMode: PlayMode.Normal }) .onClick(() { this.scaleValue this.scaleValue 1 ? 1 : 1.2; this.opacityValue this.opacityValue 1 ? 1 : 0.6; }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }注意几个细节animation是放在属性之后的修饰符它只管它之前的那些属性。如果组件上有多个属性要动但animation写在了某个属性中间那animation之前的属性会有动画之后的就不会。这个顺序问题我见过不少新人踩坑。animateTo则是把“状态变化”包在一个闭包里执行闭包里所有影响到的可动画属性都会以动画形式过渡。它不绑定在组件上更像是一个命令Entry Component struct ExplicitAnimationDemo { State translateX: number 0; State rotateAngle: number 0; build() { Column({ space: 20 }) { Button(移动并旋转) .translate({ x: this.translateX }) .rotate({ angle: this.rotateAngle }) .onClick(() { animateTo({ duration: 500, curve: Curve.EaseInOut, onFinish: () { console.info(动画结束); } }, () { this.translateX this.translateX 0 ? 100 : 0; this.rotateAngle this.rotateAngle 0 ? 45 : 0; }); }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }animateTo的优势是“批量”闭包里改多个状态变量所有相关组件会一起动而且组件的动画参数不用逐个配置。但如果不同组件需要不同的动画时长或者曲线就得分多个animateTo来包或者配合animation覆盖组件级参数。什么时候优先用animation组件自身属性变化比较固定、而且动画参数不因场景改变的比如一个常驻按钮的按下反馈。什么时候用animateTo状态变化涉及多个组件、或者动画是一次性的业务交互比如点击后整个布局重新排列。我的经验是项目里 70% 的动画用animateTo因为它更直观前后状态一目了然。2.2 哪些属性可以动哪些动不了不是所有属性都能参与动画插值。我整理了一份实际开发中高频用到的“可动画属性表”按类别分好大家可以直接收藏。类别可动画属性说明基础布局width、height、padding、margin、position、offset这些属性变化会触发布局重算性能开销相对大视觉外观opacity、backgroundColor、borderColor、borderWidth、borderRadius、backgroundBlurStyle颜色插值在 ArkUI 中做了线性过渡浅色到深色效果自然变换rotate、scale、translate只影响绘制不触发布局性能最优推荐优先使用其他shadow、filter、clip、zIndexfilter 和 clip 的开销较大谨慎用于大面积组件注意layoutWeight、flexBasis这类由布局系统计算出来的属性是没法直接做动画的。还有visibility也只能做是否可见不能“渐变地隐藏”。我一开始天真地想把visibility配animation实现渐隐结果发现它直接切换没有任何过渡。后来改成用opacityhitTestBehavior或者在动画结束后再改visibility才达到效果。关于自定义属性动画ArkUI 提供了Animatable接口可以扩展自定义数据类型的插值。比如你有一个自定义的颜色类或者路径类实现了Animatable接口然后某个自定义组件的属性引用了这个类型就能参与动画。这个在实际业务中用到的不多但遇到复杂图形场景时会非常有用例如地图上绘制路径渐变时属性值从一组经纬度坐标点过渡到另一组。2.3 动画参数与回调时长、曲线、循环与结束监听duration是时长单位毫秒。经验值按钮反馈 100-200ms列表项增删 200-300ms页面级转场 300-400ms超过 500ms 的动画除非是强调展示否则会让人觉得拖沓。curve是曲线ArkUI 内置的Curve枚举有Linear、Ease、EaseIn、EaseOut、EaseInOut等支持Curve.spring()创建弹簧曲线。弹簧曲线在很多场景下比缓动曲线更有“物理感”比如列表卡片回弹、按钮点击后的回弹效果。它的参数有曲线质量和初速度我常用的配置是Curve.spring(0.4, 0.9)具体数值需要根据 UI 手感微调没法给一个通吃所有场景的参数。delay是延迟启动单位毫秒。常用于实现“级联动画”比如多个元素依次进场各自的delay依次递增 50-100ms。但要注意delay是相对于动画触发的时刻不是相对于上一个动画结束所以级联动画的节奏控制要自己算好。iterations默认 1设置-1表示无限循环。循环动画要非常克制一旦使用要确保退出逻辑能干净地停掉否则容易造成性能损耗。playMode在animation里比较有用PlayMode.Normal是正向播放PlayMode.Reverse是反向播放PlayMode.Alternate是正向然后反向交替。做呼吸灯效果时Alternate比手动维护两个状态值方便得多。onFinish回调在动画结束后触发。这里要特别注意如果动画被新动画打断onFinish可能不会触发需要结合finishCallbackType参数来判断是正常结束还是被打断。我在做游戏化任务卡片时踩过这个坑动画被打断后没收到回调状态一直卡在中间值。3. 转场动画把组件的出现和消失做出仪式感3.1 一个 if 条件渲染就能玩转的进入和退出动画转场动画最基础的用法是配合if条件渲染。当条件从 false 变为 true 时组件创建并进场从 true 变为 false 时组件销毁并退场。transition就是用来定义这个进场和退场动画的。Entry Component struct TransitionDemo { State show: boolean false; build() { Column({ space: 20 }) { Button(this.show ? 隐藏卡片 : 显示卡片) .onClick(() { this.show !this.show; }) if (this.show) { Column({ space: 10 }) { Text(这是一张转场卡片) .fontSize(16) Text(进入时从下方上移并淡入退出时向下移并淡出) .fontSize(14) .fontColor(#666) } .padding(20) .backgroundColor(#F0F0F0) .borderRadius(12) .width(80%) .transition( TransitionEffect.asymmetric( TransitionEffect.translate({ y: 200 }).animation({ duration: 300, curve: Curve.EaseOut }), TransitionEffect.translate({ y: 200 }).animation({ duration: 200, curve: Curve.EaseIn }) ) .combine(TransitionEffect.opacity(0).animation({ duration: 300 })) ) } } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } }TransitionEffect.asymmetric可以分别定义进场和退场效果第一个参数是进场动画第二个是退场动画。TransitionEffect.opacity(0)表示透明度从 0 开始过渡TransitionEffect.translate({ y: 200 })表示从下方 200vp 的位置移动进场。transition里的animation参数负责控制转场动画自身的时长和曲线。有个很重要的细节transition要写在目标组件上而不是包裹它的容器上。如果写到了父容器子组件进场时父容器的属性也会被带动变化。另外if切换虽然简单但不要在同级写多个if分支让它们同时切换那样会出现两个组件交替进出的视觉重叠最好用if-else结构。3.2 页面级路由转场Navigation 与 NavPathStack 的配合页面跳转动画比组件转场稍微复杂一点涉及路由栈的压栈和弹栈。鸿蒙推荐的页面路由方案是NavigationNavPathStack路由转场动画需要写在pageTransition方法里。Entry Component struct NavigationTransitionDemo { pathStack: NavPathStack new NavPathStack(); build() { Navigation(this.pathStack) { Column({ space: 20 }) { Button(跳转到详情页) .onClick(() { this.pathStack.pushPath({ name: DetailPage }); }) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } .pageTransition() { PageTransitionEffect.create(120) .translate({ x: 200 }) .opacity(0) .animation({ duration: 300, curve: Curve.EaseOut }) } } }PageTransitionEffect.create后面的参数是转场时长单位毫秒这里准确说应该是动画的持续时间参数配合.animation使用其实系统会自动处理。实际项目里更常见的是用自定义转场效果在pageTransition里可以分别定义Push和Pop的效果.pageTransition() { PageTransitionEffect.create(300) .translate({ x: 300 }) .animate({ duration: 350, curve: Curve.EaseOut }) .enter() .exit() }关于pageTransition我建议直接按导航栈的语义来理解Push是压栈进入Pop是弹栈返回。如果你只定义了进入效果返回效果会使用系统默认的两套风格不一致会显得很突兀。所以自定义转场时最好把 Push 和 Pop 都写了。3.3 共享元素转场让两个组件“穿越”起来共享元素转场是我个人觉得 ArkUI 做得相当漂亮的一个特性。场景很典型商品列表里有一张图点击进入详情页这张图从列表的位置“飞”到详情页的头部中间过程像穿越一样连贯。它背后的原理是在路由跳转时系统识别sharedTransition中相同的id自动为这两个组件创建一条“视频连线”式的过渡路径。用法分三步第一步在源页面列表页的图片组件上绑定sharedTransition(sharedImage)。第二步在目标页面详情页的图片组件上绑定相同的 id。第三步保证两个页面都处于同一个Navigation容器下并且源页面组件在跳转时仍然存在于组件树中。// 列表页 Image(this.imageUrl) .width(100) .height(100) .sharedTransition(sharedCoverImage, { duration: 400, curve: Curve.EaseInOut }) // 详情页 Image(this.imageUrl) .width(100%) .height(300) .sharedTransition(sharedCoverImage, { duration: 400, curve: Curve.EaseInOut })共享元素转场有一些限制要提前知道首先两个页面的容器类型要保持一致否则缩放计算会出问题。其次sharedTransition不能用于列表项循环渲染的场景比如ForEach生成的多个 item 都写了同一个 id系统会不知道跟谁匹配。我试过在一个商品列表里给每个 item 的图片都写sharedTransition(cover)结果跳转动画完全失效。正确的做法有两种一是通过路由参数把目标页面唯一确定二是给每个 item 的 id 拼接唯一标识比如cover_ item.id然后在详情页接收路由参数后拼出同样的 id。第二种做法更通用也是官方推荐的方向。3.4 自定义转场与“组合拳”TransitionEffect支持用.combine把多个效果组合起来。实际项目里纯位移或者纯透明比较少更多是把位移、缩放、透明度一起用。.transition( TransitionEffect.asymmetric( TransitionEffect.translate({ y: 50 }) .combine(TransitionEffect.scale({ x: 0.9, y: 0.9 })) .combine(TransitionEffect.opacity(0)) .animation({ duration: 350, curve: Curve.EaseOut }), TransitionEffect.translate({ y: 100 }) .combine(TransitionEffect.opacity(0)) .animation({ duration: 250, curve: Curve.EaseIn }) ) )组合转场对组件自身的布局有一定要求。比如scale直接作用在组件上会导致它的占位空间和视觉大小不一致如果父容器是Stack居中的还好说如果是在Row或Column里缩放会影响兄弟节点的排列其实不会scale不参与布局它只影响绘制。这一点和width/height动画有本质区别所以组合里如果用到了scale可以放心地配合其他属性使用。另外transition和animation是可以同时作用的。组件既写了transition自身又写了animation那么转场动画负责出现/消失属性动画负责进场后或离场前的属性变化。这种组合场景通常出现在弹窗和浮层里比如一个卡片弹出来之后内部的某个长条进度再做个宽度动画这时候两个系统各司其职不会冲突。4. 三个练手案例把动画链路一次性跑通4.1 列表项新增和删除的平滑转场列表增删是移动端最高频的交互之一。没有动画的列表新增项是“啪”一下冒出来删除项是“唰”一下消失视觉上非常生硬。用了转场动画之后增删过程有了明显的过渡层次用户能感知到“这里多了一个东西”或“这里少了一个东西”操作反馈清晰多了。实现思路是在ForEach的 item 上直接配置transition这样每个 item 在创建和销毁时都会走转场逻辑。Entry Component struct ListTransitionDemo { State items: number[] [1, 2, 3, 4, 5]; build() { Column({ space: 20 }) { Button(items.length 8 ? 新增一项 : 删除一项) .onClick(() { if (this.items.length 8) { this.items [...this.items, this.items.length 1]; } else { this.items this.items.slice(0, this.items.length - 1); } }) List({ space: 10 }) { ForEach(this.items, (item: number) { ListItem() { Text(项目 ${item}) .width(100%) .padding(16) .backgroundColor(#E8F0FF) .borderRadius(8) } .transition( TransitionEffect.asymmetric( TransitionEffect.translate({ y: 50 }) .combine(TransitionEffect.opacity(0)) .animation({ duration: 300, curve: Curve.EaseOut }), TransitionEffect.translate({ y: -30 }) .combine(TransitionEffect.opacity(0)) .animation({ duration: 250, curve: Curve.EaseIn }) ) ) }, (item: number) item.toString()) } .width(100%) .layoutWeight(1) } .padding(20) .width(100%) .height(100%) } }这个场景我实际跑下来有几个心得ForEach的第三个参数keyGenerator一定要给而且要返回稳定的唯一值。不要用数组下标做 key因为增删操作会导致下标错位动画绑定会乱套。另外当items是数组时增删要用不可变方式重新赋值比如展开后追加、slice后赋值而不是直接用this.items.push()因为 ArkUI 的状态管理监听的是赋值行为原数组的push操作不触发 UI 更新动画自然也不会发生。4.2 卡片展开和折叠的自然过渡卡片展开折叠是另一个必练案例。点击卡片头部下方内容区域展开或收起要求两部分配合头部箭头旋转 90 度内容区高度从 0 到实际高度或反向如果内容区里有文字还要考虑淡入淡出。Entry Component struct ExpandCardDemo { State expanded: boolean false; build() { Column({ space: 20 }) { Column() { Row() { Text(订单详情) .fontSize(18) .fontWeight(FontWeight.Bold) Blank() Text(展开) .rotate({ angle: this.expanded ? 45 : 0 }) .animation({ duration: 200, curve: Curve.EaseOut }) } .height(50) .padding({ left: 16, right: 16 }) .onClick(() { this.expanded !this.expanded; }) if (this.expanded) { Column() { Text(这里是折叠时看不到的内容) Text(展开后通过透明度渐显提升层次感) } .padding({ left: 16, right: 16, bottom: 16 }) .opacity(this.expanded ? 1 : 0) .transition( TransitionEffect.asymmetric( TransitionEffect.opacity(0).animation({ duration: 300, delay: 100 }), TransitionEffect.opacity(0).animation({ duration: 100 }) ) ) } } .width(90%) .backgroundColor(#FFFFFF) .borderRadius(12) .shadow({ radius: 10, color: rgba(0, 0, 0, 0.08) }) .clip(true) } .width(100%) .height(100%) .backgroundColor(#F5F5F5) } }卡片类组件做展开动画flex 布局下高度由内容撑起直接给height设置一个固定值去做动画并不合适因为内容是不确定的。我试过两种方案第一种是用if控制内容区出现/消失并用transition让内容区淡入淡出卡片整体的高度变化由布局自然完成——但这样高度变化本身没有动画是瞬间的第二种是先测量内容高度再显式地设置 height 从 0 到测量值。明显第一种在多数场景下够用且更稳定配合opacity的转场观感上已经足够“顺”。需要高度动画的场合可以延迟内容显示让高度先做动画内容在半途再淡入这样看起来更高级实现上也只要给transition加上delay就行。调delay的时候我建议先想清楚时间线。在这个例子里展开时高度变化 200ms内容出现延迟 100ms所以内容其实是到一半的时候才开始显示折叠时内容先消失100ms 后高度再收回用户不会看到文字被压缩变形。这套时间差逻辑可以复用到抽屉、手风琴菜单等几乎所有展开收起场景。4.3 详情页转场滑动加缩放的组合路线第三个案例是页面级转场把前面讲到的Navigation路由转场和共享元素综合起来。场景商品列表点击后进入详情页希望封面图有共享转场同时整个页面从右侧滑入并伴随轻微的缩放。Entry Component struct ProductListDemo { pathStack: NavPathStack new NavPathStack(); State productList: string[] [商品A, 商品B, 商品C]; build() { Navigation(this.pathStack) { List({ space: 10 }) { ForEach(this.productList, (name: string) { ListItem() { Row({ space: 12 }) { Image(https://example.com/cover.png) .width(80) .height(80) .borderRadius(8) .sharedTransition(cover_ name, { duration: 350 }) Text(name) .fontSize(20) } .padding(10) .backgroundColor(#FFFFFF) .borderRadius(12) .onClick(() { this.pathStack.pushPath({ name: ProductDetailPage, param: { productName: name } }); }) } }, (name: string) name) } .padding(10) .width(100%) .height(100%) } .pageTransition() { PageTransitionEffect.create(120) .translate({ x: 200 }) .opacity(0) .animation({ duration: 400, curve: Curve.EaseOut }) } } }详情页那边在aboutToAppear里取出路由参数给图片设置和列表页相同的sharedTransition(cover_ productName)这样点击跳转时图片会从列表位置平滑移动到详情页头部。注意共享元素和pageTransition一起用共享元素会覆盖普通转场对图片的效果也就是说图片不走“从右滑入”而是“跟随页面一起来但独立做位置/大小变化”。目标页面图片的大小如果和源页面差异很大系统会自动插值计算效果非常惊艳。这套路由转场我建议在真机上调试模拟器有时会因为渲染机制不同导致动画闪烁。5. 常见问题与排查技巧实录5.1 为什么动画没有生效这是个高频问题我总结了几个最常见的原因基本能覆盖 80% 的状况。现象原因解决方案属性变了但无动画组件没有写animation或状态变化不在animateTo闭包内补上animation或将状态修改包进animateTo只有部分属性有动画animation写在了几个属性的中间只对它前面的属性生效把animation移到所有需要动画的属性之后动画一闪而过duration太短或状态变化被连续触发检查时长和事件触发频率退场没有动画transition没写在组件自身上或使用了visibility控制显隐把transition写到目标组件上改用if渲染转场生效但很生硬使用了Curve.Linear或没有配置曲线使用Curve.EaseOut/EaseInOut还有一个隐蔽原因动画是生效了但因为目标组件的父容器没有足够的空间位移被裁剪了看起来像没移动。比如在List的第一项上做translate({ y: -100 })可能被List的边界裁剪。排查方法很简单把clip(false)加到父容器试试如果能看到组件移出边界就说明被裁剪了。状态驱动不生效的问题我还想强调一点ArkUI 的State只监听一层属性变化如果你直接修改对象里的某个字段UI 不会更新。比如this.userInfo.name 新名字这是无效的必须this.userInfo { ...this.userInfo, name: 新名字 }。动画本质依赖于状态更新这个没搞明白排查多久都白搭。5.2 卡顿与性能开销动画卡顿通常不是动画 API 的问题而是布局频繁计算和渲染压力过大。如果动画属性是width、height、padding这种参与布局的属性每变化一帧都可能触发父组件重新测量和排列代价很高。特别是列表里的每个 item 都在做宽度动画时卡顿几乎是必然的。优化思路是能用scale和translate实现的效果不要用width/height比如按钮的按下“缩小”效果用scale({ x: 0.95, y: 0.95 })就比改width和height强得多。另一个容易踩的坑是在ForEach循环里给每个 item 写独立的animation并且 item 的 key 不稳定。每次状态变化ForEach可能重建组件动画刚启动就重建不仅效果看不到还在不停创建销毁节点性能雪上加霜。正确做法是尽量让 item 的 key 稳定动画效果配置在 item 组件根节点上不要在父容器上搞全局动画。scale虽然不触发布局但如果组件数量很多GPU 的绘制压力依然存在。大型列表不建议让所有 item 同时做缩放动画可以考虑只对“当前交互的 item”做动画其他 item 保持静态。5.3 手势冲突与动画打断手势场景下动画和用户手指操作的优先级经常打架。最典型的是用户正在滑动列表这时候某个 item 刚好触发了自转动画结果手势被动画吃掉或者动画被手势打断体验很怪。ArkUI 在动画参数上有一个interruptible属性控制动画是否可以被新触发动画打断。默认情况下新动画会打断未完成的动画并跳到新动画的起点这在多数场景是合理的比如用户快速连点按钮旧动画被打断是应该的。但如果你的动画特别重要比如引导动画、成就展示动画就要设置interruptible: false或者配合手势检测让动画完成后才允许新的交互。手势和转场动画同时存在时可以在组件上使用gesture绑定TapGesture或者LongPressGesture然后在手势的回调里去触发animateTo。不推荐在onTouch里自行判断手势并触发动画因为 onTouch 的触发频率太高动画容易被反复打断。5.4 我踩过的一些值得一提的坑第一曲线参数不要用字符串。ArkUI 的curve参数类型是Curve枚举或ICurve直接写curve: ease-out是不会报错但还是会不生效的具体取决于版本现象是动画直接跳到终点。遇到动画“跳变”的时候先检查曲线参数类型。第二TransitionEffect里的.animation()如果没写会怎么样答案是转场会瞬间完成没有任何过渡。有时候你以为写了transition就自动有动画其实转场效果必须显式指定.animation来定义时长和曲线。第三共享元素转场期间源组件和目的组件不能同时绑定点击事件去触发其他动画否则会出现两个动画同时作用导致的视觉跳变。有一次我给详情页的图片同时写了sharedTransition和点击放大的animation结果图片在转场过程中就开始缩放看起来就别扭了。第四弹窗类组件如果用visibility控制显隐transition不会触发。鸿蒙的bindSheet和bindContentCover是专门的半模态组件它们有自己的显隐动画逻辑不需要也不能用transition控制。我在做底部弹窗时一开始也是用 if 控制自定义弹层的显隐后来发现手势拖拽关闭就很难模拟最后换成了bindSheet顺滑很多。尾声熟练之后要建立“动画先于代码”的思维动画这块内容实践下来的体会是先想清楚状态和时间线再写代码是最高效率的方式。我每次给页面加动画都会先用手比划一下这个元素从哪里来到哪里去应该先动哪个后动哪个时长大概多少曲线是快进慢出还是匀速。这些想清楚了对应的 API 基本是固定的代码只是把设计意图翻译出来而已。另外还有两个小建议给正在走鸿蒙进阶路线的朋友。第一动画参数尽量抽成常量比如Animations.ButtonFeedback { duration: 150, curve: Curve.EaseOut }项目里统一使用不然每个页面手感不一样领导验收时会觉得细节不到位。第二多看一眼官方文档里的动画性能建议ArkUI 的渲染管线和一些性能优化配置比如合理使用update更新局部组件在实际项目中非常管用。如果要把这篇笔记继续往下延伸下一步值得研究的是属性动画的高级用法比如关键帧动画和路径动画以及插值器自定义实现非线性非内置的动画效果。我计划在后续的课程笔记里再整理这两块到时候可以对比着看动画的底层能力基本就覆盖全面了。