☰
Android Transition动画框架详解:从Scene到共享元素的完整实践
2026/10/3 14:30:18 网站建设 项目流程

做Android开发这几年,凡是涉及界面切换、视图增删、列表item变化的动画,我几乎都绕不开Transition框架。android.transition.framework在官方包android.transition.*里,从API 19开始成为系统级动画方案,本质是"拍两张状态图,把差异变成动画"。这套东西学透了,Activity共享元素过渡、场景布局切换、RecyclerView item动画都能做得既流畅又干净,不需要手写一堆属性动画回调。

这个框架解决的痛点很实际:传统animate()+AnimatorSet的方式,写两三个视图的联动的确不难,但一旦涉及"整个布局从状态A变到状态B",比如首页两套筛选条件切换、搜索页收起展开、列表项展开收起,手动计算每个视图起始值和结束值,改一个需求就要重写一遍。Transition框架把"记录前后状态"这件事抽象成了场景,把"自动推算每个视图动画"做成了内置能力,开发者只告诉它"从哪变到哪",细节全部交给框架。

这篇文章写给两类人:一类是刚接触android.transition,想搞懂它到底怎么用、什么时候用、为什么能省事;另一类是已经写过几个过渡动画,但遇到"共享元素怎么传参数""过渡动画怎么被打断""为什么Fade后布局错位"这类问题的人。我会从核心概念讲起,再到四类典型动画的实测细节,最后给一套可直接抄的工程化方案和排错清单。

1. 过渡框架的核心概念:场景、TransitionManager与Transition

1.1 场景Scene是什么

Transition框架里最容易被忽视、但最该先理解的概念是Scene。它代表"视图层级在某一个时间点的完整快照",由一个根视图和一组子视图构成。拿首页的筛选面板举例,"折叠状态"是一个Scene,"展开状态"是另一个Scene。框架要做的事情,就是让用户在两个Scene之间切换时,自动给发生变化的视图生成动画。

创建Scene有两种常用方式:

// 方式一:代码构建 ViewGroup root = findViewById(R.id.container); Scene sceneNormal = new Scene(root, R.layout.scene_normal); Scene sceneExpanded = new Scene(root, R.layout.scene_expanded);
<!-- 方式二:通过merge标签定义场景布局 --> <merge xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 场景内的具体视图 --> </merge>

创建完Scene之后,真正触发切换的是TransitionManager.go():

TransitionManager.go(sceneExpanded, new AutoTransition());

这行代码背后发生的事比表面看起来多得多。框架会先记录当前视图状态的起始值,然后执行场景内容切换,再在下一帧记录结束值,最后对所有发生过变化的属性生成Animator并同步启动。注意,这里不需要你手动调用任何setVisibility(VISIBLE)或setTranslationY(),框架会把Fade、Move、Resize拆解成对应动画。

Scene里有一个关键点:根视图必须是同一个(container一致),否则框架无法做前后对比。如果你在代码里不小心 new 了两个不同ViewGroup作为根,TransitionManager 会直接忽略,甚至抛出异常。这是我踩过最基础的坑。

1.2 TransitionManager与延迟执行

TransitionManager.go()的同步调用会立刻执行场景切换,但也有场景需要动画延迟启动,比如等待图片加载完成、等待键盘收起、或者等网络数据返回后再进入新状态。这时候可以用TransitionManager.beginDelayedTransition():

TransitionManager.beginDelayedTransition(mContainer, new AutoTransition()); // 在这里修改视图状态,比如addView、removeView、改变布局参数

beginDelayedTransition()和go()的本质差异在于:go()是用新Scene替换旧状态,而beginDelayedTransition()是"拍一张当前状态的照片,然后等你手动改界面,再拍第二张照片,最后自动对比"。它不需要预定义Scene,非常适合动态数据驱动的界面更新。

一个典型场景是购物车加购后的角标动画。我先拍快照,再往容器里塞入一个"已加购"标签,框架自动给标签生成Fade和Move动画,不需要关心它一开始在哪、后来到哪。多个视图同时变化时,这套机制尤其省心。

TransitionManager内部还会维护一个延迟事务队列,同一时间只能有一个正在进行的transition。如果你在动画还没结束时又调用了一次go(),框架会终止前一个动画,直接从当前状态生成新动画。这个行为既能避免动画抖动,也容易造成"动画突然跳变"的错觉,后面我会专门讲如何处理。

