Android Framework(八)WMS-窗口动效概述

文章目录

动画简述

  • 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" ,这个是根据动画类型定义的,映射的方法如下:

java 复制代码
# 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值返回一个字符串,看看使用的地方:

java 复制代码
# 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" 相关的图层。 过滤后可以得到下面这些日志:

bash 复制代码
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个动画具体的体现,需要在开发者选项放慢动画时间,才能看的比较清楚,整理了一下对应的动画效果如下:

bash 复制代码
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部分相对简单,看对应的具体分析即可。

触发动画执行的套路

不管什么类型的动画都会执行:

java 复制代码
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,输出如下:

java 复制代码
// 触发远程动画,打印传递过去的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 这些日志的打印也在这,忽略掉无关代码再看一下这个方法:

bash 复制代码
# 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 方法里触发的。

java 复制代码
# 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 。

这里也有打印,比如当前场景的日志输出为:

java 复制代码
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。 所以壁纸动画到底是本地还是远端呢?有点迷了。

相关推荐
贺biubiu2 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
xuekai200809013 小时前
mysql-组复制 -8.4.7 主从搭建
android·adb
nono牛4 小时前
ps -A|grep gate
android
未知名Android用户5 小时前
Android动态变化渐变背景
android
nono牛6 小时前
Gatekeeper 的精确定义
android
猫吻鱼6 小时前
【系列文章合集】【全部系列文章合集】
spring boot·dubbo·netty·langchain4j
stevenzqzq7 小时前
android启动初始化和注入理解3
android
城东米粉儿9 小时前
compose 状态提升 笔记
android
粤M温同学9 小时前
Android 实现沉浸式状态栏
android
ljt272496066110 小时前
Compose笔记(六十八)--MutableStateFlow
android·笔记·android jetpack