HarmonyOS 6.0 页面转场动画与共享元素进阶

📅 2026/7/27 14:30:46 👁️ 阅读次数 📝 编程学习
HarmonyOS 6.0 页面转场动画与共享元素进阶

页面跳转时的动画不只是"好看"——它告诉用户"你从哪来,要到哪去"。没有动画的跳转像电视换台,突然蹦出一个新画面,用户要花半秒才能反应过来。好的转场动画是空间连续的,用户能感知到前后页面的逻辑关系。

Navigation内置转场

Navigation默认有推入/推出动画——新页面从右侧滑入,返回时向右滑出。这是iOS/Android/HarmonyOS通用的标准转场。

但不是所有场景都适合右滑。底部弹出的半模态、从中心展开的卡片、淡入淡出的tab切换,都需要自定义转场。

自定义NavDestination转场

NavDestination支持自定义转场动画:

NavDestination(){// 页面内容}.transition(TransitionEffect.OPACITY)

TransitionEffect提供几种预设:

预设效果
OPACITY淡入淡出
SLIDE(SlideEffect.Right)从右滑入
SLIDE(SlideEffect.Left)从左滑入
SLIDE(SlideEffect.Up)从下方滑入
SLIDE(SlideEffect.Down)从上方滑入
NONE无动画

组合动画:

NavDestination(){// 页面内容}.transition(TransitionEffect.SLIDE(SlideEffect.Right).combine(TransitionEffect.OPACITY))

combine把两个动画组合——同时滑动+淡入。

animateTo自定义转场

更灵活的控制用animateTo。在pushPath后手动触发动画:

@StatepageScale:number=0.8@StatepageOpacity:number=0pushToDetail(){this.navPathStack.pushPath({name:'Detail'});animateTo({duration:300,curve:Curve.EaseOut},()=>{this.pageScale=1;this.pageOpacity=1;});}

配合NavDestination的属性绑定:

NavDestination(){// 内容}.scale({x:this.pageScale,y:this.pageScale}).opacity(this.pageOpacity)

这种方式需要自己管理动画状态,但可以实现任意效果——缩放、旋转、3D翻转都行。

共享元素转场

共享元素转场(Shared Transition)让同一个元素在两个页面间平滑过渡。比如列表页的封面图"飞"到详情页的大图位置。

第一步:给两个页面的对应元素设相同的sharedTransition名称:

// 列表页Image(item.cover).width(120).height(80).sharedTransition('cover_'+item.id,{duration:300,curve:Curve.EaseInOut})// 详情页Image(this.item.cover).width('100%').height(240).sharedTransition('cover_'+this.item.id,{duration:300,curve:Curve.EaseInOut})

名称必须完全一致。duration和curve也要匹配,否则进出动画不协调。

sharedTransition的名称必须唯一——如果列表中有多个图片用同一个名称,动画会错乱。所以用’cover_’ + id组合唯一名称。

共享元素动画参数

.sharedTransition('unique_name',{duration:300,curve:Curve.EaseInOut,delay:0,zIndex:0,type:SharedTransitionEffectType.Static})

type有两种值:

  • Static:静态共享,元素直接从起点飞到终点,中间不跟随手势
  • Animated:动画共享,支持更流畅的过渡

zIndex控制共享元素的层级——值越高越在上面。如果共享元素被其他元素遮挡,加大zIndex。

多元素共享

多个元素同时做共享转场:

// 列表页Image(item.cover).sharedTransition('cover_'+item.id,{duration:300})Text(item.title).sharedTransition('title_'+item.id,{duration:300,delay:50})Text(item.author).sharedTransition('author_'+item.id,{duration:300,delay:100})// 详情页Image(this.item.cover).sharedTransition('cover_'+this.item.id,{duration:300})Text(this.item.title).sharedTransition('title_'+this.item.id,{duration:300,delay:50})Text(this.item.author).sharedTransition('author_'+this.item.id,{duration:300,delay:100})

delay让各元素依次出发,形成"波浪"效果。50ms间隔不会太密集也不会太分散。

注意:共享元素数量别超过5个。每个共享元素都需要独立的动画通道,太多会掉帧。

共享元素与页面背景

共享元素动画期间,两个页面都在渲染。如果页面背景不协调,会出现"穿帮"——比如列表页白底、详情页灰底,过渡期间背景切换很明显。

解决方式:让两个页面的背景色一致,或者给共享元素加一个背景遮罩:

Image(item.cover).sharedTransition('cover_'+item.id,{duration:300,zIndex:10}).backgroundColor(Color.White).padding(8)

给共享元素加白色背景+padding,让它"包裹"内容。动画时白色背景一起移动,遮挡后面的背景切换。

手势返回与共享元素

手势返回时共享元素需要反向动画——从详情页飞回列表页。Navigation默认支持,但有个前提:列表页的共享元素在返回时还必须在屏幕上

如果列表是LazyForEach,滚动到详情页后列表项可能已经被回收了。返回时找不到对应的共享元素,动画就没了。

解决方式:在pushPath前记录被点击项的索引,返回时先scrollToIndex让该项可见:

privateclickedItemIndex:number=-1;// 跳转前记录onClick(()=>{this.clickedItemIndex=index;this.navPathStack.pushPath({name:'Detail',param:{id:item.id}});})// 返回后恢复aboutToAppear():void{this.navPathStack.on('pop',()=>{if(this.clickedItemIndex>=0){this.scroller.scrollToIndex(this.clickedItemIndex);}});}

页面进入/退出动画

除了共享元素,页面本身也有进入和退出动画:

NavDestination(){Column(){// 页面内容}.width('100%').height('100%')}.onShown(()=>{animateTo({duration:300},()=>{this.contentOpacity=1;this.contentTranslateY=0;});}).onHidden(()=>{animateTo({duration:200},()=>{this.contentOpacity=0;this.contentTranslateY=20;});})

onShown在页面显示时触发,onHidden在页面被覆盖时触发。用animateTo驱动内容区的出现/消失动画。

关键:onShown/onHidden不等同于页面的mount/unmount。页面被覆盖(新页面push到上面)时触发onHidden但不销毁,返回时触发onShown但不重新mount。所以动画状态要在onShown中重置。

模态转场

全屏模态弹出需要从底部滑入的转场:

@StateshowSheet:boolean=false@StatesheetOffset:number=100Column(){// 内容}.width('100%').height('100%').translate({y:this.showSheet?0:100}).animation({duration:300,curve:Curve.EaseOut})

但这不是NavDestination,是同一个页面内的视图切换。真正的模态用bindSheet或bindContentCover,它们自带转场动画。

转场动画性能

转场动画期间两个页面同时渲染,性能压力是平时的两倍。几个优化点:

  1. 减少共享元素数量:不超过3个
  2. duration别太长:300ms是舒适区,超过500ms用户会觉得慢
  3. 共享元素避免shadow/blur:阴影和模糊的离屏渲染开销大
  4. 关闭不必要的animation:转场期间暂停其他动画
  5. 用renderGroup:把页面内容标记为渲染组,减少重绘范围
Column(){// 页面内容}.renderGroup(true)

renderGroup让组件树整体绘制到离屏缓冲区,转场动画只需要移动/缩放缓冲区,不需要重绘每帧内容。

踩坑清单

问题原因解决
共享元素没动画两个页面的名称不一致名称必须完全相同
动画闪烁两个页面背景色不一致统一背景色或给元素加背景
返回时动画丢失列表项被LazyForEach回收返回前scrollToIndex
多个元素动画错乱用了相同的sharedTransition名称每个元素用唯一名称
转场期间掉帧共享元素太多或有shadow不超过3个,去掉阴影
onShown中动画不执行页面已显示但状态没重置先重置状态再animateTo
combine动画不生效transition写法有误用combine链式调用
模态弹出没有动画直接改@State显示/隐藏用bindSheet或加translate动画
共享元素被遮挡zIndex太低加大sharedTransition的zIndex
手势返回动画卡顿两个页面同时渲染太重加renderGroup(true)

转场动画的原则:共享元素不超过3个,duration 300ms,curve用EaseInOut,zIndex保证在最上层。好的转场是克制的——只让最关键的视觉元素做动画,其他元素安静地淡入淡出。