浅析Android动画原理
什么是动画
动画(英语:Animation)是一种通过一系列多个静止的固态图像(帧)以一定频率连续变化、运动的速度(如每秒60张)而导致肉眼的视觉残留产生的错觉——而误以为图画或物体(画面)运动的作品及其视频技术。
在软件行业,动画的描述包含视频帧动画以及视图动画。帧视频动画是根据静态图片进行帧播放,而视图动画则是针对软件组件或视图在交互呈现上能够借助不断移动、旋转、缩放等方式实现类似帧动画的效果。
Android动画
在移动行业,动画也使得用户在手机设备上的操作或观感不再生硬,而是以顺畅自然的过渡效果让应用在呈现上更加生动。特别是在应用在响应用户操作而发生变化时,给视图添加相应的动画能够给予用户引导及带来良好的交互视觉体验。
当然,在Android系统上也支持动画,能够借助官方提供的动画库api轻松实现各类日常使用中运用到的动画:帧动画、视图动画以及组件的属性动画。接下来,就以动画在Android系统上的动画为例,深入了解在Android使用到的动画库,针对其中的实现与运用进行详解。
Android中的动画分类
首先,咱们先从Android中所运用到的动画分类说起。Android动画大体可以分为帧动画、视图动画以及属性动画。其中帧动画是在drawable层面做的处理,本文主要聚焦在较常使用的动画框架方面,即视图动画及属性动画。
视图动画View Animation
视图动画可以简单理解为补间动画,顾名思义就是可以使用该视图动画系统对视图进行补间动画。补间动画可针对一个视图指定对应动画的结束状态(位移、缩放、透明度、旋转)以及动画时长,系统则会针对起始状态、终点状态计算整个动画并运用展示在视图上。
视图动画在使用上简单做个介绍,将一个文本视图做一个渐现的展示动画。
- 首先在res文件夹下新建对应动画资源文件anim.xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="0"
android:toAlpha="1.0"/>
- 在需要动画的场景获取对应动画并设置给视图view
TextView textView = findViewById(R.id.text);
Animation animation = AnimationUtils.loadAnimation(context, R.anim.anim);
textView.startAnimation(animation);
属性动画Property Animation
属性动画从命名上也能看出,属性动画是实实在在针对视图的属性做的动画,它是一个更加强健的框架,可以为几乎任何内容添加动画效果。属性动画可以定义一个随时间更改任何对象属性的动画,会在指定时长内更改属性的值,无论其是否绘制到屏幕上。
从上图官方给的例子上解释,例如一个对象的属性x,对它做属性动画,持续时长为40ms,目标值为40,则在属性动画系统上,会根据时间单位不断修改对象x的值。例如在10ms时值为10,40ms时其值为40.
借助属性动画系统,您可以定义动画的以下主要特性:
- 时长:根据需要指定动画的时长。默认时长为 300 毫秒。
- 时间插值:指定如何根据动画的当前已播放时长来计算属性的值。
- Animator set:您可以将动画分成多个逻辑集,它们可以一起播放、按顺序播放或者在指定的延迟时间后播放。
同样以文本视图渐现的例子,我们用属性动画的处理方式来实现:
TextView textView = findViewById(R.id.text);
Animator alphaAnim = ObjectAnimator.ofFloat(textView, "alpha", 0f, 1f);
alphaAnim.start();
两者区别
视图动画 | 属性动画 | |
---|---|---|
属性支持 | 只支持alpha、scale、translate、rotate等枚举属性 | 理论上可以支持视图任意属性 |
属性变换 | 只是在绘制视图的位置进行修改,不会修改视图本身的属性,事件响应位置跟视图不对应 | 实际改变了视图的属性,能在动画的变换位置响应事件 |
动画支持范围 | 只支持视图的部分属性动画 | 除视图动画支持更广(颜色、尺寸)外,还可以为任何对象添加动画效果 |
动画是如何驱动的?
视图动画
在开发中使用视图动画时最常用的方法是新建动画资源xml文件创建各个动画节点。然后在具体使用的场景先通过AnimationUtils.loadAnimation()从xml资源中得到具体的动画,随后通过view.startAnimation()方法给视图添加动画。其中比较重要的两个操作是将xml解析得到具体的动画,二是view内部是如何驱动一个动画去执行的。
XML解析
动画资源xml的解析可以从AnimationUtils.loadAnimation入手,查看动画解析的具体实现:
public static Animation loadAnimation(Context context, @AnimRes int id){
XmlResourceParser parser = null;
try {
//首先通过contextResource根据xml得到动画xmlParser
parser = context.getResources().getAnimation(id);
//再通过parser创建最终使用的动画animation
return createAnimationFromXml(context, parser);
} catch {...}
}
//创建方法最终会调用该方法得到实际的animation
private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
AnimationSet parent, AttributeSet attrs){
Animation anim = null;
...
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
//不断遍历xmlParser的节点,根据解析动画类型创建不同类型的动画
if (name.equals("set")) {
anim = new AnimationSet(c, attrs);
createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
} else if (name.equals("alpha")) {
anim = new AlphaAnimation(c, attrs);
} else if (name.equals("scale")) {
anim = new ScaleAnimation(c, attrs);
} else if (name.equals("rotate")) {
anim = new RotateAnimation(c, attrs);
} else if (name.equals("translate")) {
anim = new TranslateAnimation(c, attrs);
} else if (name.equals("cliprect")) {
anim = new ClipRectAnimation(c, attrs);
} else {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
if (parent != null) {
parent.addAnimation(anim);
}
}
return anim;
}
从上面代码片段可以看出,将xml解析成视图动画的流程是比较简单的。总体来说是使用contxt资源根据对应xml创建得到xmlParse,然后解析parse相应字段节点根据不同动画类型创建不同动画。
动画实现
动画的具体实现还是从代码view.startAnimation方法开始分析
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
其中,setAnimation只是简单的给成员变量mCurrentAnimation赋值,并没有开启动画的操作,那动画的执行具体在哪呢?这需要从最后一行invalidate()说起,它的作用是请求View树进行重绘,执行measure、layout、draw流程中的draw。最终会走到view.draw(Canvas canvas, ViewGroup parent, long drawingTime):
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
//可以理解为是否还有动画的标志位
boolean more = false;
...
//动画最终用到的transform
Transformation transformToApply = null;
...
//获取view的动画
final Animation a = getAnimation();
if (a != null) {
//判断是否需要应用动画的核心方法,如果有动画则会重新调用绘制
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();
}
...
//如果apply不为空(有动画时赋值),则将动画变换矩阵运用到canvas上
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
...
return more;
}
而其中跟动画相关的核心方法为applyLegacyAnimation,其中会通过animation.getTransformation 方法判断是否还有动画,而其中具体的实现如下代码
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
...
//调用animation.getTransformation获取实际是否有动画
boolean more = a.getTransformation(drawingTime, t, 1f);
...
//当有动画时会调用parent.invalidate再次触发重绘
if (more) {
if (!a.willChangeBounds()) {
...
else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {
...
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
public boolean getTransformation(long currentTime, Transformation outTransformation) {
...
if (duration != 0) {
//根据动画时长以及绘制时间计算动画的百分比
normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
(float) duration;
}
final boolean expired = normalizedTime >= 1.0f || isCanceled();
//得到是否还有动画
mMore = !expired;
...
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//该方法最终根据具体的动画子类重写对transform进行设值
applyTransformation(interpolatedTime, outTransformation);
...
return mMore;
}
从以上代码分析可以看到,getTransformation内部会计算动画的进度并将view传递的transformation进行设值,最终返回给view.draw方法应用到canvas画布。
从以上代码剖析可以得到视图动画的整体流程简化为view在设置animation时触发invalidate,重绘会通过animation内部计算得到动画并更新transformation,同时再次触发下一次的绘制,最后将结果返回给view在绘制时对画布做矩阵变换从而实现动画。因为是在draw层面实现动画的,因此视图动画只是在视觉上进行了变换,而视图实际的属性并未发生变化。
属性动画
属性动画的实现我们还是从动画代码调用开始,以上面alpha动画为例进行展开。
动画构建
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
//构建animator对象,设置target以及propertyName
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
//设置动画属性值
anim.setFloatValues(values);
return anim;
}
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
//由于是通过属性字符串设置的动画,因此构建的是不带property的holder
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}
ObjectAnimator.ofFloat(textView, "alpha", 0f, 1f) 静态方法内部new了一个ObjectAnimator实例,ObjectAnimator继承自ValueAnimator(内部实现了动画值运算,封装了插值器),初始化给target以及属性值赋值,接着调用了setFloatValues,实现是为了创建属性动画涉及到的核心实例PropertyValuesHolder。简单介绍下PropertyValuesHolder以及Property
- Property是个抽象类,内部保存了属性名称以及属性类型,需要子类实现get以及set方法
- PropertyValuesHolder则持有Property,负责计算属性值,以及属性的更新。特别的如果只指定了name,没设置property,则在更新属性时会通过反射的形式调用对象方法更新对应属性;而如果设置了property,则会调用property的set方法更新属性。因此如果对view的现有属性进行更新,可以自定义property初始化animator,View已经默认支持了部分属性,例如上面例子可优化成ObjectAnimator.ofFloat(textView, View.ALPHA, 0f, 1f)。
public static final Property<View, Float> ALPHA = new FloatProperty<View>("alpha") {
@Override
public void setValue(View object, float value) {
object.setAlpha(value);
}
@Override
public Float get(View object) {
return object.getAlpha();
}
};
动画实现
动画的执行入口从animator.start()开始
private void start(boolean playBackwards) {
...
mStarted = true;
mPaused = false;
mRunning = false;
...
//添加动画回调
addAnimationCallback(0);
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
startAnimation();
}
}
private void addAnimationCallback(long delay) {
//调用animationHandler实例方法添加frame回调
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
在start方法调用中,主要是初始化设置了一些状态以及添加动画回调,如果是立即播放的动画则会调用startAnimation方法,但其内部也只是初始化了一些属性,将mRunning状态转为true,以及给PropertyValuesHolder初始化get/set方法。并没有实际开启动画的逻辑。动画的逻辑主要是在callback方法中,接下来我们看看AnimationHandler是什么以及addCallback做了些啥。
AnimationHandler是一个线程单例的类,其中持有了主要有以下几个属性:
- mDelayedCallbackStartTime:非立即执行的动画
- mAnimationCallbacks:所有注册的动画
- mProvider:动画帧回调的提供者,默认实现是MyFrameCallbackProvider,其中通过Choreographer触发实现动画帧通知
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//帧回调,在此方法里通知给所有的动画
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
//仅当有动画时才向Provider注册帧回调
if (mAnimationCallbacks.size() == 0) {
//默认为MyFrameCallbackProvider,实现是通过持有的Choreographer进行postFrameCallback
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
由上可知,所有动画的驱动都是依靠向Choreographer注册callback的doFrame回调,然后通知给对应animator,在其内部完成计算动画属性的工作。其核心方法为ValueAnimator.doAnimationFrame:
public final boolean doAnimationFrame(long frameTime) {
...
if (mPaused) {
mPauseTime = frameTime;
//如果暂停,移除callback,在调用resume时再次注册
removeAnimationCallback();
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
mStartTime += (frameTime - mPauseTime);
}
}
if (!mRunning) {
if (mStartTime > frameTime && mSeekFraction == -1) {
return false;
} else {
//在延迟开启的情况下,此时实际开始动画
mRunning = true;
//初始化动画
startAnimation();
}
}
if (mLastFrameTime < 0) {
if (mSeekFraction >= 0) {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false;
}
mLastFrameTime = frameTime;
final long currentTime = Math.max(frameTime, mStartTime);
//在此计算动画时间
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
//结束回调
endAnimation();
}
return finished;
}
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
...
//实际计算动画值
animateValue(currentIterationFraction);
}
return done;
}
在doAnimationFrame中我们可以看到里面处理了暂停、延迟开始、以及结束的操作,实际计算动画的代码则在animateValue中
//ObjectAnimator动画值处理
void animateValue(float fraction) {
...
//基类计算好动画值
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//通过PropertyValuesHolder设置属性(使用property或者反射调用)
mValues[i].setAnimatedValue(target);
}
}
//ValueAnimator基类
void animateValue(float fraction) {
//插值器处理
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//调用PropertyValuesHolder的计算方法计算得到动画值
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
//通知动画更新回调
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
到此,属性动画的整体流程就大致结束了,总结一下就是在调用start方法后,会将动画自身作为callback回调注册到AnimationHandler,handler在适时将自身作为frameCallback注册到Choreographer,当收到vsync信号回调doFrame会通知到动画做动画计算,计算完后给目标视图更新设置动画属性。
各类动画的运用场景
从以上实现分析以及动画对比,可以根据开发中的实际场景做出选择:
- 如果动画没有交互,例如弹窗弹出场景,视图展示场景,完全可以通过视图动画使用xml进行实现,代码量较少
- 需要对视图的一些特殊属性做出调整,例如颜色动画、尺寸高宽做修改的动画场景,则试图动画无法实现,需要使用属性动画实现
- 对于强交互型的动画,例如动画过程中需要根据视图实际位置响应触摸点击,那只能通过属性动画实现
- 文本实现数字增长滚动的类似非视图属性效果,也可以借助属性动画实现
总结
以上便是本篇文章的所有内容,对Android动画系统做了大致的介绍以及分析,但其中有许多细节的点没有展开进行深入解读,例如动画系统中涉及到的一些状态及核心属性的设计,属性动画中Choreographer、KeyFrame的相关知识,这些也都是跟动画息息相关的,后续如果有时间也会深入了解一波再做分享~
转载自:https://juejin.cn/post/7154269374369497119