ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android Transition动画框架详解:从Scene到共享元素的完整实践

Android Transition动画框架详解:从Scene到共享元素的完整实践 做Android开发这几年凡是涉及界面切换、视图增删、列表item变化的动画我几乎都绕不开Transition框架。android.transition.framework在官方包android.transition.*里从API 19开始成为系统级动画方案本质是拍两张状态图把差异变成动画。这套东西学透了Activity共享元素过渡、场景布局切换、RecyclerView item动画都能做得既流畅又干净不需要手写一堆属性动画回调。这个框架解决的痛点很实际传统animate()AnimatorSet的方式写两三个视图的联动的确不难但一旦涉及整个布局从状态A变到状态B比如首页两套筛选条件切换、搜索页收起展开、列表项展开收起手动计算每个视图起始值和结束值改一个需求就要重写一遍。Transition框架把记录前后状态这件事抽象成了场景把自动推算每个视图动画做成了内置能力开发者只告诉它从哪变到哪细节全部交给框架。这篇文章写给两类人一类是刚接触android.transition想搞懂它到底怎么用、什么时候用、为什么能省事另一类是已经写过几个过渡动画但遇到共享元素怎么传参数过渡动画怎么被打断为什么Fade后布局错位这类问题的人。我会从核心概念讲起再到四类典型动画的实测细节最后给一套可直接抄的工程化方案和排错清单。1. 过渡框架的核心概念场景、TransitionManager与Transition1.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:androidhttp://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的运动曲线默认是FastOutSlowInInterpolatorFade的默认时长是300msExplode也是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过渡、共享元素与自定义Transition3.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:transitionNamecover ... /共享元素过渡的关键点在于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 idFade动画失效先setVisibility又调beginDelayedTransition在beginDelayedTransition之后再setVisibilityChangeBounds无效ViewGroup设置了clipChildrenfalse但parent没有允许布局动画确认父容器和上一级容器都开启clipChildren false必要时用requestLayoutTransition时长不生效子Transition自带duration覆盖了setDuration给每个子Transition显示设置同值duration动画过程中视图闪烁未调用Scene.getSceneRoot().setLayoutTransition(null)配合检查是否有旧版LayoutTransition在同时运行调setLayoutTransition(null)另外比较常见的是RecyclerView与Adapter动画冲突。如果你在Item上调用beginDelayedTransition同时NotifyDataSetChanged会触发RecyclerView自身的ItemAnimator两个系统同时改可见性结果是动画互相取消。这时候可以关闭默认ItemAnimatorrecyclerView.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会特别直观你可以清楚看到每个阶段谁在移动、谁在淡出、哪个属性先变了。我每次排查动画错拍第一件事就是把时长拉长慢放以后问题原因基本一目了然。
返回列表