Android Framework--系统动画

拼接转屏动画矩阵这里之所以要说“系统”动画,是为了跟APP开发中的动画区分开,注意它们的原理是一样的,用到的都是android.view.animation.Animation类,这里主要从以下两个方面来区分出“系统”动画:

  1. 动画由谁来施加和步进
  2. 动画的目标

本文贴出的代码来源于Android N。


什么是系统动画

做过APP开发的童鞋都知道下面知识点:

  • Activity切换的时候有“切换动画”,可以自行定制
  • 窗口切换的时候有“窗口动画”,可以自行定制
  • 转屏的时候有“转屏动画”,由系统决定,不可定制

这里提及的三种动画,我在这里称之为“系统动画”,相比平常APP开发中施加到View上的“补间动画”,根据给出的两点标准,来看下它们的区别:

  1. 动画由谁来施加和步进
    系统动画:框架来施加和步进,即system_server进程
    补间动画:应用进程来施加和步进

  2. 动画的目标
    系统动画:整个窗口绘制出的内容,即view tree绘制出的内容
    补间动画:具体的View

不难看出,之所以用“系统”这个词,是因为系统对这些动画占据了主导权。根据这里描述的区别,可以得出一个结论:
系统动画如果出现卡顿,责任在于system_server进程,跟应用进程无关,因为应用进程跟系统动画的步进没有直接关系。
如果APP开发童鞋还在为某个切换动画或者窗口动画卡顿而苦恼,请注意,不是你们的锅!!!当然,如果因为APP乱跑导致整个系统很卡,进而影响系统动画,那又是另外一回事。


Animation原理

这里简单讲一下,所谓动画,就是以渐进的方式来呈现目标状态的改变。

那么渐进以什么为维度?很显然就是时间。时间的粒度是多大?以60fps为例,每16ms刷新一次目标的状态即可。

Animation动画就这么四种:

  • 位移(TranslateAnimation)
  • 缩放(ScaleAnimation)
  • 旋转(RotateAnimation)
  • 透明度(AlphaAnimation)

举个简单例子,比如有个透明度动画,要求目标的透明度从fromAlpha变为toAlpha,时长为duration,开始的时间为startTime,下面从数学的角度解释下状态渐进的过程:即到了一个刷新状态的时间点currentTime,求目标的透明度currentAlpha。

这个问题类似于小明从A点跑到B点,告诉你耗时,问你某个时间小明的位置。

讨论一个最简单的情形,这个过程是匀速的,那么可以得到:

v = (toAlpha - fromAlpha) / duration
elapsedTime = currentTime - startTime
currentAlpha = fromAlpha + v * elapsedTime
             = fromAlpha + (toAlpha - fromAlpha) * (currentTime - startTime) / duration

利用这个公式,每16ms计算出当前的状态并刷新,就能看到一种渐进的透明度变化的效果。

要是不匀速怎么破?比如我就想要前半段变化快一点,后半段变化慢一点。把上面最后的公式改一下:

elapsedTimePercent = (currentTime - startTime) / duration
currentAlpha = fromAlpha + (toAlpha - fromAlpha) * f(elapsedTimePercent)

