拼接转屏动画矩阵这里之所以要说“系统”动画,是为了跟APP开发中的动画区分开,注意它们的原理是一样的,用到的都是android.view.animation.Animation类,这里主要从以下两个方面来区分出“系统”动画:
本文贴出的代码来源于Android N。
做过APP开发的童鞋都知道下面知识点:
这里提及的三种动画,我在这里称之为“系统动画”,相比平常APP开发中施加到View上的“补间动画”,根据给出的两点标准,来看下它们的区别:
动画由谁来施加和步进
系统动画:框架来施加和步进,即system_server进程
补间动画:应用进程来施加和步进
动画的目标
系统动画:整个窗口绘制出的内容,即view tree绘制出的内容
补间动画:具体的View
不难看出,之所以用“系统”这个词,是因为系统对这些动画占据了主导权。根据这里描述的区别,可以得出一个结论:
系统动画如果出现卡顿,责任在于system_server进程,跟应用进程无关,因为应用进程跟系统动画的步进没有直接关系。
如果APP开发童鞋还在为某个切换动画或者窗口动画卡顿而苦恼,请注意,不是你们的锅!!!当然,如果因为APP乱跑导致整个系统很卡,进而影响系统动画,那又是另外一回事。
这里简单讲一下,所谓动画,就是以渐进的方式来呈现目标状态的改变。
那么渐进以什么为维度?很显然就是时间。时间的粒度是多大?以60fps为例,每16ms刷新一次目标的状态即可。
Animation动画就这么四种:
举个简单例子,比如有个透明度动画,要求目标的透明度从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对窗口的组织形式来决定的,并具体反映到代码实现上。
综上所述:
这么说好像有点干巴巴,我们看下跟这两种动画的定制有关的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,它跟上述动画类之间的组织形式如下:
对应关系如下:
还能看到三个类有一些共性的东西:
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的方法,但我想说的是,优化好应用自身,才是最应该去做的事情。