页面跳转时的动画不只是"好看"——它告诉用户"你从哪来,要到哪去"。没有动画的跳转像电视换台,突然蹦出一个新画面,用户要花半秒才能反应过来。好的转场动画是空间连续的,用户能感知到前后页面的逻辑关系。
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后手动触发动画:
@State pageScale: number = 0.8
@State pageOpacity: number = 0
pushToDetail() {
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让该项可见:
private clickedItemIndex: 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中重置。
模态转场
全屏模态弹出需要从底部滑入的转场:
@State showSheet: boolean = false
@State sheetOffset: number = 100
Column() {
// 内容
}
.width('100%')
.height('100%')
.translate({ y: this.showSheet ? 0 : 100 })
.animation({ duration: 300, curve: Curve.EaseOut })
但这不是NavDestination,是同一个页面内的视图切换。真正的模态用bindSheet或bindContentCover,它们自带转场动画。
转场动画性能
转场动画期间两个页面同时渲染,性能压力是平时的两倍。几个优化点:
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保证在最上层。 好的转场是克制的——只让最关键的视觉元素做动画,其他元素安静地淡入淡出。
网硕互联帮助中心







评论前必须登录!
注册