1.3 Transition的六大系统实现

Transition类是整个框架的执行单元,负责"知道哪些属性变了、怎么生成动画"。系统提供了一批现成实现,我按使用频率列一下:

Transition类监听属性典型用途
Fadevisibility、alpha淡入淡出,进场/退场最常用
Slidevisibility、translation滑动进出场
Explodevisibility、translation以中心为原点放射状进出场
ChangeBounds视图left/top/width/height位置和尺寸变化
ChangeTransformrotation/scaleX/scaleY缩放旋转变化
ChangeImageTransformMatrix、bounds图片矩阵变换,例如头像放大缩小
AutoTransition以上全部默认组合,进场+Fade、退场+Fade、变化+ChangeBounds

需要注意一点:AutoTransition的好用也带来一个隐患。它内部是TransitionSet,顺序是"先退场动画"再"变更动画"最后"进场动画",而且changeBounds是默认加入的。如果你的布局里有宽高变化的视图,但没指望它做位移动画,AutoTransition的默认行为可能会让你感觉"布局自己会飘",这时就应该显式指定你需要的Transition,而不是无脑用AutoTransition。

2. 四类常用Transition的实测与适配细节

2.1 Visibility类:Fade、Slide、Explode的取舍

Visibility类Transition处理的是"视图从GONE变VISIBLE"或"VISIBLE变GONE"的过程。这三个类都继承自Visibility,但动画路径完全不一样:

  • Fade:透明度变化,线性、克制,适合强调内容本身的场景。
  • Slide:视图沿边缘滑入滑出,默认从底部滑入(Slide的默认edge是Gravity.BOTTOM)。你可以在构造时指定起始边缘。
  • Explode:视图从容器边缘向中心聚合,或从中心向外爆发。视觉冲击力强,适合数据层级的"刷出"效果。

实测数据上,Slide的运动曲线默认是FastOutSlowInInterpolator,Fade的默认时长是300ms,Explode也是300ms。这个时长在低端机上会稍微卡顿,建议在RecyclerView列表批量item切换时手动改短到200ms。

使用Visibility类时有一个经常被忽略的参数:setVisibility()自动处理了,但如果你在代码里先手动setVisibility(VISIBLE),再调用TransitionManager.beginDelayedTransition(),框架检测不到 "状态变化" 的起点,动画会失效。正确做法永远是先记录Transition,再改VISIBILITY。

TransitionManager.beginDelayedTransition(container, new Fade()); view.setVisibility(View.VISIBLE); view.setAlpha(1f);

注意顺序别反了。反了之后Transition会拿到两个相同的visibility快照,直接什么都不做。

2.2 ChangeBounds、ChangeTransform、ChangeImageTransform

这三类是"变更类Transition",负责处理视图几何形状变化。ChangeBounds监听的是 left、top、width、height,也就是说凡是通过updateLayoutParams、setFrame、setLayoutParams造成的尺寸位置变化,它都能捕获。ChangeTransform监听的是 rotation、scaleX、scaleY,通常用在卡片缩放、按钮按压态反馈上。ChangeImageTransform专门处理ImageView的scaleType变化,例如从CENTER_CROP变成FIT_CENTER,它会在底层用Matrix插值,避免图片跳变。

实际项目中最多的是ChangeBounds和ChangeTransform组合使用,做成"卡片点击展开的平滑效果"。我建议在组合时给每个子Transition单独设置插值器,否则默认的FastOutSlowIn在不同设备上表现差异很大。

TransitionSet cardChange = new TransitionSet(); cardChange.addTransition(new ChangeBounds().setDuration(300)); cardChange.addTransition(new ChangeTransform().setDuration(200)); cardChange.setOrdering(TransitionSet.ORDERING_TOGETHER); cardChange.setInterpolator(new DecelerateInterpolator());

ORDERING_TOGETHER表示所有子Transition并行执行,ORDERING_SEQUENTIAL则是顺序执行。列表展开场景里并行会让整体节奏更快,但容易让两个Transition同时修改同一个属性造成冲突,所以自定义组合时最好遵循"一个Transition只改一类属性"的原则。

2.3 TransitionSet与AutoTransition的完整拆解

AutoTransition虽然是系统默认,但它在源码里本质是长这样的:

