Transition.onTransactionReady的内容比较长,我们挑重点的部分逐段分析(跳过的地方并非不重要,而是我柿子挑软的捏)。
1 窗口绘制状态的流转以及显示SurfaceControl
注意我们这里的SurfaceControl特指的是WindowSurfaceController的mSurfaceControl,如果对这个不是很了解的,可以回顾一下之前写的关于SurfaceControl的文章:
【基础】2、Surface的创建【Android 12】 - 掘金 (juejin.cn)
接着分析代码:
先跳过Transition.commitVisibleActivities,看到首先是将Transition.mState置为STATE_PLAYING,这意味着动画马上就要执行了。
然后是为Transition的两个成员变量mStartTransaction以及mFinishTransaction赋值,mFinishTransaction不用多说,看到mStartTransaction被赋值为传参transaction,传参即我们上一篇分析中的在SyncGroup.finishNow创建的一个Transaction,局部变量merged:
一个“start transaction”和一个“finish transaction”,我按照个人的理解,举个例子说明一下,如果我们从ActivityA上启动了一个ActivityB:
1)、对于ActivityA来说,它相关的SurfaceControl(准确一点说则是WindowSurfaceController.mSurfaceControl)需要在动画结束的时候再隐藏,如果它在动画开始前就隐藏,那么就无法看到ActivityA的动画效果了(向右平移退出或者淡出之类的动画)。
2)、对于ActivityB来说,它相关的SurfaceControl需要在动画开始的时候就显示出来,如果它在动画开始的时候还没有显示,那么同样也无法看到ActivityB的动画效果了(向右平移进入或者淡入之类的动画)。
从以上分析可知,ActivityA和ActivityB相关的SurfaceControl可见性变化的时机是不同的,那么这个行为通过一次Transacton.apply是无法做到的,所以就需要两个Transaction,即“start transaction”和“finish transaction”。“start transaction”在动画开始前调用apply,用于在动画开始执行前提前将ActivityB进行显示,“finish transaction”则是在动画结束的时候调用apply,用于在动画结束的时候再将ActivityA隐藏。
最重要的是要弄清楚“start transaction”和“finish transaction”这两个Transaction调用apply方法的时机,在以后的Transition流程中会分析到。
再来看Transition.commitVisibleActivities方法的内容:
如该方法的注释所说,当前Transition已经准备好执行动画了,这里先让“start transaction”把相关需要显示的SurfaceControl显示出来。
Transition.mParticipants是参与动画的WindowContainer集合,那么这个方法就是遍历这个集合:
1)、调用ActivityRecord.commitVisibility设置相关ActivityRecord的为可见。
2)、调用ActivityRecord.commitFinishDrawing进一步设置相关SurfaceControl为可见。
ActivityRecord.commitVisibility方法内容比较多,主要是用来ActivityRecord的可见性,即其成员变量mVisible,除此之外还有很多别的逻辑,但是和我们要分析的Transition内容无关,只需要知道这里设置了ActivityRecord的可见性即可,不去多说。我们主要看下ActivityRecord.commitFinishDrawing:
很简单,为每一个child,即WindowState调用commitFinishDrawing方法:
1)、调用WindowStateAnimator.commitFinishDrawingLocked方法,继续将窗口对应的WindowStateAnimator的mDrawState,即绘制状态进行流转。
2)、调用WindowStateAnimator.prepareSurfaceLocked,设置SurfaceControl的可见性。
这两个方法都比较重要,我们接下来分别进行分析。
1.1 窗口绘制状态的流转
SurfaceControl最终的显示和窗口的绘制状态密切相关,所以我感觉这里有必要看一下WindowStateAnimator.mDrawState这个状态是如何切换的,并且我自己对这个窗口的绘制状态也是不求甚解,也希望借着这个机会了解一下。
先分析一下代码,回头再试着总结一下。
·1.1.1 WindowStateAnimator.commitFinishDrawingLocked
首先将WindowState.mDrawState设置为READY_TO_SHOW。
然后如果当前WindowStateAnimator相关的WindowState满足以下条件之一,则继续调用WindowStateAnimator.performShowLocked:
1)、没有对应的ActivityRecord,即是一个非Activity窗口:
activity == null
2)、有对应的ActivityRecord,并且此时已经可以显示窗口了:
activity.canShowWindows()
重要是就是看这个ActivityRecord的mSyncState是不是SYNC_STATE_WAITING_FOR_DRAW,如果是这个值,那么就说明这个ActivityRecord是处于动画中的。
但是有一个问题是,ActivityRecord的mSyncState是不会被设置为SYNC_STATE_WAITING_FOR_DRAW的,只有WindowState才会,那岂不是每次走到这里判断ActivityRecord是否drawn,都将一直是true。
3)、是一个TYPE_APPLICATION_STARTING类型的窗口,即SplashScreen或者Snapshot:
mWin.mAttrs.type == TYPE_APPLICATION_STARTING
总而言之,如果这个WindowState满足了绘制了条件,那么将继续调用WindowState.performShowLocked。
1.1.2 WindowState.performShowLocked
如果WindowStateAnimator.mDrawState不是READY_TO_SHOW,那么返回false,否则将其置为HAS_DRAWN,并且返回true,这将使得我们可以下一步继续调用WindowStateAnimator.prepareSurfaces方法。
从WindowStateAnimator.commitFinishDrawingLocked以及WindowState.performShowLocked这两个方法都能看到,窗口的绘制状态是循序渐进的,必须是状态A -> 状态B -> 状态C,不存在状态A直接到状态C之类的。
1.1.3 窗口绘制状态小结
首先是mDrawState在WindowStateAnimator的定义,以及几个取值:
结合着Activity启动的一般流程,我大致总结一下:
1)、NO_SURFACE:当没有Surface的就置为这个状态。
这个很好理解,一般窗口销毁相关的流程,会将WindowStateAnimator.mDrawState设置为NO_SURFACE,比如:
WindowState.removeImmediately
-> WindowStateAnimator.destroySurfaceLocked
-> WindowStateAnimator.destroySurface
此阶段没有窗口,也没有Surface。
2)、DRAW_PENDING:当Surface被创建之后,窗口被添加但还没有开始绘制之前,就会置为这个状态。在这个时期,Surface是隐藏的。这表明Surface正等待应用程序绘制窗口的内容。
当窗口被添加,接着App侧开始走measure、layout以及draw流程,在draw之前,会将窗口在WMS侧进行relayout,经过:
WMS.relayoutWindow
-> WMS.createSurfaceControl
-> WindowStateAnimator.createSurfaceLocked
-> WindowStateAnimator.resetDrawState
会将WindowStateAnimator.mDrawState设置为DRAW_PENDING。
这个流程我们也很熟悉,即之前分析创建WindowSurfaceController的SurfaceControl的流程。
此阶段窗口被添加但还没绘制出来,SurfaceControl也是隐藏的。
3)、COMMIT_DRAW_PENDING:当窗口的绘制操作完成,但是这个Surface还没有显示出来之前,状态会设置为此值。这个Surface会在下次layout过程中显示出来。
当窗口绘制完成,App侧调用ViewRootImpl.reportDrawFinished后,就会调用IWindowSession的对端,经过:
Session.finishDrawing
-> WMS.finishDrawingWindow
-> WindowState.finishDrawing
-> WindowStateAnimator.finishDrawingLocked
会将WindowStateAnimator.mDrawState设置为COMMIT_DRAW_PENDING。
此阶段窗口已经绘制完成,但是Surface由于一些原因还不能显示。
4)、READY_TO_SHOW:这个状态标识窗口的绘制操作已经提交,但Surface还没有真正显示。在一组窗口(例如属于同一个应用的多个窗口)准备显示时,系统会使用这个状态来延迟显示Surface,直到所有相关窗口都准备好一起显示。
首先我们看到在动画的流程中,窗口的绘制状态被设置为READY_TO_SHOW的流程为:
Transition.onTransactionReady
-> Transition.commitVisibleActivities
-> ActivityRecord.commitFinishDrawing
-> WindowState.commitFinishDrawing
-> WindowStateAnimator.commitFinishDrawingLocked
结合注释,我个人的理解是,绘制状态被置为READY_TO_SHOW,表明此窗口已经绘制完了,可以准备显示它的SurfaceControl了,但是它的SurfaceControl需要等待和其它的SurfaceContrl一起显示,或者说等待动画走到特定阶段才能显示,因此我们这里推迟其SurfaceControl的显示时间,将窗口的绘制状态设置为READY_TO_SHOW。
如果不考虑和其它窗口一起显示,那么我想在这一步就可以将绘制状态设置为HAS_DRAWN了,即READY_TO_SHOW这个状态值是不必要的。
5)、HAS_DRAWN:当窗口首次在屏幕上显示时,就会设置为此状态。
这个值在WindowState.performShowLocked方法中被设置,紧跟着WindowStateAnimator.commitFinishDrawingLocked方法。
严谨一点的话注释的说法其实是不准确的,当窗口绘制状态被设置为HAS_DRAWN的时候,只是说明SurfaceControl接下来可以显示了,但是SurfaceControl仍然没有显示,屏幕上是看不见的。
6)、总结一下,从以上分析可知,这些状态值不只涉及了窗口的绘制流程,还涉及了SurfaceControl的显示流程:
- NO_SURFACE:没窗口,也没SurfaceControl。
- DRAW_PENDING:有窗口,但没开始绘制。有SurfaceControl,但不能显示。
- COMMIT_DRAW_PENDING,窗口刚刚绘制完,SurfaceControl还不能显示。
- READY_TO_SHOW:窗口已经绘制完了,SurfaceControl可以显示了,但没必要,再等等。
- HAS_DRAWN:窗口已经绘制完了,SurfaceControl也可以显示了。
1.2 显示SurfaceControl
回到WindowState.commitFinishDrawing,在调用WindowStateAnimator.commitFinishDrawingLocked将窗口的绘制状态走完后,接下来就是调用WindowStateAnimator.prepareSurfaceLocked来显示SurfaceControl了。
注意这个方法被调用的地方有两处:
还有一处调用的地方在WindowState.prepareSurfaces,这个是更通用的流程,但是动画流程下,则稍微不同,即我们分析的这个流程。
看代码:
我们只看和显示SurfaceControl相关的部分:
1)、如果窗口不在屏幕上,则调用WindowStateAnimator.hide -> WindowSurfaceController.hide来隐藏SurfaceControl。
2)、如果窗口在屏幕上,那么进一步判断窗口的绘制状态,只有窗口的绘制状态为HAS_DRAWN,才能继续调用WindowSurfaceController.showRobustly来显示SurfaceControl:
关键的就那一句,调用Transaction.show来显示相关SurfaceControl,但是要注意的是这里并没有调用Transaction.apply,所以这个时候窗口还是没有显示。
窗口的最终显示则是和这个传参Transaction对象有关,这个Transaction对象则是之前说的”start transaction“,那么这个Transaction的apply方法的调用时机则是跟Transition的流程相关,以后的分析会看到。
2 计算动画目标
这一节的内容是调用Transition.calculateTargets来计算动画的目标:
Transition的成员变量mTargets定义为:
之前收集到的动画参与者提升后的最终的动画目标,也就是说最终执行动画的主体并非是之前收集到的动画参与者,而是这一步用动画参与者计算得到的动画目标。
Transition.calculateTargets的内容为:
大致的内容为:
1)、创建一个Transition.Targets类型的局部变量targets,来收集动画目标。
2)、遍历Transition.mParticipants,从Transition.mChanges中取出对应的ChangeInfo对象放到Transition.Targets.mArray中,但是跳过WindowState类型的动画参与者,以及跳过那些根据ChangeInfo.hasChanged得出前后没有发生变化的动画参与者。
3)、调用Transition.tryPromote尝试提升targets中保存的动画目标的级别。
我们这一节主要来看下这个Transition.tryPromote。
”promote“,提升的动画目标在WindowContainer层级结构中的级别,这个逻辑之前在AppTransitionController.getAnimationTargets也用到了,思想都是类似的。比如一个Task中有两个ActivityRecord,并且这两个ActivityRecord要分别执行一段动画,也就是动画执行的主体是ActivityRecord。如果这两个ActivityRecord刚好都想向左平移同样的距离,那么我们就不需要为这两个ActivityRecord分别应用一段平移的动画,而是直接将这个平移的动画应用到它们共同的父容器Task上,并且实现的效果是一样的。这也就是”promote“的含义,动画的目标主体从ActivityRecord”提升“到了更高一级的Task上。
接着看代码,Transition.tryPromote。
2.1 Transition.tryPromote
主要逻辑为遍历Targets.mArray中的每一个ChangeInfo对象,调用Transition.canPromote方法来判断他们是否能够提升为父容器。
1)、如果不能,直接跳过该ChangeInfo对象,判断下一个。
2)、如果能,就说明提升成功。此外还要调用Transition.reportIfNotTop来继续判断它是否是organized(我的理解就是这个WindowContainer是否是系统开机后自动创建的,不是需要的时候再去创建的)。如果不是,那么将当前WindowContainer对应的ChangeInfo从局部变量targets中移除,然后把它的父WindowContainer对应的ChangeInfo加如到targets中。如果是,那么在不移除当前WindowContainer对应的ChangeInfo的前提下,把它的父WindowContainer对应的ChangeInfo加如到targets中。这里应该是针对organized的WindowContainer的特殊处理,确保organized的WindowContainer的变化也能够报告到WMShell那边。
因此重点其实是Transition.canPromote逻辑。
2.2 Transition.canPromote
感觉这段代码还是比较重要的,我们逐行分析。
2.2.1 片段1
1)、对应WindowContainer.canCreateRemoteAnimationTarget方法,目前只有TaskDisplayArea、TaskFragment以及ActivityRecord会返回true,其它类型的WindowContainer都会返回false,也就是说父容器不是这几类的WindowContainer将无法得到提升,那么目前只有这几种提升:WindowState到ActivityRecord,ActivityRecod到TaskFragment,TaskFragment到TaskFragment(因为TaskFragment存在嵌套,比如Home类型的TaskFragment),以及TaskFragment到TaskDisplayArea。另外从Transition.calculateTargets的逻辑我们看到了执行动画的target至少是WindowToken这一级的,并且看收集的逻辑,似乎也没有看到过直接收集WindowState的,因此实际上提升只存在以下几种情况:
- ActivityRecod到TaskFragment。
- TaskFragment到TaskFragment。
- TaskFragment到TaskDisplayArea。
2)、如果找不到父WindowContainer对应的ChangeInfo,则不提升,返回false。
3)、如果父WindowContainer有ChangeInfo,但是此时的状态和收集开始时的状态没有变化,则不提升,返回false。
2.2.2 片段2
1)、如果当前要提升的WindowContainer是Wallpaper类型的,则不提升,返回false。
2)、如果当前WindowContainer前后的父WindowContainer不一致,即发生reparent了,则不提升,返回false。
2.2.3 片段3
遍历父WindowContainer的所有子WindowContainer:
1)、如果姊妹WindowContainer在Transition.mChanges中找不到一个对应的ChangeInfo对象,或者有这么一个ChangeInfo对象,但是该ChangeInfo对象不在Targets.mArray中,这种情况一共可以理解为这个姊妹WindowContainer没有参与到本次动画,那么还需要继续判断:
----1.1)、如果该姊妹WindowContainer可见,那么就不提升,直接返回false,当前WindowContainer无法提升到父WindowContainer。毕竟该姊妹WindowContainer是没有参与到动画中的,并且是可见的,如果你提升了,那后续动画执行的时候用户不是会看到该姊妹WindowContainer跟着一起动了嘛,这肯定是不对的。
----1.2)、如果该姊妹WindowContainer不可见,那么就跳过对这个WindowContainer的检查。不可见的姊妹WindowContainer对于本次动画也没有太大影响,即使跟着一起进行动画用户也看不到,直接跳过检查下一个姊妹WindowContainer就好了。
2)、如果姊妹WindowContainer从Transition.mChanges中能找到一个对应的ChangeInfo对象,并且该ChangeInfo对象也在局部变量targets中,那么认为该姊妹WindowContainer也参与了本次动画,那么分别为他们的TransitionMode调用Transition.reduceMode方法来看它们动画的大方向是否是一致的,首先是根据ChangeInfo.getTransitMode拿到各自的TransitionMode:
TransitionMode定义在TransitionInfo中:
看到Transition模式其实就是定义在WindowManager中的Transition类型的子集。
ChangeInfo.getTransitMode的内容也比较简单:
TRANSIT_CHANGE:收集阶段的可见性和Transition就绪阶段的可见性没有发生变化。
TRANSIT_OPEN:存在发生了变化,且当前可见,即从无到有。
TRANSIT_CLOSE:存在发生了变化,且当前不可见,即从有到无。
TRANSIT_TO_FRONT:存在没有发生变化,且当前可见,说明从后台移动到了前台,从不可见变为了可见。
TRANSIT_TO_BACK:存在没有发生变化,且当前不可见,说明从前台移动到了后台,从可见变为了不可见。
再根据Transition.reduceMode的逻辑:
- TRANSIT_TO_BACK和TRANSIT_CLOSE是一类的。
- TRANSIT_TO_FRONT和TRANSIT_OPEN是一类的。
- TRANSIT_CHANGE单独一类。
如果动画的大方向是一致的,那么即使TRANSIT_TO_BACK和TRANSIT_CLOSE的动画有点差别,但是为了大局考虑,各别同志也不是不能适当调整一下来实现集体上的一致。
如果动画的大方向都不一致,那么它们中的无论哪个肯定都是不能提升为它们的父容器的。比如TaskA想向左平移,TaskB想向右平移,那么如果擅自提升为父容器TaskDisplayArea,不管TaskDisplayArea向左还是向右平移肯定都不合适,这种矛盾就属于不可调和了,那父容器TaskDisplayArea就不用管了,也就是别提升了,让冲突的TaskA和TaskB自己玩去吧。
3)、最后总结一下检查姊妹WindowContainer的这段逻辑,其实就是检查所有的姊妹WindowContainer中,有没有和当前WindowContainer冲突的姊妹WindowContainer,至于是否冲突则看是否满足了以下条件之一:
- 检查所有没有参与动画的姊妹WindowContainer,看能否找到一个可见的。
- 检查所有参与了动画的姊妹WindowContainer,看能否找到一个动画的大方向和当前WindowContainer不一致。
只要找到了这么一个姊妹WindowContainer,我们就无法提升动画的主体。
3 构建TransitionInfo对象
这一节我感觉其实没有什么好说的,大概介绍一下TransitionInfo以及它的内部类Change。
1)、TransitionInfo,实现了Parcelable,结合注释,用来收集WMCore这边的Transition信息,用来同步给WMShell的TransitionPlayer。成员变量大概有这些:
2)、TransitionInfo.Change,同样实现了Parcelable,代表了WindowContainer在一个Transition期间的变化。看其成员变量,保存的信息还是挺多的,还有一个RunningTaskInfo的对象:
再结合Transition.calculateTransitionInfo方法,很明显就大概能弄懂这两个类的作用:
1)、TransitionInfo,对应一个Transition对象,用来收集WMShell感兴趣的Transition的信息,后续同步给WMShell。
2)、TransitionInfo.Change,对应一个Transition.ChangeInfo对象,用来收集WMShell感兴趣的Transition.ChangeInfo的信息,后续同步给WMShell。
顺便一提,google为啥不将Transition中ChangeInfo的命名为”Change“,将TransitionInfo中的Change命名为”ChangeInfo“呢,强迫症犯了。
4 Transition移动到PLAYING状态
其实在Transition.onTransactionReady方法的开头已经将Transition.mState状态置为STATE_PLAYING,这里又调用了一个TransitionController.moveToPlaying方法,看下是干啥的:
其实也非常简单:
1)、开始动画了,意味着当前Transition已经不能收集了,所以将TransitionController.mCollectingTransition置空。特别的,如果有其它Transition在排队,那么就继续将TransitionController.mCollectingTransition赋值为排队队列队首的那个Transition,我播我的动画,你收集你的WindowContainer,互不干扰。
2)、将当前Transition添加到TransitionController.mPlayingTransitions:
一个当前处于playing状态的Transition的队列,也就是说playing的Transition可以有多个。
5 切换到WMShell:onTransitionReady
在Transition.onTransactionReady方法的最后,调用了ITransitionPlayer.onTransitionReady方法将切换到了WMShell:
切换到WMShell意味着Transition就绪阶段已经结束,正式进入Transition的playing阶段,Transitions.TransitionPlayerImpl.onTransitionReady就是我们下一篇文章的起点。
最后稍微看一下调用ITransitionPlayer.onTransitionReady方法之前调用的Transition.buildFinishTransaction方法:
传入的Transaction对象为Transition.mFinishTransaction,如该方法的注释所说,这里对”finish transaction“的操作保证了动画结束后,所有的”reparent“操作或者是Layer的变化将会得到重置,特别是Layer的几何信息(位置、缩放、旋转这些)。如果你的Layer在动画结束的时候在Layer的这些信息上的确有变化,那就要注意不要让这个方法把你对Layer的操作重置了。