9、导航动画:为页面跳转添加过渡动画、自定义动画资源、使用Shared Element Transition
导航动画这件事,说实话,很多开发者容易忽略。我见过不少项目,功能做得挺扎实,但页面切换就是“啪”一下硬切过去,用户体验瞬间掉了一个档次。你想想看,用户滑动屏幕、点击按钮,结果界面生硬地闪一下——这感觉就像看电影时画面突然卡帧,很出戏。
我个人习惯是,只要项目用了Navigation组件,动画配置一定是第一时间加上的。因为成本真的很低,但收益非常明显。今天我们就来聊聊怎么给页面跳转加上丝滑的过渡动画,包括自定义动画资源,以及那个让用户“哇”出来的Shared Element Transition。
9.1 基础过渡动画:从零到一
Navigation组件对动画的支持,说白了就是通过NavOptions来配置。你不需要写什么复杂的动画框架,系统自带的几个动画就够用。
先看一个最简单的例子。假设我们有A、B两个Fragment,从A导航到B时,想让A从左侧滑出,B从右侧滑入:
// 在Navigation代码中配置动画
val navOptions = NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(R.anim.slide_in_left)
.setPopExitAnim(R.anim.slide_out_right)
.build()
findNavController().navigate(
R.id.action_a_to_b,
null,
navOptions
)
这里四个动画分别对应什么场景?我刚开始用的时候也搞混过,后来总结了一个规律:
| 动画属性 | 含义 | 实际场景 |
|---|---|---|
| enterAnim | 目标页面进入时的动画 | B页面从右侧滑入 |
| exitAnim | 当前页面离开时的动画 | A页面向左侧滑出 |
| popEnterAnim | 返回时目标页面的进入动画 | 从B返回A,A从左侧滑入 |
| popExitAnim | 返回时当前页面的离开动画 | 从B返回A,B向右侧滑出 |
嗯,这里要注意:pop前缀的动画是给返回操作用的。如果你只配置了前两个,返回时就会没有动画,或者动画方向反了。我在项目中就踩过这个坑,测试反馈说“返回时页面闪了一下”,其实就是pop动画没配。
核心原则:前进和返回的动画应该是对称的。前进时A左出B右入,返回时就应该是B右出A左入。
9.2 自定义动画资源:让过渡更有质感
系统自带的slide_in_right这些动画,其实只是简单的位移。如果你想做缩放、淡入淡出、甚至旋转效果,就得自己写动画资源文件了。
动画资源放在res/anim/目录下。我给大家看一个我常用的“缩放+淡入”组合动画:
<!-- res/anim/scale_fade_in.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:interpolator/decelerate_cubic">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0" />
<scale
android:fromXScale="0.8"
android:fromYScale="0.8"
android:toXScale="1.0"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%" />
</set>
这个动画的效果是:页面从80%大小淡入到正常大小,持续时间300毫秒。我个人觉得这个动画比单纯的滑动要优雅,尤其适合那种详情页或者弹窗式的页面。
对应的退出动画:
<!-- res/anim/scale_fade_out.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:interpolator="@android:interpolator/accelerate_cubic">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0" />
<scale
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.8"
android:toYScale="0.8"
android:pivotX="50%"
android:pivotY="50%" />
</set>
小技巧:进入动画的duration可以比退出动画长一点。比如进入300ms,退出200ms。这样用户感觉页面是“从容地出现,干脆地离开”,心理体验更好。
然后在NavOptions里引用这些自定义资源:
val options = NavOptions.Builder()
.setEnterAnim(R.anim.scale_fade_in)
.setExitAnim(R.anim.scale_fade_out)
.setPopEnterAnim(R.anim.scale_fade_in)
.setPopExitAnim(R.anim.scale_fade_out)
.build()
你看,代码量几乎没有增加,但效果完全不一样了。
9.3 Shared Element Transition:让元素“飞”过去
这是导航动画里最炫酷的部分。Shared Element Transition,说白了就是让一个View从A页面“飞”到B页面。比如一个商品缩略图,点击后放大到详情页的顶部大图——这个过渡效果会让用户觉得非常自然。
实现这个效果需要三步走。我一步步说清楚。
第一步:给两个页面的共享View设置相同的transitionName
<!-- FragmentA的布局 -->
<ImageView
android:id="@+id/product_thumb"
android:layout_width="100dp"
android:layout_height="100dp"
android:transitionName="product_image"
android:src="@drawable/product_thumb" />
<!-- FragmentB的布局 -->
<ImageView
android:id="@+id/product_detail_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:transitionName="product_image"
android:src="@drawable/product_thumb" />
注意:transitionName必须完全一致,大小写都不能错。我曾经因为一个下划线没写对,调试了半天才发现。
第二步:在Fragment中设置共享元素
// FragmentA中发起导航
val extras = FragmentNavigatorExtras(
product_thumb to "product_image"
)
findNavController().navigate(
R.id.action_a_to_b,
null,
null,
extras
)
这里的product_thumb是View的id,"product_image"是transitionName。两者通过to关键字关联起来。
第三步:在FragmentB中延迟启动过渡
// FragmentB的onViewCreated中
postponeEnterTransition()
// 等图片加载完成后
product_detail_image.doOnPreDraw {
startPostponedEnterTransition()
}
为什么要延迟?因为Shared Element Transition需要知道目标View的最终位置和大小,才能计算出动画路径。如果图片还没加载完成,系统拿不到正确的尺寸,动画就会乱跳。我遇到过最夸张的情况是图片从屏幕左上角飞过来——因为ImageView当时还没布局完成,位置是(0,0)。
避坑指南:如果你用了Glide或Coil加载网络图片,一定要在图片加载完成后再调用startPostponedEnterTransition()。否则动画会失效,或者出现奇怪的闪烁。
9.4 知识体系总览
为了让你更直观地理解导航动画的整体结构,我画了一张图:
9.5 实战中的一些经验
最后分享几个我在项目中积累的小经验。
关于动画时长:不要超过400ms。我见过有人把动画设成800ms,结果用户觉得页面响应太慢。移动端的动画讲究“快而自然”,200-350ms是比较舒服的范围。
关于动画冲突:如果你同时用了Navigation动画和Fragment自身的动画,可能会产生冲突。我的建议是统一用Navigation的NavOptions来控制,不要在Fragment的onCreateAnimation里再写一套。
关于性能:Shared Element Transition对性能要求比较高。如果共享的View是一个大图,建议先压缩再过渡。我曾经在一个低端机上测试,图片从缩略图放大到全屏时明显掉帧,后来加了图片压缩就解决了。
一个实用技巧:在调试动画时,可以打开开发者选项里的“动画时长缩放”,把它调到10x,这样动画会变慢,方便你观察每个细节。调试完记得改回来,否则用户会以为你的App卡顿了。
导航动画这件事,说白了就是给用户一个“视觉上的引导”。让他知道“我从哪里来,要到哪里去”。好的动画让用户觉得流畅自然,差的动画让用户觉得莫名其妙。你想想看,一个App如果页面跳转都做不好,用户怎么放心把数据交给你?
好了,这一章的内容就到这里。动画配置虽然简单,但细节不少。建议你打开项目,把今天讲的三种动画都试一遍,感受一下不同参数带来的效果差异。
公众号:蓝海资料掘金营,微信deep3321