public class AutoTransition extends TransitionSet { public AutoTransition() { setOrdering(ORDERING_SEQUENTIAL); addTransition(new Fade(Fade.OUT)).addTransition(new ChangeBounds()); addTransition(new Fade(Fade.IN)); } }

顺序执行意味着先退场再移动再进场。如果子Transition里没有显式设置时长,它们都会继承TransitionSet的时长(默认300ms)。如果设置了,就会各按各的。这里有一个中级开发者容易踩的坑:给AutoTransition设置了setDuration(600),但子TransitionFade自己也有300ms的默认时长,最后执行时两个时长不统一,导致动画错拍。所以给TransitionSet统一设置duration时,最好把子Transition的时长一起设置,或者干脆用setDuration(0)让子类继承。

TransitionSet支持通过XML声明,也支持代码创建。我在动态场景控制需求多的时候更偏向代码,因为可以灵活做条件判断,而XML方式适合那些"整个页面的默认过渡风格"。

3. 实战:Activity与Fragment过渡、共享元素与自定义Transition

3.1 Activity共享元素过渡完整流程

共享元素过渡是Transition框架最出彩的能力,它让用户感觉两个页面的同一个图片/标题是"飞过去"的。系统提供的目标是Activity或Fragment进入/退出时的共享元素映射,核心是setSharedElementEnterTransition()和setSharedElementReturnTransition()。

基础用法三步走:

// 第一个页面 startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, Pair.create(coverImage, "cover"), Pair.create(titleView, "title")).toBundle());
// 第二个页面 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS); getWindow().setSharedElementEnterTransition(new ChangeImageTransform().setDuration(300));

两个页面之间通过transitionName建立配对:

<ImageView android:transitionName="cover" ... />

共享元素过渡的关键点在于transitionName必须完全一致,否则无法匹配。注意Activity之间共享元素动画执行时有两次:进入时执行 enter,返回时执行 return,如果你的返回动画没设置,默认会使用 自动Transition,表现不如预期。我给这套流程加了返回值缓存方案:第二个页面onCreate里setSharedElementReturnTransition()的Transition要和enter呈现镜像效果,否则返回时会出现缩略图跳动。

还有一个细节:如果第二个页面的图片和第一个页面的尺寸差异比较大,默认ChangeBounds会让View边界跳变,这时候一定要换成ChangeImageTransform。它会把ImageView的Matrix也纳入动画,让图片内容随边界平滑变化。

3.2 Fragment场景切换与透传

Fragment的Transition体系略有不同,它依赖FragmentTransaction.setCustomAnimations(),但同时也支持Transition框架中的内容过渡。从API 21开始,Fragment有两个关键方法:setEnterTransition()和setExitTransition()。例如:

fragment.setEnterTransition(new Fade().setDuration(300));

Fragment切换推荐使用setReorderingAllowed(true),它让Fragment系统在进入/退出动画时做延迟处理。不设置的话,Transition的"退出"和"进入"会同时在一个容器上执行,容易互相干扰。

实际项目里我会在Fragment基类里做默认过渡:

public abstract class BaseFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setEnterTransition(new Slide(Gravity.END).setDuration(250)); setExitTransition(new Slide(Gravity.START).setDuration(250)); setReorderingAllowed(true); } }

这样所有子Fragment自动获得滑动切换效果,业务代码不用重复写。拿搜索页举例,从底部滑入的结果页Fragment自带从右往左的进场动画,用户体验接近iOS的导航栈。

3.3 自定义Transition:捕获属性与生成Animator

如果系统Transition不满足业务需求,比如要做一个"文字逐字符替换""背景色渐入",就得写自定义Transition。灵魂在三层:

