资讯动态

Android Transition框架全解析:从Scene到自定义转场动画

发布时间:2026/10/8 20:09:55 来源:尧图企业网站定制
在Android里做动画最不缺的就是方案AnimationSet、ObjectAnimator、AnimatorSet、Transitions还有后来的MotionLayout。但如果你和我一样维护的是已经有三四年历史、View层级复杂、动不动就要“同一界面根据状态切换成不同布局”的App你会发现手写Animator这条路越走越重。本篇要讲的android_transition_framework是API 19引入的过渡框架它能用一两行代码完成一组View在位置、尺寸、透明度、背景色上的渐进变化核心思路不是命令动画怎么动而是描述界面从A状态到B状态让框架自己反推动画。适合刚接触动画的新人也适合想优化转场方案的老人。其实Transition Framework在Android世界里的地位被严重低估了。很多人只拿它做Activity的转场效果结果一遇到定制需求就觉得鸡肋于是转投MotionLayout。MotionLayout是强但它是ConstraintLayout体系内闭环的而在一个老项目里ConstraintLayout占比可能不到一半。android.transition就不受这个制约它能作用在普通LinearLayout、FrameLayout上甚至能嵌到RecyclerView的Item动画里。这篇文章我会把Scene、Transition、TransitionManager这三样核心组件从机制讲到实战再把我这几年踩过的和Transition相关的坑一并列出来辅助你照着落地。1. 为什么我最终选定了android_transition_framework作为转场基底层1.1 手写属性动画的三个顽疾状态管理、时序控制、代码量在还没有Transition框架之前我做过一个从“列表卡片”到“详情浮层”的效果。需求很简单一个View从列表里的较小尺寸位置放大移动到屏幕中央同时背景从半透明变成全透明里面文字渐显。手写方案时我需要记住起始坐标和终止坐标而列表位置会随滑动变化所以坐标不能写死我还要在动画执行中拦截用户触摸防止在中间态点击出错更要处理动画被打断后的恢复。比如用户快速点击多个Item上一个动画还没结束就要强制取消然后重新Start如果真的没处理好就会看到View卡在某个Alpha值上。如果ViewModel在动画中途发起状态刷新动画值还会和新状态打架。这些还不包括动画编排本身。AnimatorSet虽然有playTogether和playSequentially但一旦场景里涉及多个View的属性联动代码量会随View数量爆炸式增长。你会发现动画其实不是“实现出来”最难最难的是“在正确的时机保持正确状态”。手动控Animator的图景往往是每次新增一个动画属性就要新加一组变量、一段回调、一堆异常保护。到最后动画业务代码和交互逻辑全混在一起连code review都没眼看。1.2 Transition框架重新定义动画从“命令”到“声明”Transition框架的做法和手写动画有本质区别。你不再告诉View“你从坐标A移动到坐标B用时300ms中间用DecelerateInterpolator。”你只告诉框架“这个界面现在处于A布局状态过一会儿变成B布局状态。”框架自己去对比两个状态下同一个View的属性差异凡是变化的属性都会生成动画没有变化的属性自动忽略。这就带来几个好处。第一状态管理被框架接管。你不需要手动保存View的起始值Transition内部会通过captureStartValues捕获起始状态。第二时序编排简化。一个Scene切换到另一个Scene时框架会一次性处理所有需要动画的View你再也不用对着N个Animator做playTogether。第三代码量戏剧性下降。之前的“列表卡片变详情”需求我后来用transitonToScene重写去掉布局改动后核心动画代码只剩不到20行。更重要的是新增一个动画属性时框架会自动对比而不是要你去改AnimatorSet。1.3 它与其他Android动画方案的边界划分很多人会把Transition Framework和View动画、属性动画搞混这里我直接给一个边界参考方案适用场景局限View Animation / Alpha、Translate简单临时效果不常驻不改变实际布局位置点击区域不跟随移动Property Animator (ObjectAnimator/AnimatorSet)需要精细控制单个View或少数View状态管理全在自己手里场景复杂时代码量大Transition FrameworkView树整体状态切换比如一组View位置/尺寸/可见性变化对复杂自定义View的很多属性不自动识别需要自定义MotionLayoutConstraintLayout约束条件下的复杂路径动画需要全部子View挂在ConstraintLayout下老项目改造重Transition并不是万能的。它不擅长做无限循环的动画也不擅长控制单个View的持续旋转。它擅长的是一组View在两种布局状态之间平滑过渡。理解这点你才知道什么时候该用它。比如一个动态表单页面的字段展示形态切换、一个购物车的折叠展开、一个主页框架的底部Tab切换这些都是Transition的典型主场。2. Scene、Transition、TransitionManager三者的机制拆解2.1 Scene一个轻量级View树快照Scene的本意是场景。你可以把Scene理解为“某个容器View在特定时刻的完整布局状态”。它不持有View树本身而是持有这个View树的根引用或layout资源。Transition框架在动画开始前通过Scene.getSceneForLayout(sceneRoot, layoutRes, this)创建目标Scene这个layoutRes就是场景要切换成的布局。这里有个很多人搞错的点sceneRoot不是Scene本身而是两个Scene共用的父容器。Transition之所以能在两个Scene之间过渡是因为FrameLayoutsceneRoot内部的子View在切换前后对应关系是可追踪的。如果你创建Scene时用的layoutRes中有和当前View树完全不同的ViewId集合框架也能工作但那些找不到对应起始值的View就只会直接出现或消失不会有动画。换句话说Scene之间能否有“动效承接”取决于是否需要保留同id的View作为共同锚点。2.2 TransitionManager的两种启动方式go()与beginDelayedTransition()TransitionManager提供两个入口使用场景完全不同。TransitionManager.go(scene, transition)会先离开当前Scene再进入目标Scene在进入过程中执行Transition常见场景是Activity/Fragment内部整块内容切换。而TransitionManager.beginDelayedTransition(sceneRoot, transition)不会切换Scene只是预注册一次过渡。你用Java代码对sceneRoot内的View做属性修改或添加删除运行到消息循环末尾时框架对比修改前后的View树状态自动执行Transition。后者我日常用得最多。比如RecyclerView里点击“展开”按钮使得一个Item内多出一个TextView外部其他Item高度变化用beginDelayedTransition就能让添加View和高度变化都带动画。它本质上是利用了View树消息队列机制把“延迟捕获起始状态”和“下一次布局时捕获结束状态”串起来。这里的重点是修改View属性的操作必须发生在beginDelayedTransition调用之后否则框架捕获到的start和end可能一致动画自然就不出现。2.3 Transition内部的生命周期capture → play → animate一个Transition跑起来有三个阶段。首先captureStartValues(values)在执行开始时遍历所有满足条件的target将每个View需要追踪的属性写入TransitionValues。然后在布局结束发生变化后captureEndValues(values)再捕获一次结束状态。接着框架会两两比对找出同一View在start和end中属性值的差异创建Animator并存入过渡数组。最后阶段把这些Animator交给一个AnimatorSet统一播放同时通过setDuration、setInterpolator、setPropagation控制整体节奏。理解这个生命周期对自定义Transition至关重要。因为框架只认识它内置的Fade、ChangeBounds、ChangeTransform等属性对你自定义View的属性一无所知。你需要做的就是在captureStartValues和captureEndValues里把自定义属性的起始结束值存进TransitionValues.values再在createAnimator里读取并构建ObjectAnimator。我自己在做进度条颜色变化时就这么干的后面第四部分会给出完整代码。2.4 Target过滤为什么默认scope的真实含义不能忽略Transition默认作用于容器内的所有View但很多时候我们只想让部分View参与动画。这时可以用addTarget(view)、excludeTarget(view, true)来做白名单或黑名单。这里有个隐蔽坑excludeTarget如果你传的是View对象它只排除这个View本身如果传的是View类类型它会排除该类的所有实例。我遇到过排查半天都不生效的情况因为列表场景里误用excludeTarget(R.id.someId, true)但那个View后来被RecyclerView复用成了另一个对象目标ID其实已经变了。还有一点容易被忽略excludeTarget并不会阻止框架对该View的start/end值捕获它只是不生成Animator。所以如果你在自定义Transition里遍历全部View被排除的View的TransitionValues仍然会被填充。这时候你就得自己在createAnimator里根据transitionValues.view.getId()做二次判断不要以为框架帮你过滤了。3. 从静态界面到动态场景一个选项卡切换的完整落地3.1 两个Scene布局的设计要点我用一个实际例子来演示底部Tab切换两个内容页。场景甲是“查询列表页”场景乙是“快捷入口页”——两块大按钮加一条banner。两块内容挂在一个FrameLayout容器下注意这个容器id保持不变比如id/root_container。设计Scene布局时有个原则如果要让两个页面的同名View之间产生“跑动”效果需要在两边布局里保留同id的View并且这个View的类型、层级位置尽量稳定。比如我希望“快捷入口页”的标题能从列表页顶部搜索框“飞”过来那就在列表页里给搜索框加idid/entry_title在入口页也给标题TextView用id/entry_title这样Transition才能认出它们是同一个View的两种状态。如果不保留同id也没关系只是那个View会单独做一个Fade不会有位移效果。3.2 用TransitionSet把Fade、ChangeBounds和ChangeBackground串起来一组View在切换时往往既有位置尺寸变化又有透明度变化还有背景色变化。我不会去手动分开写而是直接构建TransitionSetTransitionSet set new TransitionSet(); set.addTransition(new ChangeBounds()); set.addTransition(new Fade(Fade.OUT)); set.addTransition(new Fade(Fade.IN)); set.addTransition(new ChangeBackground()); set.setOrdering(TransitionSet.ORDERING_TOGETHER); set.setDuration(300);这里ChangeBounds负责捕捉View的left、top、right、bottom变化Fade负责旧View淡出新View淡入ChangeBackground则平滑过渡背景色。ORDERING_TOGETHER是让所有动画同时运行如果你希望先淡出再淡入可以改成ORDERING_SEQUENTIAL。多数界面切换我更喜欢同时进行因为Android社区的MaterialMotion建议里过渡转场应当有一个CrossFade的窗口期同时进行不会有断层感。如果你的切换涉及文字内容变化还可以加入ChangeText来平滑处理文本颜色、大小变化。3.3 通过Tab切换的两个Scene接下来在Tab响应里调用TransitionManager.go即可Scene sceneB Scene.getSceneForLayout( rootContainer, R.layout.scene_quick_entry, this ); TransitionManager.go(sceneB, transitionSet);实测下来这几十行代码已经能覆盖绝大多数Tab切换动画。要注意的是Scene.getSceneForLayout在每次调用时都会重新inflate布局如果你频繁切换Scene对象建议复用不然会有布局实例化的开销特别在低端机上帧率会掉。复用方式是把两个Scene都缓存成成员变量切换时只换transition引用。我习惯里还有一个小细节如果目标场景里有需要网络加载的图片我会先占位再切换这样过渡动画开始后Bitmap到位时正好有淡入效果视觉上很自然。3.4 运行效果与常见小问题这个方案在真机上能明显看到列表页淡出、入口页淡入、同id标题View平滑位移的合成效果。但有几个现象我需要提前说两个页面切换时高宽变化较大的View动画过程中子View文本会有一瞬间“跳变”这是因为文本测量在ChangeBounds之后才触发。解决方案是在目标View上预先设置一个固定宽高或者改用配合ChangeText。背景色变化要配合ChangeBackground但如果你用了shape drawable作为背景很多时候drawable并不是ColorDrawableChangeBackground无法生成渐变只会硬切。要渐变就只能把背景抽成纯色或者自定义BackgroundTransition。如果你的页面里有EditText切换Scene时焦点和软键盘状态会被打断。建议在切换前判断软键盘是否打开若打开就先收起软键盘再执行Transition不然动画会像被卡住一样顿一下。4. 自定义Transition让过渡动画真正贴合业务4.1 自定义Transition的四个重写点如果你已经用内置Transition覆盖了80%场景剩下20%就需要自定义了。比如一个带圆角进度条的自定义View在状态切换时进度值要从20变化到80同时进度条颜色要变内置Transition完全不会处理。自定义Transition通常重写三个方法核心链路如下public class ProgressTransition extends Transition { private static final String PROP_PROGRESS progress; private static final String PROP_COLOR color; Override public void captureStartValues(TransitionValues transitionValues) { MyProgressView view (MyProgressView) transitionValues.view; transitionValues.values.put(PROP_PROGRESS, view.getProgress()); transitionValues.values.put(PROP_COLOR, view.getProgressColor()); } Override public void captureEndValues(TransitionValues transitionValues) { MyProgressView view (MyProgressView) transitionValues.view; transitionValues.values.put(PROP_PROGRESS, view.getProgress()); transitionValues.values.put(PROP_COLOR, view.getProgressColor()); } Override public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { if (startValues null || endValues null) return null; ObjectAnimator progressAnimator ObjectAnimator.ofInt( endValues.view, progress, (int) startValues.values.get(PROP_PROGRESS), (int) endValues.values.get(PROP_PROGRESS)); ObjectAnimator colorAnimator ObjectAnimator.ofArgb( endValues.view, progressColor, (int) startValues.values.get(PROP_COLOR), (int) endValues.values.get(PROP_COLOR)); AnimatorSet set new AnimatorSet(); set.playTogether(progressAnimator, colorAnimator); return set; } }用的时候和内置Transition一样加入TransitionSet即可。但要注意captureStartValues里View的读取必须在动画开始前完成如果你在调用beginDelayedTransition之后又改了progress捕获到的start值就可能是你改后的值动画会失效。自定义Transition里最容易被忽略的是对startValues null的判断因为有可能在动画开始时目标View刚被添加进布局start状态缺失这时候就不能盲目取start值否则会抛空指针。4.2 用PathTransition实现图片沿曲线移动如果希望View沿贝塞尔曲线移动而ChangeBounds只会走直线你可以考虑自定义Transition重写createAnimator在里面用ObjectAnimator.ofObject配合PathEvaluator来实现。我项目里做过一个“购物车吸附”效果底部弹出的商品卡片点击加入购物车时一个小圆点按弧线飞到购物车图标上。关键代码其实不复杂但需要注意View.X和View.Y是两个独立属性如果只对X或Y做动画就会变成水平或垂直移动。想要沿Path移动可以让ObjectAnimator同时绑定一个自定义属性在setValue里同时更新view.setX()和view.setY()或者用Keyframe把X、Y拼成一个PropertyValuesHolderPath path new Path(); path.moveTo(startX, startY); path.quadTo(controlX, controlY, endX, endY); ObjectAnimator animator ObjectAnimator.ofObject( targetView, new PathProperty(path), new PathEvaluator(), path );中间的控制点坐标我建议取起点和终点Y坐标的最小值再减一段距离这样曲线会有一个向上的弧度吸附感很强。这个效果用AnimatorSet也能做但多了个自定义Transition后它就能轻松嵌入到其它TransitionSet里与其他View动画同时触发比如同时让商品卡片的其他文字淡出、按钮缩小整体节奏统一。4.3 当协调布局(Banner滚动)遇上Transition如何联动老有人在热搜里找“android中协调布局banner”其实这里有一个真实场景CoordinatorLayout里一个Banner视图在折叠或展开Toolbar时需要有缩放、平移的联动而Banner本身又是ViewPager。如果你直接在Banner的外层容器上用TransitionManager加动画会跟CoordinatorLayout原有的嵌套滚动机制抢坐标现象是Banner要么抽搐要么跳位置。我的解法是不要对Banner的根容器做ChangeBounds改成对Banner内部一个背景图层做自定义Transition。把Banner高度变化由CoordinatorLayout的AppBarLayout偏移量驱动而不是由Transition驱动Transition只负责背景画层和标题文字的回弹效果。这样各司其职既保留Banner轮播悬停效果也不冲突。总结起来就是遇到协调布局时先判断“这个变化本来就是滚动驱动的吗”如果是就别再用Transition包外层而是让Transition做滚动手势之外的视觉增强。5. 生产环境避坑清单来自线上项目的实战教训5.1 Fragment转场与Transition的冲突项目里经常同时使用Fragment的setCustomAnimations和TransitionManager。有一阵我只要在FragmentTransaction里设置了自定义动画同时又在Fragment的根View上执行Transition就会出现动画结束后View半透明残留。原因其实很简单Fragment进入动画比如Slide会临时改变根View的Alpha而Transition捕获了改变后的Alpha作为start/end两者叠加导致最终Alpha没有复位。解决方案是二选一如果Fragment已经用了setCustomAnimationsFragment内部就不要再调用beginDelayedTransition如果一定要用请确保在Fragment动画结束的onAnimationEnd之后再进行Transition操作或者把Fragment动画彻底移除全部交给TransitionManager统一调度。我把这个规则写进了团队的编码规范里现在很少再出现鬼打墙一样的透明度残留。5.2 状态保存后startValues丢失问题在Activity被系统回收重建后TransitionValues会丢失。表现在代码里就是TransitionValues.values里拿到的start/end全是nullcreateAnimator直接返回null页面切换退化成没有动画。这是因为TransitionValues并没有实现Parcelable它依赖的是进程内对象实例。处理办法是在Activity的onSaveInstanceState里手动记录需要动画的View的状态重建后恢复View属性再启动Transition。对关键动画还可以把“本次动画是否允许执行”标记也保存下来万一上次动画没播完就被回收重建后就不强制补播避免视觉上的突兀跳跃。这个方法我用了很多年稳定度很高。5.3 RecyclerView增量更新中的过渡动画陷阱AutoTransition在RecyclerView的Item增删上看起来很顺但真正生产时我踩过很深的坑notifyItemInserted之后如果同时刷新了Item内部数据Transition会捕获到“新数据渲染后”的start状态导致新增Item的入场动画实际上从最终状态开始播视觉上只是闪一下。所以如果你的Item增删伴随数据变更新请分两步走先notifyItemChanged让布局稳定再延迟几十毫秒notifyItemRemoved/Inserted或者干脆用notifyDataSetChanged配合requestLayout延迟触发Transition。另外Item里的图片加载完再触发Transition是常见需求但Transition的start捕获发生在消息队列尾部如果你在图片加载回调里才开始修改布局一定要把beginDelayedTransition放在修改前的那个调用栈里否则捕获不到正确的起始状态。5.4 大区域动画性能与硬件加速Transition会把所有同时变化的View动画交给一个AnimatorSet统一播放。如果视图层级很深或者一张大图在做ChangeBounds你会发现低端机上动画掉帧甚至花屏。这不是框架bug而是硬件图层没有配合。解决方案是给变化的目标View在动画开始前开setLayerType(View.LAYER_TYPE_HARDWARE, null)在动画结束回调里恢复为LAYER_TYPE_NONE。这样View会被GPU合成到一个离屏缓冲中移动缩放时不再每帧触发整棵View树重绘。我做过一次性能对比同一个包含1280x720大图的过渡场景加硬件加速后帧时间从45ms下降到20ms。要注意的是开硬件图层并不是越多越好图层本身有内存开销并且不要在动画没结束时提前开第二个图层。一个容易犯的错是在RecyclerView滑动过程中对Item开硬件层那会导致部分Item花屏因为这些Item复用时硬件层没有及时释放。5.5 兼容性适配API 19与低端机降级android.transition在API 19可用但Activity转场相关API到21才完整。如果你的minSdkVersion是19建议做一套判断在Build.VERSION.SDK_INT 21时使用Activity过渡否则直接静默降级为普通切换。兼容层代码其实不多主要是window.setEnterTransition等API需要反射或V7包去兼容。如果你的项目已经使用AndroidX可以关注androidx.transition库它在旧版系统上也提供了较为一致的接口不过它内部实现是基于support library的LayoutTransition不是系统原生的transition两者细节有差异选型时要把团队维护成本也算进去。我见过不少项目切了AndroidX后以为自动解决了兼容结果发现Fragment过渡动画的时序和原生不同反而多填了一堆TODO。最后分享一点实际体会。用了很久Transition Framework之后我最惊喜的是它和View Hierarchy的天然绑定你可以把任意两个状态告诉它剩下的交给框架。想入手的读者建议从beginDelayedTransition开始仅用它做列表内单个Item的扩展/收起你会立刻感受到声明式动画和命令式动画的差别。如果非要给一个学习路径那就是先吃透Scene和TransitionManager再啃自定义Transition最后再来细节调优。这些坑都是我一行行代码踩出来的希望能帮你把这个框架真正用得顺、用出效果。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