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

相关推荐
勿问东西5 小时前
【Android】多媒体
android
彭于晏68914 小时前
Android数据存储
android·数据库
诸神黄昏EX14 小时前
Android OTA升级
android
金色熊族14 小时前
安卓真机调试“no target device found“以及“ INSTALL_FAILED_USER_RESTRICTED“两个问题的解决办法
android
提笔忘字的帝国14 小时前
【Android】获取备案所需的公钥以及签名MD5值
android
customer0814 小时前
【开源免费】基于SpringBoot+Vue.JS美容院管理系统(JAVA毕业设计)
android·java·vue.js·spring boot·spring cloud·开源
图王大胜15 小时前
Android SystemUI组件(10)禁用/重启锁屏流程分析
android·systemui·锁屏·keyguard
奋斗的小鹰16 小时前
kotlin 委托
android·开发语言·kotlin
Wency(王斯-CUEB)18 小时前
【文献阅读】政府数字治理的改善是否促进了自然资源管理?基于智慧城市试点的准自然实验
android·kotlin·智慧城市