文章目录
- 动画简述
- 本地、远端动画的定义
- 什么是“leash”图层
- “leash”图层的命令与创建
- Winscope流程
- 小结
- 动画流程概览分析
- Activity启动app_transition 动画的主要事件
- 触发动画执行的套路
- 动画真正执行
- 动画的结束回调
- 触发远端动画的Target
动画简述
-
1、动画的原理也是利用了视觉停留原理,控制时间点“对象”显示,来组成动画效果。
-
2、这个系列说的虽然是 framework 层的动画,但是本质上和应用内写动画一样的,要么是加载动画的 xml 文件,要么是通过 Animator 对象。
-
3、所以后面的分析重点其实不在动画本身,更重要的动画的播放流程和时机。
-
4、动画的目的,为了提升用户体验,有一个自然的过渡,所以他本身不应该干预业务逻辑,也就是说就算把动画这段代码移掉,业务逻辑也应该正在执行。
本地、远端动画的定义
对于 framework 层来说,动画类型可以分为2类:
- 1、本地动画 (LocalAnimation)
- 2、远端动画(RemoteAnimation)
本地和远端值得是播放动画是在哪个进程执行的,这个本地和远端的概念是相对的,不过既然是 framework 层的角度去分析,所以本地动画值得就是在 "system_service " 进程播放的动画。 而远端动画,指的就是在非 "system_service " 进程播放的动画。
这2个点还是挺重要的,比如当前场景视觉明显可见的点击图标后,图标开始放大铺满屏幕的动效,就是在 launcher 进程播放的,所以也是远端动画。
什么是“leash”图层
既然是动画,肯定是对某个“对象”不停的修改其相关属性来达到动画效果的,比如写 APP 的时候写的动画“对象”会是一个 View 。
而当前分析的 framework 层动画的“对象”则是一个 Surface (图层)。
但是前面说了,动画只是为了提升过渡体验不应该干预实际业务。
比如 APP 里有个很复杂的 View 需要做动画,避免对业务的干扰最好的方式就是在外面嵌套一个 View,然后对这个外面的 View 做动画就好了。
AOSP 的设计也确实如此,会创建一个 “leash” 图层,然后把需要做动画的图层挂载到其下面,再对 “leash” 图层做动画。
Leash 丢给百度翻译,解释为:(牵狗的)皮带。(瞬间就有了一个人用皮带牵着一条狗的画面了),其实这个就是 AOSP 单独为动画创建了一个图层,然后把需要做动画的容器图层挂载到这个图层下,那么就也有了动画效果。如果看到 leash 图层这个名词,指的就是做动画的那个图层了。
结合实际 leash 图层创建前后的层级关系看看:
leash图层创建前:
leash图层创建后:
对比可以看到要开始做动画的时候,Task 上面出现了一个name为 “animation-leash of app_transition ”的图层,这个就是 “leash”图层。
前面的 “animation-leash of” 是固定的,后面的 “ app_transition ” 是这次动画的类型这些后面都会看到代码的定义,目前有个了解即可。
画个图总结一下动画前后的图层改变:
这种设计符合六大原则的单一原则,需要动画就单独拿一个图层来做动画,动画结束后再恢复到原来的层级结构。
当前这只是举个例子,不是说每次 leash 图层都是在 Task 上的,这个是需要看具体情况的, AOSP 有个方法会对不同场景计算出这个 leash 图层需要创建在哪里。
比如远端动画就会对创建在 Taks 上面,对整个 Task 做动画,而 window_animation 一般就是在 WindowToken 或者 WindowState 上做动画。
“leash”图层的命令与创建
上面看到的图层命令,是需要通过看前面的Surface Name 才能确定是哪个窗口的图层,也就是出现在前面的 “Surface(name= XXX” 这个图层的名字,这个看窗口三部曲的时候提过,都是在 SurfaceControl::setName 进行设置的。
至于后面的 " - animation-leash of " 固定的,然后就是 “app_transition” ,这个是根据动画类型定义的,映射的方法如下:
# SurfaceAnimator
static String animationTypeToString(@AnimationType int type) {
switch (type) {
case ANIMATION_TYPE_NONE: return "none";
case ANIMATION_TYPE_APP_TRANSITION: return "app_transition"; // 应用间切换动画
case ANIMATION_TYPE_SCREEN_ROTATION: return "screen_rotation"; // 屏幕旋转动画
case ANIMATION_TYPE_DIMMER: return "dimmer"; // 调光动画
case ANIMATION_TYPE_RECENTS: return "recents_animation"; // 最近任务动画(没发现具体场景,不是从最近任务列表点击A)
case ANIMATION_TYPE_WINDOW_ANIMATION: return "window_animation"; // 窗口动画,比如窗口移除
case ANIMATION_TYPE_INSETS_CONTROL: return "insets_animation"; // 插入动画,但是官方注释说这其实不是一个动画
case ANIMATION_TYPE_TOKEN_TRANSFORM: return "token_transform"; // 动画类型转换
case ANIMATION_TYPE_STARTING_REVEAL: return "starting_reveal"; // 窗口要显示前的动画
default: return "unknown type:" + type;
}
}
这段代码里看到是根据传入的type值返回一个字符串,看看使用的地方:
# SurfaceAnimator
static SurfaceControl createAnimationLeash(Animatable animatable, SurfaceControl surface,
Transaction t, @AnimationType int type, int width, int height, int x, int y,
boolean hidden, Supplier<Transaction> transactionFactory) {
// 自己加的堆栈
android.util.Log.e("biubiubiu", "SurfaceAnimator createAnimationLeash: "+animationTypeToString(type), new Exception());
// 日志
ProtoLog.i(WM_DEBUG_ANIM, "Reparenting to leash for %s", animatable);
final SurfaceControl.Builder builder = animatable.makeAnimationLeash()
.setParent(animatable.getAnimationLeashParent()) //设置父节点
.setName(surface + " - animation-leash of " + animationTypeToString(type)) //命名
.setHidden(hidden)
.setEffectLayer()
.setCallsite("SurfaceAnimator.createAnimationLeash");
......
return leash; // 返回leash图层
}
这个方法后面还会单独详细分析,当前只看setName这一块,看得出来 Winscope 信息中的信息和这里的格式是匹配上的。前面是“surface”的名字,然后拼上一个 “animation-leash of” ,最后面就是根据type返回一个类型,比如"app_transition"。
这里也有响应的日志来说明创建了哪个窗口的 leash 图层。
Winscope流程
先使用 Winscope 工具观察图层的改变,提取关键点的截图如下:
这个是默认状态,点击图标后,就会有以下改变:
可以看到出现了3个动画
- 1、壁纸的 window_animation
- 2、launcher 对应 Task 的 app_transition(退出)
- 3、“电话” 对应 Task 的 app_transition(打开)
这3个动画从 Winscope 看几户是同一帧出现的,稍后从日志上看,也几户是同时执行的。(所以不必纠结这3个的先后顺序)
这3个动画里,最关心的是 “电话” 的打开动画,可以在左边看到下部分已经有一个小矩形出现,结合右边点击的 Visiable 可以确定这快 surface 显示的可见内容其实是
Splash Screen 的 Window。(应用窗口这会还没添加上来。)
根据之前的源码,是先出现Task, 再挂载 ActivityRecord 然后出现 Splash Screen 。
后面一段都是动画执行的过程,主要是这个 Splash Screen 的内容是从小放大到全屏,这个和用户实际看到的视觉效果也是匹配的。
这里只是截取了其中的一个过程。
这个时候是动画执行的过程截取的图,可以看到 Splash Screen 的 window 已经很大了,即将铺满全屏。
等动画结束后(当前抓到的是3个动画在同一帧结束),就剩下“电话” 对应 Task 相关的图层了,其他的都不可见了。
这个时候出现了3个图层–动画的leash图层–窗口容器图层,以及窗口真正的显示图层。
所以可以知道这个 starting_reveal 动画是真正要显示内容前出现的:
从可见性上,starting_reveal 这个动画图层和 Splash Screen 是同级的,但是这个时候 Splash Screen 的窗口从容器顺序上是盖在 starting_reveal 图层上面的
然后就是执行一段时间的 starting_reveal 动画。这个动画结束后说明应用窗口已经要显示了,那么就需要移除 Splash Screen 了,于是开始StartWindow移除动画。
这只是我当前这次抓取的 Winscope 信息,不过不是每次都是这样的。 但是大致流程是一样的,具体的动画出现和结束的时机可能会有点区别,比如某一帧 window_animation 图层已经出现了,但是starting_reveal 图层还在,下一帧才移除。这种1,2帧的差距很正常。
最后的这个状态就是应用已经完全启动展示最后的 Activity 的样子了,其他的窗口都不可见了(最重要的是动画结束后Splash Screen 也移除了)。
小结
上面截取了各个关键节点的图,发现一共出现了5个动画,关于壁纸和 launcher 的可以先不关注,就启动的这个应用来说分为以下几步:
-
1、有一个 app 打开的动画,app_transition ,这个时候显示的内容是并不是应用窗口,而是 Splash Screen 的这个 STtartWindow 。
-
2、app_transition 动画结束后不久,应用窗口绘制后将要显示了,这个时候显示的是 starting_reveal 动画
-
3、starting_reveal 动画结束后,开始的是移除 Splash Screen 的 window_animation 动画
-
4、最终显示的是应用的窗口
不过也可能会有一两帧是几个通话同时存在的,所以也可能抓到的 Winscope 是下面这种图:
动画流程概览分析
前面的分析提过动画会创建 leash 图层,也就是会执行 SurfaceAnimator::createAnimationLeash 方法,我本地代码加上了堆栈。
然后需要过滤掉 “insets_animation” 类型的动画,因为官方注释也说了这个其实不是动画。然后上一节看 Winscope 也确实没看到 “insets_animation” 相关的图层。 过滤后可以得到下面这些日志:
biubiubiu: SurfaceAnimator createAnimationLeash: app_transition
biubiubiu: SurfaceAnimator createAnimationLeash: app_transition
biubiubiu: SurfaceAnimator createAnimationLeash: window_animation
biubiubiu: SurfaceAnimator createAnimationLeash: starting_reveal
biubiubiu: SurfaceAnimator createAnimationLeash: window_animation
看到依次有这5个动画的创建,这个和上面分析的 Winscope 看到的也是对应上了。
这5个动画具体的体现,需要在开发者选项放慢动画时间,才能看的比较清楚,整理了一下对应的动画效果如下:
SurfaceAnimator createAnimationLeash: app_transition dialer Task 》dialer的打开动画 从小放大
SurfaceAnimator createAnimationLeash: app_transition home Task 》launcher的关闭动画 略微放大
SurfaceAnimator createAnimationLeash: window_animation Wallpaper 的动画, 但没看到具体的效果
SurfaceAnimator createAnimationLeash: starting_reveal 应用窗口WindowState 要显示时的动画
SurfaceAnimator createAnimationLeash: window_animation StartWindow 移除动画
上面的这个5个动画主要分为3大部分:
-
1、Activity启动最新出现的 app_transition 动画。
这个阶段会出现3个动画:app_transition(应用),app_transition(桌面),window_animation(壁纸) -
2、应用窗口WindowState 要显示时的 starting_reveal 动画
-
3、移除 StartWindow 的 window_animation 动画
第一部分重点介绍的是应用的 app_transition 动画,这个过程中也会涉及到另外2个。 其中桌面的关闭动画也是 app_transition 类型,所以流程和应用的启动 app_transition 动画流程大致一样的。只有最后 launcher 开始动画的时候做了一下区别。
不过壁纸的 window_animation 在 WallpaperAnimationAdapter 这个专门给壁纸做动画的 Adapter 并没有看到真正的动画执行,然后这边也看到其作为 wallpaperTargets 也传递到 launcher 了,但是最后也没发现有做动画的地方。这点就很奇怪,不过在 Winscope 也只是看到 有 window_animation 的图层,但是也没看到有相关数值的改变,所以个人觉得壁纸的 window_animation 可能并没有什么实际的动画。
在分析应用启动 app_transition 流程的代码前先看看下面的这几个事件,这个只是我个人在撸完整个动画流程,从代码执行顺序上列出来的9个节点。目前不知道是啥没关系,毕竟这只是我个人整理的,不是什么权威的关键节点,不过接下来的代码分析也会一个个的看到。
Activity启动app_transition 动画的主要事件
-
1、launcher 进程构建 RemoteAnimationAdapter,AppLaunchAnimationRunner
-
2、prepareAppTransition 流程
-
3、executeAppTransition,AppTransition.setReady 流程
-
4、 GOOD TO GO 打印
-
5、system_service 创建动画leash 图层
-
6、goodToGo()流程,真正触发远端动画执行
-
7、launcher 开始远端动画
-
8、launcher 具体动画的update
-
9、launcher 动画结束,回调到system_service
后面的2部分相对简单,看对应的具体分析即可。
触发动画执行的套路
不管什么类型的动画都会执行:
WindowContainer::startAnimation
SurfaceAnimator::startAnimation
会在 SurfaceAnimator::startAnimation
方法中创建动画 leash 图层
,并通过 Adapter 来开始动画。然后才是适配器模式各自动画的 Adapter 做自己的处理。
一般:
- 本地动画就是直接开始执行
- 远端动画则是会在 goodToGo 触发远端执行
动画真正执行
动画的执行是适配器模式,但是真正干活的也不会是这个 Adapter 。
一般都是 Adapter + Runner
模式,真正干活的是这个 Runner 。
比如本地动画就是 LocalAnimationAdapter + SurfaceAnimationRunner
而分析的应用启动动画是远端动画,它们的组合是是 RemoteAnimationAdapter + LauncherAnimationRunner
动画的结束回调
目前看到的本地动画和远端动画,都会执行到 SurfaceAnimator::startAnimation 。而每个窗口构建的时候都会创建一个 SurfaceAnimator ,并且专递一个动画结束回调(WindowContainer::onAnimationFinished)过去。这个回调被封装成在 mInnerAnimationFinishedCallback 变量了。
所以本地动画和远端动画的结束回调,执行都是 WindowContainer::onAnimationFinished
方法。而这个方法最终又会执行到 WindowManagerService::onAnimationFinished
。
触发远端动画的Target
远端动画执行前会打印这次操作了哪些图层: 这段日志会打印动画的 Target,输出如下:
// 触发远程动画,打印传递过去的3个类型的leash图层数量
D WindowManager: goodToGo(): onAnimationStart, transit=TRANSIT_OLD_WALLPAPER_CLOSE, apps=2, wallpapers=1, nonApps=0
D WindowManager: startAnimation(): Notify animation start:
I WindowManager: Starting remote animation
// 传递到远端的动画图层打印
I WindowManager: container=Task{cdcc410 #1 type=home ?? U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
// 桌面的
I WindowManager: Target:
I WindowManager: mode=1 taskId=8 isTranslucent=false clipRect=[0,0][0,0] contentInsets=[0,70][0,84] prefixOrderIndex=16 position=[0,0] sourceContainerBounds=[0,0][720,1600] screenSpaceBounds=[0,0][720,1600] localBounds=[0,0][720,1600]
I WindowManager: windowConfiguration={ mBounds=Rect(0, 0 - 720, 1600) mAppBounds=Rect(0, 70 - 720, 1516) mMaxBounds=Rect(0, 0 - 720, 1600) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=home mAlwaysOnTop=undefined mRotation=ROTATION_0}
I WindowManager: leash=Surface(name=Surface(name=Task=1)/@0x1842ced - animation-leash of app_transition)/@0x21da6b6
I WindowManager: taskInfo=TaskInfo{userId=0 taskId=8 displayId=0 isRunning=true baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10208000 cmp=com.android.launcher3/.uioverrides.QuickstepLauncher } baseActivity=ComponentInfo{com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher} topActivity=ComponentInfo{com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher} origActivity=null realActivity=ComponentInfo{com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher} numActivities=1 lastActiveTime=500834 supportsSplitScreenMultiWindow=true supportsMultiWindow=true resizeMode=2 isResizeable=true minWidth=-1 minHeight=-1 defaultMinSize=220 token=WCT{RemoteToken{d94d7b1 Task{512e747 #8 type=home I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 rootTaskId=1 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}}} topActivityType=2 pictureInPictureParams=null shouldDockBigOverlays=false launchIntoPipHostTaskId=0 displayCutoutSafeInsets=Rect(0, 70 - 0, 0) topActivityInfo=ActivityInfo{5fdf496 com.android.launcher3.uioverrides.QuickstepLauncher} launchCookies=[] positionInParent=Point(0, 0) parentTaskId=-1 isFocused=false isVisible=false isSleeping=false topActivityInSizeCompat=false topActivityEligibleForLetterboxEducation= false locusId=LocusId[17_chars] displayAreaFeatureId=1 cameraCompatControlState=hidden}
I WindowManager: allowEnterPip=true
I WindowManager: windowType=-1 hasAnimatingParent=false backgroundColor=0container=Task{3a0b2b8 #13 type=standard A=10140:com.google.android.dialer U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
// “电话”应用的
I WindowManager: Target:
I WindowManager: mode=0 taskId=13 isTranslucent=false clipRect=[0,0][0,0] contentInsets=[0,70][0,84] prefixOrderIndex=20 position=[0,0] sourceContainerBounds=[0,0][720,1600] screenSpaceBounds=[0,0][720,1600] localBounds=[0,0][720,1600]
I WindowManager: windowConfiguration={ mBounds=Rect(0, 0 - 720, 1600) mAppBounds=Rect(0, 70 - 720, 1516) mMaxBounds=Rect(0, 0 - 720, 1600) mDisplayRotation=ROTATION_0 mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0}
I WindowManager: leash=Surface(name=Surface(name=Task=13)/@0xd0a4264 - animation-leash of app_transition)/@0x62f9fb7
I WindowManager: taskInfo=TaskInfo{userId=0 taskId=13 displayId=0 isRunning=true baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 pkg=com.google.android.dialer cmp=com.google.android.dialer/.extensions.GoogleDialtactsActivity } baseActivity=ComponentInfo{com.google.android.dialer/com.google.android.dialer.extensions.GoogleDialtactsActivity} topActivity=ComponentInfo{com.google.android.dialer/com.android.dialer.main.impl.MainActivity} origActivity=ComponentInfo{com.google.android.dialer/com.google.android.dialer.extensions.GoogleDialtactsActivity} realActivity=ComponentInfo{com.google.android.dialer/com.android.dialer.main.impl.MainActivity} numActivities=1 lastActiveTime=500849 supportsSplitScreenMultiWindow=true supportsMultiWindow=true resizeMode=2 isResizeable=true minWidth=-1 minHeight=-1 defaultMinSize=220 token=WCT{RemoteToken{37af324 Task{3a0b2b8 #13 type=standard A=10140:com.google.android.dialer U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}}} topActivityType=1 pictureInPictureParams=null shouldDockBigOverlays=false launchIntoPipHostTaskId=0 displayCutoutSafeInsets=Rect(0, 70 - 0, 0) topActivityInfo=ActivityInfo{346ae8d com.google.android.dialer.extensions.GoogleDialtactsActivity} launchCookies=[android.os.BinderProxy@c557842] positionInParent=Point(0, 0) parentTaskId=-1 isFocused=true isVisible=true isSleeping=false topActivityInSizeCompat=false topActivityEligibleForLetterboxEducation= false locusId=null displayAreaFeatureId=1 cameraCompatControlState=hidden}
I WindowManager: allowEnterPip=true
I WindowManager: windowType=-1 hasAnimatingParent=false backgroundColor=0
这些日志里有很多关键信息,可以在遇到问题的时候看,数据太多就不一一介绍了,继续看日志是在哪里控制的吧。
触发远端动画的流程在 RemoteAnimationController::goodToGo 这些日志的打印也在这,忽略掉无关代码再看一下这个方法:
# RemoteAnimationController
// 远端动画的Adapter
private final RemoteAnimationAdapter mRemoteAnimationAdapter;
void goodToGo(@WindowManager.TransitionOldType int transit) {
// 打印goodToGo(),表现这才是真正的触发了goodToGo()逻辑
ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo()");
......
// 打印日志,真正开始触发动画
ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo(): onAnimationStart,"
+ " transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
AppTransition.appTransitionOldToString(transit), appTargets.length,
wallpaperTargets.length, nonAppTargets.length);
// 重点* 3. 这里是触发远端动画真正执行的地方
mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets,
wallpaperTargets, nonAppTargets, mFinishedCallback);
......
// 日志处理
if (ProtoLogImpl.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) {
ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation(): Notify animation start:");
writeStartDebugStatement();
}
......
}
可以看到前面2个打印都在这,后续的打印是 RemoteAnimationController::writeStartDebugStatement 方法里触发的。
# RemoteAnimationController
private void writeStartDebugStatement() {
ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Starting remote animation");
// 打印内容
final StringWriter sw = new StringWriter();
final FastPrintWriter pw = new FastPrintWriter(sw);
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
// 触发每个适配器的dump
mPendingAnimations.get(i).mAdapter.dump(pw, "");
}
pw.close();
ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "%s", sw.toString());
}
可以看到整理的打印是从 mPendingAnimations 下的对象取出对应的 RemoteAnimationRecord 相关的变量然后dump的。
mPendingAnimations 在应用启动动画-app_transition-3 的时候看到,是构建一个 RemoteAnimationRecord 对象就会把其添加进 mPendingAnimations 。
这里也有打印,比如当前场景的日志输出为:
D WindowManager: createAnimationAdapter(): container=Task{8d3f87a #20 type=standard A=10140:com.google.android.dialer U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
D WindowManager: createAnimationAdapter(): container=Task{7bfafb5 #1 type=home ?? U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
这个和之前的分析是一样的, 一个是新启动应用的Task 一个是 launcher 的。所以只有2个,注意,没有壁纸的,前面的日志也没看壁纸的Target。 所以壁纸动画到底是本地还是远端呢?有点迷了。