public class ColorChangeTransition extends Transition { private static final String PROPERTY_BACKGROUND = "colorTransition:bg"; @Override public void captureStartValues(TransitionValues values) { values.values.put(PROPERTY_BACKGROUND, ((View) values.view).getBackground()); } @Override public void captureEndValues(TransitionValues values) { values.values.put(PROPERTY_BACKGROUND, ((View) values.view).getBackground()); } @Override public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { ColorDrawable startBg = (ColorDrawable) startValues.values.get(PROPERTY_BACKGROUND); ColorDrawable endBg = (ColorDrawable) endValues.values.get(PROPERTY_BACKGROUND); // 用ArgbEvaluator生成colorAnimator } }

三层缺一不可:captureStartValues记录开始状态,captureEndValues记录结束状态,createAnimator在两者不同时生成Animator。注意TransitionValues里的key是全局日志,统一放在一个常量为前缀比较安全。

还有一个容易被忽略的细节:createAnimator()里startValues或endValues可能为null,说明视图是新增或移除的,这时候必须做空判断返回null或只生成alpha动画,否则一进场直接崩。

4. 常见问题与性能陷阱排查实录

4.1 Transition不起作用的五个原因

我整理了一份排错速查表,按发生频率从高到低排:

现象可能原因解决方案
go()被调用后没有动画容器根视图不一致,或切换前后视图状态没变化检查两个Scene的root是否为同一个ViewGroup,注意不能是不同layout id
Fade动画失效先setVisibility又调beginDelayedTransition在beginDelayedTransition之后再setVisibility
ChangeBounds无效ViewGroup设置了clipChildren=false但parent没有允许布局动画确认父容器和上一级容器都开启clipChildren false,必要时用requestLayout
Transition时长不生效子Transition自带duration,覆盖了setDuration给每个子Transition显示设置同值duration
动画过程中视图闪烁未调用Scene.getSceneRoot().setLayoutTransition(null)配合检查是否有旧版LayoutTransition在同时运行,调setLayoutTransition(null)

另外比较常见的是RecyclerView与Adapter动画冲突。如果你在Item上调用beginDelayedTransition,同时NotifyDataSetChanged,会触发RecyclerView自身的ItemAnimator,两个系统同时改可见性,结果是动画互相取消。这时候可以关闭默认ItemAnimator:

recyclerView.setItemAnimator(null);

让Transition框架统一接管。

4.2 配合ConstraintLayout实现批量动画

androidx.constraintlayout里的ConstraintSet和Transition框架天生合拍。ConstraintSet负责描述整套约束关系,例如筛选面板里每个筛选按钮的间距、内边距、链结构,通过TransitionManager.beginDelayedTransition可以轻松实现整组视图的位移动画:

ConstraintSet newConstraint = new ConstraintSet(); newConstraint.clone(context, R.layout.item_filter_expanded); TransitionManager.beginDelayedTransition(root, new ChangeBounds().setDuration(300)); newConstraint.applyTo(root);

这段代码是"先拍快照,再更新ConstraintSet,再应用"。框架会遍历容器内所有View,自动为位移、尺寸、alpha变化生成动画,比你手动监听每个控件的更新回调要省事得多。

用这种方式做筛选面板伸缩时,一定要给容器设置固定宽高,避免ConstraintSet先改变自身尺寸导致连锁位移。如果容器宽高也需要变化,就给容器本身也分配一个transitionName,并确保它在前后两个ConstraintSet里都能被正确约束。

4.3 低端机性能优化与生命周期注意点

Transition框架本质是属性动画的组合,性能瓶颈主要在截图和动画执行两个阶段。低端机上两个大坑:

  • 动画期间过度绘制严重,特别是ChangeBounds会触发多次layout pass。解决方法是把要动画的View放在一个专用的独立ViewGroup里,并开启setLayerType(View.LAYER_TYPE_HARDWARE, null)。动画结束后再切回LAYER_TYPE_NONE。
  • 列表滚动时不建议执行Transition。RecyclerView的回收机制会让Transition捕获到错误的起始状态,造成列表闪烁。我会在ListView或RecyclerView滚动回调里加一个isScrolling标志,在滚动时抑制beginDelayedTransition。

生命周期方面,Activity onStop之后执行Transitions容易引发内存泄漏或NoSuchMethodError。保险做法是在onPause()时调用TransitionManager.endTransitions(sceneRoot)结束当前所有Transition,或者干脆在onStop()里清除监听器。

Last but not least,我在真实项目里的经验是:不要滥用。Transition框架很强,但它适合"页面级或容器级的整体形态变化",不适合高频的滚动手势动画。那些高频场景,用animate()+MotionLayout反而更可控。像我团队里约定俗成的标准是:列表项动态增减用ItemAnimator,整体筛选面板用Transition框架,手势跟手用MotionLayout,各司其职才不至于让工程复杂度失控。

最后再分享一个小技巧:调试Transition时,把Transition的时长调成3000ms会特别直观,你可以清楚看到每个阶段谁在移动、谁在淡出、哪个属性先变了。我每次排查动画错拍,第一件事就是把时长拉长,慢放以后问题原因基本一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询