上述f(x)是一个函数,对于匀速变化而言,f(x) = x。那么如果想定制变化率,问题就转化为定制f(x)函数。这个f(x)函数负责将elapsedTimePercent(逝去时间百分比)映射为一个新的值,称之为差值器(Interpolator),定义如下:

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {
    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

现在我们就知道给定下面几个元素,就可以启动一个动画:

  • 起始状态
  • 结束状态
  • 动画时长
  • 差值器

然后在每个刷新的时间点计算当前的状态值,称之为动画的步进,直到时间结束。那么,如果负责动画步进的人没有及时进行状态刷新,比如32ms才刷新一次,肉眼就会感觉到突兀,也就是常说的动画卡顿。

还有个问题是,为什么Animation只支持上述的四种动画?

从数学的角度来看,位移、缩放以及旋转的原理是矩阵(Matrix)变换,Animation封装了Transformation,它包含下面两个重要成员变量:

protected Matrix mMatrix;
protected float mAlpha;

Animation只不过是一种高层的抽象,它将定制动画的能力封装起来,并跟UI系统整合在一起,而它的底层实现,实际上是Matrix。准确地说,Animation只能实现Matrix和Alpha能实现的动画范畴。这个Matrix,我们后面还会看到,它可以执行一些变换,Matrix之间还能进行拼接以合并变换,具体可以查阅源码:

frameworks/base/graphics/java/android/graphics/Matrix.java

系统动画的实现

回到我们提及的三种系统动画:

  • 切换动画
  • 窗口动画
  • 转屏动画

先回答一个问题,为什么要区分切换动画和窗口动画?

在Android系统中窗口是有类型的,可以简单分为Activity窗口和非Activity窗口。先说窗口动画,前面讲到,系统动画的目标是窗口(Window),说明窗口是系统动画施加的最基本的单位。实际上正是如此,所有窗口都可以定制自己的窗口动画,这是Android系统赋予窗口最基本的功能。这种动画是独立的,没有关联性,也就是窗口动画的施加完全取决于自己:什么时候自己准备好显示就施加进入动画,什么时候该移除就施加退出动画,不受其它窗口的影响,也没有任何联动效果。

再讲到切换动画,应用开发中一个Activity,可以对应多个窗口,这样可以在UI上赋予Activity更大的能力,系统将这些归属于同一个Activity的窗口统一管理,把它们当做一个整体,并让它们以相同的动画同时进入或退出。另外,用户在界面上的跳转,实际上主要是Activity之间的跳转,它包含了进入和退出两个方面,是有一种联动的效果,用一个单词来形容叫transition,顾名思义,叫做切换动画。可以这么说,切换动画是仅针对Activity窗口,这很大程度上是由于Android对窗口的组织形式来决定的,并具体反映到代码实现上。

综上所述:

  • 任何窗口都可以有窗口动画,但是切换动画为Activity窗口独有
  • 窗口动画没有关联性,而切换动画涉及到切换的双方
  • 窗口动画在窗口进入或退出时发生,切换动画在Activity切换时发生

这么说好像有点干巴巴,我们看下跟这两种动画的定制有关的styleable,完整声明太长,这里只列出一部分,童鞋们可以自行查看源码,位于frameworks/base/core/res/res/values/attrs.xml:


<declare-styleable name="WindowAnimation">
    
    <attr name="windowEnterAnimation" format="reference" />
    
    <attr name="windowExitAnimation" format="reference" />
    
    <attr name="windowShowAnimation" format="reference" />
    
    <attr name="windowHideAnimation" format="reference" />

    
    <attr name="activityOpenEnterAnimation" format="reference" />
    
    <attr name="activityOpenExitAnimation" format="reference" />
    ...
declare-styleable>

窗口动画只有四个属性可以定制,区别请看注释:

  • 添加动画
  • 移除动画
  • 显示动画
  • 隐藏动画

窗口动画的无关联性,意思就是当一个窗口添加、移除、显示、隐藏时,窗口动画就会被施加(如果有的话),跟其它因素无关。

而切换动画的属性就比较多了,原因是切换的场景会有多种,比如是否在Task范围内切换、是启动新界面还是退出当前界面、是否有壁纸等等,开发者可以对不同的场景定制不同的动画。每个切换场景对应进入和退出两个属性,分别对应切换的双方,这也就是切换动画有联动性的含义。当一个切换场景发生时,系统会选取即将显示的那一方,作为获取动画的基准方。比如现在从Activity-A切换到Activity-B,系统会以Activity-B的主窗口为基准,获取该场景下配置的一对动画,然后将进入动画施加到B,退出动画施加到A。记住,每个切换场景只有一个基准。

现在可以聊点代码相关的东西,这三种系统动画,分别对应三个类:

frameworks/base/services/core/java/com/android/server/wm/AppWindowAnimator.java
frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java
frameworks/base/services/core/java/com/android/server/wm/ScreenRotationAnimation.java

在WMS中,一个窗口对应一个WindowState,它跟上述动画类之间的组织形式如下:
这里写图片描述

对应关系如下:

  1. AppWindowToken跟Activity是一对一关系,而一个Activity可以有多个窗口,所以AppWindowAnimator对应多个WindowState
  2. 一个WindowState对应一个WindowStateAnimator
  3. ScreenRotationAnimation跟屏幕一一对应,如果有,它会施加到所有可见窗口上

还能看到三个类有一些共性的东西:

  • Animation成员,表示要执行的Animation
  • Transformation成员,表示Animation成员计算出的Transformation,里面包含Matrix和alpha,最后要施加到窗口上
  • stepAnimation函数,用来步进Animation成员表示的动画,可以想象理想情况下它应该每16ms执行一次

WindowStateAnimator.computeShownFrameLocked()是一个重要函数,它负责将一个窗口可能有的系统动画整合到一起,合并成一个结果并最后施加到窗口上。实际上一个窗口不可能同时有三种系统动画,至少转屏动画存在的情况下会禁用另外两种动画,这个很好理解,转屏的时候掺杂进其它动画显然是很奇怪的,stepAnimation会识别出是否在转屏并提前结束切换动画或窗口动画。但切换动画跟窗口动画理论上是可以共存的,虽然两者拼接在一起的效果会比较奇怪。

void computeShownFrameLocked() {
    // 是否有窗口动画
    final boolean selfTransformation = mHasLocalTransformation;
    // 父窗口是否有动画
    Transformation attachedTransformation =
(mAttachedWinAnimator != null && mAttachedWinAnimator.mHasLocalTransformation)
            ? mAttachedWinAnimator.mTransformation : null;
    // 是否有切换动画
    Transformation appTransformation = (mAppAnimator != null && mAppAnimator.hasTransformation)
            ? mAppAnimator.transformation : null;
    ...
            final int displayId = mWin.getDisplayId();
    final ScreenRotationAnimation screenRotationAnimation =
            mAnimator.getScreenRotationAnimationLocked(displayId);
    // 是否有转屏动画
    final boolean screenAnimation =
            screenRotationAnimation != null && screenRotationAnimation.isAnimating();

    mHasClipRect = false;
    if (selfTransformation || attachedTransformation != null
            || appTransformation != null || screenAnimation) {
        // 如果有上述任一动画
        if (selfTransformation) {
            // 拼接窗口动画矩阵
            tmpMatrix.postConcat(mTransformation.getMatrix());
        }
        if (attachedTransformation != null) {
            // 拼接父窗口动画矩阵
            tmpMatrix.postConcat(attachedTransformation.getMatrix());
        }
        if (appTransformation != null) {
            // 拼接切换动画矩阵
            tmpMatrix.postConcat(appTransformation.getMatrix());
        }
        ...
        if (screenAnimation) {
            // 拼接转屏动画矩阵
            tmpMatrix.postConcat(screenRotationAnimation.getEnterTransformation().getMatrix());
        }
        tmpMatrix.getValues(tmpFloats);
        mDsDx = tmpFloats[Matrix.MSCALE_X];
        mDtDx = tmpFloats[Matrix.MSKEW_Y];
        mDsDy = tmpFloats[Matrix.MSKEW_X];
        mDtDy = tmpFloats[Matrix.MSCALE_Y];
        float x = tmpFloats[Matrix.MTRANS_X];
        float y = tmpFloats[Matrix.MTRANS_Y];
        // tmpMatrix是最后的矩阵拼接结果,将变换相关的属性保存下来,SurfaceFlinger需要这些属性来执行变换
        mWin.mShownPosition.set((int) x, (int) y);
        // 原始alpha值
        mShownAlpha = mAlpha;
        if (!mService.mLimitedAlphaCompositing
                || (!PixelFormat.formatHasAlpha(mWin.mAttrs.format)
                || (mWin.isIdentityMatrix(mDsDx, mDtDx, mDsDy, mDtDy)
                        && x == frame.left && y == frame.top))) {
            if (selfTransformation) {
                // 拼接窗口动画alpha值
                mShownAlpha *= mTransformation.getAlpha();
            }
            if (attachedTransformation != null) {
                // 拼接父窗口动画alpha值
                mShownAlpha *= attachedTransformation.getAlpha();
            }
            if (appTransformation != null) {
                // 拼接切换动画alpha值
                mShownAlpha *= appTransformation.getAlpha();
            }
            if (screenAnimation) {
                // 拼接转屏动画alpha值
                mShownAlpha *= screenRotationAnimation.getEnterTransformation().getAlpha();
            }
            ...
}

将所有存在的动画拼接在一起后,最后得到一个Matrix和alpha值,作为当前这一帧的变换结果。这里用到的Transformation是怎么来的呢,显然是通过每帧调用stepAnimation计算出来,这里仅以WindowStateAnimator为例,另外两个类似:

boolean stepAnimationLocked(long currentTime) {
    // Save the animation state as it was before this step so WindowManagerService can tell if
    // we just started or just stopped animating by comparing mWasAnimating with isAnimationSet().
    mWasAnimating = mAnimating;
    final DisplayContent displayContent = mWin.getDisplayContent();
    if (displayContent != null && mService.okToDisplay()) {
        // We will run animations as long as the display isn't frozen.

        if (mWin.isDrawnLw() && mAnimation != null) {
            mHasTransformation = true;
            mHasLocalTransformation = true;
            if (!mLocalAnimating) {// mLocalAnimating表示是否已经初始化mAnimation
                final DisplayInfo displayInfo = displayContent.getDisplayInfo();
                if (mAnimateMove) {
                    // 初始化mAnimation
                    mAnimateMove = false;
                    mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
                            mAnimDx, mAnimDy);
                } else {
                    // 初始化mAnimation
                    mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
                            displayInfo.appWidth, displayInfo.appHeight);
                }
                mAnimDx = displayInfo.appWidth;
                mAnimDy = displayInfo.appHeight;
                // 设置mAnimation的起始时间
                mAnimation.setStartTime(mAnimationStartTime != -1
                        ? mAnimationStartTime
                        : currentTime);
                mLocalAnimating = true;
                mAnimating = true;
            }
            if ((mAnimation != null) && mLocalAnimating) {
                mLastAnimationTime = currentTime;
                if (stepAnimation(currentTime)) {
                    // 步进mAnimation
                    return true;
                }
            }
            ...
        }
        mHasLocalTransformation = false;
        ...
    } else if (mAnimation != null) {
        // If the display is frozen, and there is a pending animation,
        // clear it and make sure we run the cleanup code.
        mAnimating = true;
    }

    if (!mAnimating && !mLocalAnimating) {
        return false;
    }
    ...
}

private boolean stepAnimation(long currentTime) {
    if ((mAnimation == null) || !mLocalAnimating) {
        return false;
    }
    currentTime = getAnimationFrameTime(mAnimation, currentTime);
    mTransformation.clear();
    // 调用Animation.getTransformation()步进动画,结果保存在mTransformation中
    final boolean more = mAnimation.getTransformation(currentTime, mTransformation);
    if (mAnimationStartDelayed && mAnimationIsEntrance) {
        mTransformation.setAlpha(0f);
    }
    if (false && DEBUG_ANIM) Slog.v(TAG, "Stepped animation in " + this + ": more=" + more
            + ", xform=" + mTransformation);
    return more;
}

现在已经知道stepAnimation函数负责步进动画,然后computeShownFrameLocked将所有系统动画拼接在一起,那么是谁来调用stepAnimation函数呢,这就要说到WindowAnimator,位于:

frameworks/base/services/core/java/com/android/server/wm/WindowAnimator.java

WindowAnimator是所有系统动画的管理者,它负责在vsync的步调下更新所有动画相关的事项(当然还有其它职责,但是不在这里的讨论范围),接收到vsync后会执行下面函数:

private void animateLocked(long frameTimeNs);

这里只需要知道最终会调用到各个动画类的stepAnimation函数即可,函数的最后还会判断是否存在未执行结束的动画,有的话继续调度下一个vsync,周而复始,有兴趣的可以自行阅读源码。


卡顿之殇

由上面的分析可知,系统动画要流畅地运行,重点是让WindowAnimator.animateLocked()函数保持60fps的刷新率,而这并不是每时每刻都能满足的。首先这是在持锁状态下执行的,而在WMS中还有许多操作需要持有同一把锁,如果发生锁等待,就有可能出现掉帧;其次,动画过程中需要跟SurfaceFlinger进行通讯,有些调用需要同步等待,如果SurfaceFlinger没有及时返回,同样也会出现掉帧。

当然,大部分情况下,system_server进程负载不高,基本不会出现上述所说的掉帧情况。基于这一点,我接触过一个开发者,因为应用内部某个动画卡顿,而将其替换为系统动画,来获得流畅的动画效果,不得不说这确实是一种比较hacker的方法,但我想说的是,优化好应用自身,才是最应该去做的事情。

你可能感兴趣的:(android,动画)