上一节课,我们讲了Demo中主界面那个滑动门效果的动画实现过程,这节课,我们要稍微深入一点,研究一些复杂一些的动画实现了。大家在网上随便百度一下,应该也可以搜到很多关于Android动画方面的知识,从中可以了解到Android动画的分类,有的说分为三种,分别是:补间动画、帧动画、属性动画,有的分为两种,分别是:Tween动画、Frame动画。其实分为几类都无所谓,我们从实现原理上来说明一下,一类的实现方式就是通过动画内容的不断改变产生的,就像播放电影一样,每帧的画面都不同,连接起来就是一个完整的动画了,应该这种也就叫帧动画吧;另一种呢,是画面内容不变,比如我们的Demo中的滑动门,整个动画过程中展示的都是一张图片,那么我们通过不断的改变这个View控件的属性,比如变换它的位置、放大、缩小、旋转、变形、透明度等等属性,使它在每一帧产生不同的界面,整个连起来产生的动画,这种应该就叫属性动画了。所以,Android动画从本质上讲,万变不离其宗,再如何改变都逃不出这两个范围,而复杂一些的动画,可能会把这两个方面的因素加在一起,产生一些更复杂的效果而已,但是实现上还是这两种。
好了,明白了Android动画的实现原理后,我们本节课呢,就选择一个比较简单的例子来展开我们的内容。这节课呢,我们要选取的例子是Demo当中的复杂动画的最后一个unzoom_out,大家可以先通过它的效果来大概猜一下它的实现过程。我们点击unzoom_out动画,界面的View以屏幕中心点为基准不断缩小,最后消失,动画完成后,View控件恢复成原样。没有平移,没有旋转,没有透明度,那么就是生成一个Animation类,然后在每次系统回调时,将我们生成好的Animation类的变化大小的数据传给framework,然后让它对当前视图重绘,最后连贯在一起,就成了我们看到的不断缩放的动画。理解这个过程呢,也要对Vsync信号在应用侧的Choreographer中的分发、执行有比较好的掌握,才能更好的理解它的原理,如果有哪位同学对Choreographer的执行原理还有不熟悉的,请回头学一下前面的课程:Android Choreographer源码分析,如果对基本的动画的框架性原理还有不理解的地方,请复习一下上一课的知识:Android动画全解析(一)。
好了,大概能猜出来的unzoom_out的动画的实现方式,我们就深入来分析一下它的实现。我们可以打开手机自带的绘制布局边界的功能,然后再点一下unzoom_out动画,可以看到,整个过程,中间的就只有一个View控件,我们对照代码来看一下,当前的复杂动画对应的是ComplexActivity类,它所加载的布局文件是activity_anim_complex,非常简单,就包含了一个ListView组件,unzoom_out动画的实现是通过xml方式来实现的,它的定义是unzoom_out.xml,我们把它的布局文件代码贴出来,方便后边的分析:
可以看到,它只定义了一个节点scale,那么我们在eclipse当中按快捷键Alt + / 就可以看出来,系统给我们提供的动画实现一共有五种,分别是:alpha、rotate、scale、set、translate,也就是属性动画对应的透明度、旋转、变形、集合、平移,其中的set是一个集合,它可以有多个子节点,将其他四种放在其中,组合成一个复杂的动画;而其他四种是不能在xml当中同时定义两种以上的节点属性的,我们可以试一下,就会报[2016-11-06 11:30:31 - Animation] Error in an XML file: aborting build. 动画文件的xml非法。
好,到这里呢,我们对工程当的基本的构成有了一个初步的认识,下面我们就来分析它的实现。我们操作unzoom_out动画的过程对应在代码中的,只有两句:Animation anim = AnimationUtils.loadAnimation(ComplexActivity.this, ContantValue.complex[position])、listView_anim_complex.startAnimation(anim),非常的简单,从这里也可以看到Android系统动画框架的强大,它把所有的工作都系统性的完成了,我们要使用它非常简单。当然这也有不好的地方,就是如果开发者不对它进行研究的话,那么根本看不到它的实现过程,就会对它的实现一点都不了解,而只会使用。我们搞开发的,不光要知其然,还要知其所以然,这样才能提高我们的能力,想一想,我们每次面试的时候,面试官一问动画,我们的回答都是会使用,那有什么竞争力??你会用,别人也会用啊!但是如果我们研究透了动画的实现原理,我们就可以在简历上清楚的写上精通Android动画框架,非常清楚它的系统层实现原理,那是一种什么概念,当然这样也不敢说怎么样,但是比大部分停留在应用层的人马上高出一个档次,如果有淘汰的话,那我们的命运肯定比他们长!
呵呵,又扯远了,我们现在就来研究一下unzoom_out的动画实现,两句代码,意思也非常清楚,第一句就是把我们定义好的xml文件转换成一个Animation对象,第二句就是把它应用在当前的View控件上。跟上一节课不一样的,上一节课,系统会不断的回调我们,所以我们在重写computeScroll()方法,在每次系统回调时,把我们的坐标数据传回给系统,这样达到改变View控件位置的目的。而这次,我们不需要重写任何方法,只需要一句startAnimation,从中我们也可以猜到,系统肯定是把生成的Animation对象保存在哪里了,每次回调时候,就不需要通知我们了,因为参数都定义好了,直接自己取就OK了。那我们来看一下,它是把Animation对象保存在哪里了,又是怎么取的呢?
整个过程我们就分成两步:1、生成Animation对象;2、调用startAnimation开始加载动画。
一、调用AnimationUtils.loadAnimation()方法成生Animation对象
这个方法的代码在AnimationUtils中:
/**
* Loads an {@link Animation} object from a resource
*
* @param context Application context used to access resources
* @param id The resource id of the animation to load
* @return The animation object reference by the specified id
* @throws NotFoundException when the animation cannot be loaded
*/
public static Animation loadAnimation(Context context, int id)
throws NotFoundException {
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
return createAnimationFromXml(context, parser);
} catch (XmlPullParserException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) parser.close();
}
}
整个方法的代码非常简洁,首先调用context.getResources().getAnimation(id)将我们的xml文件解析并生成一个XmlResourceParser对象,它里需要说一下,生成回来的XmlResourceParser对象当中,所有的xml的节点、属性都已经解析到这个parser当中了,我们如果要使用就直接从中取就可以了。解析的过程我们就不跟踪了,也是非常复杂,底层的实现是用C++的代码来实现的,应该也是考虑到性能的问题。这个过程和我们在Activity中加载布局文件使用的是同一套逻辑,大家可以断点一下,比如在Activity类的setContentView的代码行打一个断点,然后Debug运行,可以明显看到此句代码的执行比其他代码的耗时要长很多,肯定是非常耗时的,所以系统采用了执行性能比较高的C++的代码实现。所以呢,大家如果要解析xml方面的需求,不需要自己在写一套实现,就可以直接使用系统的现成框架了。生成好了parser对象之后,再调用createAnimationFromXml(context, parser),将我们当前的parser对象中的属性解析并进行封装,最后就得到我们的目标Animation对象了。我们来看一下createAnimationFromXml方法的实现:
private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
}
private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
Animation anim = null;
// Make sure we are on a start tag.
int type;
int depth = parser.getDepth();
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
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 {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
if (parent != null) {
parent.addAnimation(anim);
}
}
return anim;
}
我们当中的逻辑就是解析xml属性了,我们应该稍微熟悉一些。就是通过while循环,判断每个节点是什么,然后根据它的类型进行具体的解析和数据封装。首先调用parser.getDepth()获取当前parser的深度,这里也需要说一下,这个深度的意思是当前的xml对应根下面包含几个子节点,而不是把每一个子节点取出来,得到它的深度,然后对比得到一个最大深度值,大家要正确理解这个意思。比如放在我们当前的unzoom_out的例子中,取到的xml只包含一个scale节点,所以它的深度就是0,表示包含一个子节点,如果并行的还定义了两个translate、rotate节点的话,那么深度就是2,表示有三个直接的子节点。对于所有的xml动画定义来说,这里一般都是0,因为我们在xml中定义动画,只能有一个子节点,即使是定义set,也只能在set中包含其他节点,而在根节点下还是一个。然后通过while循环开始解析,先跳过起始标志XmlPullParser.START_TAG,调用String name = parser.getName()取每一个节点的名字,在我们当前的例子中,取出来的也就是scale了,然后anim = new ScaleAnimation(c, attrs)将anim对象实例化,最后(parent != null),第一次看到这块的代码,看了半天我都没明白系统的用意,这句代码是干什么用的呢?parent是方法参数传进来的,也没有返回,那么用它来添加当前的anim对象能干啥,添加完了又没有用?后来才明白,比如我们定义的是一个set集合,那么第一个节点就构造AnimationSet对象,然后循环继续循环调用createAnimationFromXml方法解析它下面的直接子节点,这时候传进来的参数parent就是第一层循环调用的对象了,也就是我们的目标Animation了,那它肯定要把后边生成的每个子节点的anim对象添加进去了,要不然,在执行动画时,子节点的属性没保存,系统从哪里获取呢?所以这里就是这个意思了。好了,我们继续往下走,来看一下ScaleAnimation的构造方法是如何创建一个ScaleAnimation对象的。
/**
* Constructor used when a ScaleAnimation is loaded from a resource.
*
* @param context Application context to use
* @param attrs Attribute set from which to read values
*/
public ScaleAnimation(Context context, AttributeSet attrs) {
super(context, attrs);
mResources = context.getResources();
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.ScaleAnimation);
TypedValue tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_fromXScale);
mFromX = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mFromX = tv.getFloat();
} else {
mFromXType = tv.type;
mFromXData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_toXScale);
mToX = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mToX = tv.getFloat();
} else {
mToXType = tv.type;
mToXData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_fromYScale);
mFromY = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mFromY = tv.getFloat();
} else {
mFromYType = tv.type;
mFromYData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_toYScale);
mToY = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mToY = tv.getFloat();
} else {
mToYType = tv.type;
mToYData = tv.data;
}
}
Description d = Description.parseValue(a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_pivotX));
mPivotXType = d.type;
mPivotXValue = d.value;
d = Description.parseValue(a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_pivotY));
mPivotYType = d.type;
mPivotYValue = d.value;
a.recycle();
initializePivotPoint();
}
先调用父类的带两个参数的构造方法,因为在父类中要进行一些必要的初始化,我们就继续跟进去看一下父类的构造方法:
/**
* Creates a new animation whose parameters come from the specified context and
* attributes set.
*
* @param context the application environment
* @param attrs the set of attributes holding the animation parameters
*/
public Animation(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Animation);
setDuration((long) a.getInt(com.android.internal.R.styleable.Animation_duration, 0));
setStartOffset((long) a.getInt(com.android.internal.R.styleable.Animation_startOffset, 0));
setFillEnabled(a.getBoolean(com.android.internal.R.styleable.Animation_fillEnabled, mFillEnabled));
setFillBefore(a.getBoolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));
setFillAfter(a.getBoolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));
setRepeatCount(a.getInt(com.android.internal.R.styleable.Animation_repeatCount, mRepeatCount));
setRepeatMode(a.getInt(com.android.internal.R.styleable.Animation_repeatMode, RESTART));
setZAdjustment(a.getInt(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL));
setBackgroundColor(a.getInt(com.android.internal.R.styleable.Animation_background, 0));
setDetachWallpaper(a.getBoolean(com.android.internal.R.styleable.Animation_detachWallpaper, false));
final int resID = a.getResourceId(com.android.internal.R.styleable.Animation_interpolator, 0);
a.recycle();
if (resID > 0) {
setInterpolator(context, resID);
}
ensureInterpolator();
}
先将系统xml文件中定义的com.android.internal.R.styleable.Animation属性取出来,解析为一个TypedArray对象,com.android.internal.R.styleable.Animation的定义在frameworks/base/core/res/res/values/attrs.xml文件中,这是系统提供的动画属性,它的定义代码如下: 这里将xml节点中定义的属性出来,然后解析并给Animation类的成员变量初始化赋值,最后判断(resID > 0)成立时,这里需要说明一下,大家要分清楚这里的判断,这里取的是对应的interpolator属性,如果用户有定义interpolator属性,则需要下面的解析,如果没有定义,则不需要解析了。调用setInterpolator(context, resID)给类成员变量mInterpolator赋值,它当中是根据我们xml中定义的Interpolator属性来生成一个插值器的,从上一节课中,我们都了解到,这个插值器对象也是非常重要的,它就是控制我们动画在每个百分比时间点的加速度的,所以我们继续分析一下插值器的生成过程。在Animation类中,是调用AnimationUtils.loadInterpolator(context, resID)来生成的,我们来看一下该方法的代码实现:
/**
* Loads an {@link Interpolator} object from a resource
*
* @param context Application context used to access resources
* @param id The resource id of the animation to load
* @return The animation object reference by the specified id
* @throws NotFoundException
*/
public static Interpolator loadInterpolator(Context context, int id) throws NotFoundException {
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
return createInterpolatorFromXml(context, parser);
} catch (XmlPullParserException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) parser.close();
}
}
private static Interpolator createInterpolatorFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
Interpolator interpolator = null;
// Make sure we are on a start tag.
int type;
int depth = parser.getDepth();
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
AttributeSet attrs = Xml.asAttributeSet(parser);
String name = parser.getName();
if (name.equals("linearInterpolator")) {
interpolator = new LinearInterpolator(c, attrs);
} else if (name.equals("accelerateInterpolator")) {
interpolator = new AccelerateInterpolator(c, attrs);
} else if (name.equals("decelerateInterpolator")) {
interpolator = new DecelerateInterpolator(c, attrs);
} else if (name.equals("accelerateDecelerateInterpolator")) {
interpolator = new AccelerateDecelerateInterpolator(c, attrs);
} else if (name.equals("cycleInterpolator")) {
interpolator = new CycleInterpolator(c, attrs);
} else if (name.equals("anticipateInterpolator")) {
interpolator = new AnticipateInterpolator(c, attrs);
} else if (name.equals("overshootInterpolator")) {
interpolator = new OvershootInterpolator(c, attrs);
} else if (name.equals("anticipateOvershootInterpolator")) {
interpolator = new AnticipateOvershootInterpolator(c, attrs);
} else if (name.equals("bounceInterpolator")) {
interpolator = new BounceInterpolator(c, attrs);
} else {
throw new RuntimeException("Unknown interpolator name: " + parser.getName());
}
}
return interpolator;
}
这里呢还是生把xml文件对应解析成一个XmlResourceParser对象,然后去查询每个节点下的名字,从解析过程中,我们也可以看到,我们在xml当中定义Interpolator的时候,只能使用系统提供的这几种,其他系统未提供的,是没有对应的xml的,如果用户乱写,则这里会抛出RuntimeException("Unknown interpolator name: " + parser.getName())解析异常。我们本例中定义的是一个android:interpolator="@android:anim/linear_interpolator",那么就生成一个interpolator = new LinearInterpolator(c, attrs)对象,我们来看一下LinearInterpolator的定义:
/**
* An interpolator where the rate of change is constant
*
*/
public class LinearInterpolator implements Interpolator {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
}
从定义当中也可以看出,它的实现非常简单,就是一个线性关系,当系统回调要取当前时间点的加速度值的时候,直接返回当前百分比input,也就是越来越快的意思,我们从unzoom_out动画的效果中也可以看出来,当视图越来越小的时候,速度也越来越快,不过差别不是那么明显,读者可以仔细对比一下。 好了,这里生成好了插值器之后,父类Animation当中必要的成员初始化就结束了,回到我们ScaleAnimation的构造方法当中,剩下的代码我们就不继续分析了,和父类中的过程基本相同,也是将com.android.internal.R.styleable.ScaleAnimation节点的属性全部取出来,然后对当前ScaleAnimation的所有类变量进行初始化。我们把com.android.internal.R.styleable.ScaleAnimation定义的代码贴出来,方便大家对ScaleAnimation有深入的认识: 其中的前四个定义fromXScale、toXScale、fromYScale、toYScale意思非常明显,就是起始点X比例、结束点X比例、起始点Y比例、结束点Y比例,我们可以试着改一下这几个相应的值,就可以看到明显的效果,比如把fromXScale改为0.8,那么动画一开始的时候,View控件的宽度明显就显示成当前宽度的80%了。而pivotX、pivotY这两个属性表示我们本次动画的目标中心点,就是它缩放过程中,以哪个点为中心点,这里要特别注意一下,当前所说的中心点的基准是当前动画对象的目标View控件为基准,也就是说当前构建的ScaleAnimation对象是要用在ListView控件上,那么中心点则以它的宽高作为基准,而不是屏幕的宽高基准,请大家一定要正确理解这两个参数的意思。 好了,到这里呢,我们的第一步就完成了,生成了一个ScaleAnimation对象。接下来,我们就继续分析第二步的实现。 二、调用listView_anim_complex.startAnimation(anim)将生成好的ScaleAnimation对象应用到我们的ListView控件上 startAnimation方法是在View类中实现的,它的代码如下:
/**
* Start the specified animation now.
*
* @param animation the animation to start now
*/
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
这个方法当的代码也非常清晰,调用animation.setStartTime(Animation.START_ON_FIRST_FRAME)给animation对象的成员变量赋值,然后将当前的方法参数animation保存到当前View控件的成员变量mCurrentAnimation中,然后设置PFLAG_INVALIDATED标志位,最后调用invalidate(true)使当的界面失效,发起重绘。乘下的入口过程就和一课相同了,在每次Vsync信号到来时,都会执行到View的draw方法,draw方法的代码我们就不重复贴了,因为代码逻辑太长,我们这里只看与我们当前的动画执行相关的逻辑。 在draw方法执行中,会调用final Animation a = getAnimation(),因为我们调用startAnimation方法时,已经将动画参数赋值到类变量中了,所以这里得到的a就不为空了,进入if分支,可以看到if分支的意图也很明显,就是给局部变量more、concatMatrix、transformToApply赋值,以便在后边使用。先来看一下第一步drawAnimation的调用。android版本在不断升级的同时,好多的地方也在不断的优化,这个方法我在公司的android系统源码中看到名字是叫applyLegacyAnimation,家里的系统源码是android4.4的,所以比较老,从这里也可以看出,android系统在不断的优化,名字都取的非常形象!那么这里呢,跟我们上节课大概一样,又是系统框架留给我们的一个入口,我们要实现的东西全部在这里实现好,系统来调我们,哈哈哈哈,真是太方便了!!好,我们来看一下这个方法的实现过程。
/**
* Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
* case of an active Animation being run on the view.
*/
private boolean drawAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
Transformation invalidationTransform;
final int flags = parent.mGroupFlags;
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
onAnimationStart();
}
final Transformation t = parent.getChildTransformation();
boolean more = a.getTransformation(drawingTime, t, 1f);
if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
if (parent.mInvalidationTransformation == null) {
parent.mInvalidationTransformation = new Transformation();
}
invalidationTransform = parent.mInvalidationTransformation;
a.getTransformation(drawingTime, invalidationTransform, 1f);
} else {
invalidationTransform = t;
}
if (more) {
if (!a.willChangeBounds()) {
if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
} else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {
if (parent.mInvalidateRegion == null) {
parent.mInvalidateRegion = new RectF();
}
final RectF region = parent.mInvalidateRegion;
a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
invalidationTransform);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
final int left = mLeft + (int) region.left;
final int top = mTop + (int) region.top;
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
又是一个非常复杂的方法,看到这种方法调用,想必大家都非常头疼,但是没有办法,因为这是一个系统,不是像我们写一个简单的应用,这些逻辑是避免不了的。我们还是看与我们的过程相关的逻辑。首先调用final boolean initialized = a.isInitialized()判断当前Animation对象是否进行了初始化,如果没有,就先执行它的初始化逻辑。isInitialized()方法的判断很简单,就是返回类变量mInitialized,我们来看一下何为初始化,也就是调用Animation的什么方法时,才算对它进行了初始化呢?可以看到只有initialize方法中会修改mInitialized类变量的值为true,我们来看一下这个方法的实现:
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mFromX = resolveScale(mFromX, mFromXType, mFromXData, width, parentWidth);
mToX = resolveScale(mToX, mToXType, mToXData, width, parentWidth);
mFromY = resolveScale(mFromY, mFromYType, mFromYData, height, parentHeight);
mToY = resolveScale(mToY, mToYType, mToYData, height, parentHeight);
mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
}
首先还是调用父类的initialize进行一些公用的初始化,然后再执行自己的逻辑。这个方法的意图也很明显,就是给当前ScaleAnimation的类变量mFromX、mToX、mFromY、mToY、mPivotX、mPivotY赋值,几个变量的赋值过程就是两个方法的调用过程:resolveScale和resolveSize,我们各举一个例子来分析一下这两个方法。分别以mFromX、mPivotY为例。先来看一下resolveScale的逻辑:
float resolveScale(float scale, int type, int data, int size, int psize) {
float targetSize;
if (type == TypedValue.TYPE_FRACTION) {
targetSize = TypedValue.complexToFraction(data, size, psize);
} else if (type == TypedValue.TYPE_DIMENSION) {
targetSize = TypedValue.complexToDimension(data, mResources.getDisplayMetrics());
} else {
return scale;
}
if (size == 0) {
return 1;
}
return targetSize/(float)size;
}
在mFromX的执行中,(float scale, int type, int data, int size, int psize),第一个scale就是我们当前ScaleAnimation的成员变量mFromX的值,也就是我们定义在xml中的值,当前就等于1.0;第二个参数type因为我们定义的fromXScale的值为浮点型,所以在构建ScaleAnimation对象时,设置fromXScale属性是执行的(tv.type == TypedValue.TYPE_FLOAT)分支,所以mFromXType是默认值private int mFromXType = TypedValue.TYPE_NULL,那么在resolveScale方法中也就执行else分支,直接返回当前的scale,也就是1.0。 好,我们再来看一下resolveSize方法的执行逻辑。
/**
* Convert the information in the description of a size to an actual
* dimension
*
* @param type One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
* Animation.RELATIVE_TO_PARENT.
* @param value The dimension associated with the type parameter
* @param size The size of the object being animated
* @param parentSize The size of the parent of the object being animated
* @return The dimension to use for the animation
*/
protected float resolveSize(int type, float value, int size, int parentSize) {
switch (type) {
case ABSOLUTE:
return value;
case RELATIVE_TO_SELF:
return size * value;
case RELATIVE_TO_PARENT:
return parentSize * value;
default:
return value;
}
}
它是直接根据传进来的type值,返回方法的计算结果。当前的mPivotYType在我们的xml定义中是50%,那么在ScaleAnimation的构造方法中,会执行d = Description.parseValue(a.peekValue( com.android.internal.R.styleable.ScaleAnimation_pivotY)),然后继续调用mPivotYType = d.type;mPivotYValue = d.value,那么执行完成后,mPivotYType的值为TypedValue.TYPE_FRACTION,TypedValue.TYPE_FRACTION的定义为public static final int TYPE_FRACTION = 0x06,化为十进制也就是6,mPivotYValue的值为0.5,所以在resolveSize方法当中,三个case ABSOLUTE、case RELATIVE_TO_SELF、case RELATIVE_TO_PARENT分别对应0、1、2都不符合,所以经过运算后,此方法的结果返回的就是value,也就是0.5。 好了,那么ScaleAnimation类的initialize方法执行完了,给该赋值的成员变量也赋值了。我们这里就可以总结一些点了,大家可以看到resolveScale是当前ScaleAnimation类自己实现的,而resolveSize方法是由父类来实现的,那就是说,resolveSize是所有动画的基本方法,才可以共用,而像alpha、rotate、scale等非共性的,你们子类就自己去处理吧。爸爸我只负责给老大、老二钱,养活你们,供你们上大学,至于老大想上清华大学,老二想上北京大学,爸爸我就不管了。 回到我们的主流程View.drawAnimation当中继续下面的分析。接下来调用a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop)来设置动画作用区域。首先final RectF region = mPreviousRegion将类变量赋值给局部变量region,类变量mPreviousRegion最初始的值是new构造出来的,然后region.set(left, top, right, bottom)设置动画的左、上、右、下四个点的位置,接着再赋值局部变量final Transformation previousTransformation = mPreviousTransformation,最后调用applyTransformation(mInterpolator.getInterpolation(0.0f), previousTransformation)将子类的动画数据收集起来。我们可以看到applyTransformation方法又是一个空实现,就是留着子类自己去实现的。好了,我们继续回到View类当中,继续调用boolean more = a.getTransformation(drawingTime, t, 1f)进行处理。我们来看一下这个方法的实现:
/**
* Gets the transformation to apply at a specified point in time. Implementations of this
* method should always replace the specified Transformation or document they are doing
* otherwise.
*
* @param currentTime Where we are in the animation. This is wall clock time.
* @param outTransformation A transformation object that is provided by the
* caller and will be filled in by the animation.
* @return True if the animation is still running
*/
public boolean getTransformation(long currentTime, Transformation outTransformation) {
if (mStartTime == -1) {
mStartTime = currentTime;
}
final long startOffset = getStartOffset();
final long duration = mDuration;
float normalizedTime;
if (duration != 0) {
normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
(float) duration;
} else {
// time is a step-change with a zero duration
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
final boolean expired = normalizedTime >= 1.0f;
mMore = !expired;
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
if (USE_CLOSEGUARD) {
guard.open("cancel or detach or getTransformation");
}
}
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
}
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
if (expired) {
if (mRepeatCount == mRepeated) {
if (!mEnded) {
mEnded = true;
guard.close();
fireAnimationEnd();
}
} else {
if (mRepeatCount > 0) {
mRepeated++;
}
if (mRepeatMode == REVERSE) {
mCycleFlip = !mCycleFlip;
}
mStartTime = -1;
mMore = true;
fireAnimationRepeat();
}
}
if (!mMore && mOneMoreTime) {
mOneMoreTime = false;
return true;
}
return mMore;
}
这里呢,还需要说一下,我们下来的主流程是View的drawAnimation方法,此方法在Vsync信号到来时,只要当前View的mCurrentAnimation对象不为空,就会一直执行,这也就是保证我们动画一帧一帧出现的原理,而上面分析的Animation类的初始化的相关方法是依赖于a.isInitialized()判断的,所以它只会执行一次。首先大家看一下这个方法的最后一个参数outTransformation,就这很能说明问题了,虽然这个方法返回值只是一个boolean值,但是实质上进行的相关运算结果都已经保存在这个参数当中了,外边是可以取到的,请大家一定要注意。这个方法中一些赋值逻辑我们就不分析了,我们主要来看一下final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime)、applyTransformation(interpolatedTime, outTransformation)这两句,第一句就是获取当前的加速度值,类变量mInterpolator在前面第一节构造Animation对象的初始化过程中已经详细说明了,大家如果没看懂,请回头看一下。取出当前的加速度后,作为参数传入applyTransformation方法当中。我们来看一下applyTransformation方法的代码实现:
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
float scale = getScaleFactor();
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
}
}
这里呢就是计算出两个局部参数sx、sy的值,最后设置到Transformation的类变量mMatrix当中去。这里大家要非常注意,可以看到参数也没有返回,上一步的方法调用完成,也没有对类变量重新赋值,那这些计算怎么起作用的呢?这里就涉及到C++的知识了,不能用我们Java的常理来考虑了。我们应该知道JVM在执行过程中,每次涉及到方法调用,都会往当前线程的方法栈中压入一个栈帧,栈帧由局部变量表、操作数栈、动态链接、返回地址构成,可以参考下图:
而变量的内存是直接分配在堆中的,方法调用时,通过栈中的reference引用指向堆中具体的内存,那么就是说,如果有多个变量指向同一块内存,我现在改变堆内存中的数据,那么这几个变量的值肯定都会相应修改了。此处计算完sx、sy之后,最终就是Matrix类的setScale方法,继续转调C++中的native_setScale方法把计算结果保存下来的,后边我们才可以取到,否则单纯按照java的模式,又没有返回值,又没有对类变量重新赋值,这样的计算完成后,没有任何效果,后边我们在取值的时候,拿到的根本就不是我们想要的结果了。 大家要仔细看一下这个方法,我们看到的视图变换的效果就是在这里进行实质性的计算的。看看下面两个分支的计算逻辑,目标值sx、sy就是根据我们在xml中定义的几个坐标位置计算,然后与插值器相乘得到的,这也就是ScaleAnimation的重点了。 if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
} 接下来的回到View类的drawAnimation方法中,scalingRequired在本例中是false,至于它的来例又有些复杂了,它是在ViewRootImpl类的setView方法中通过调用mAttachInfo.mScalingRequired = mTranslator != null赋值的,大家如果有兴趣可以跟踪一下。那么它为false,就执行else分支,将getTransformation方法中已赋值数据完成的Transformation对象t继续赋值给局部变量invalidationTransform,而more就是表示当前动画是否还在执行,动画还在执行则返回true,否则返回false,继续来看a.willChangeBounds(),当前的a是一个ScaleAnimation对象,它的willChangeBounds()方法是调用父类的,默认返回true,表示边界需要变化,我们可以看一下AlphaAnimation类,它的类代码如下:
/**
* An animation that controls the alpha level of an object.
* Useful for fading things in and out. This animation ends up
* changing the alpha property of a {@link Transformation}
*
*/
public class AlphaAnimation extends Animation {
private float mFromAlpha;
private float mToAlpha;
/**
* Constructor used when an AlphaAnimation is loaded from a resource.
*
* @param context Application context to use
* @param attrs Attribute set from which to read values
*/
public AlphaAnimation(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a =
context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AlphaAnimation);
mFromAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_fromAlpha, 1.0f);
mToAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_toAlpha, 1.0f);
a.recycle();
}
/**
* Constructor to use when building an AlphaAnimation from code
*
* @param fromAlpha Starting alpha value for the animation, where 1.0 means
* fully opaque and 0.0 means fully transparent.
* @param toAlpha Ending alpha value for the animation.
*/
public AlphaAnimation(float fromAlpha, float toAlpha) {
mFromAlpha = fromAlpha;
mToAlpha = toAlpha;
}
/**
* Changes the alpha property of the supplied {@link Transformation}
*/
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float alpha = mFromAlpha;
t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
}
@Override
public boolean willChangeTransformationMatrix() {
return false;
}
@Override
public boolean willChangeBounds() {
return false;
}
/**
* @hide
*/
@Override
public boolean hasAlpha() {
return true;
}
}
它就重写了父类的willChangeBounds()方法,返回false,从实现上也很容易理解,我们的旋转、变形、平移都是要变换边界的,而透明度动画就不需要了,所以它单独重写该方法并返回false。那么就继续执行else分支,调用a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region, invalidationTransform)将前面applyTransformation方法中保存的matrix矩阵的值取出来设置到region当中,修改PFLAG_DRAW_ANIMATION标志位,最后调用父元素的invalidate,重绘变形后的区域。在Vsync信号到来时,一帧一帧连起来,无效区域不断的变化,就形成我们看到的ScaleAnimation的动画了。 到这里呢,我们这节课就结束了,但是大家可以看一下,当前只分析完了drawAnimation方法,再回来主干道draw当中,下面还有一大段复杂的逻辑,我们就不跟进去了,有兴趣的同学请自己分析,有结果的话,也请指教我一下。我们的drawAnimation方法完成后,相关的数据都处理好封装在Transformation当中了,后面的draw过程就是对canvas的变换处理,然后执行绘制的逻辑,也是非常复杂的。大家如果能静下心来把这个方法理解透彻,那么对理解Android系统最核心的measure、layout、draw将会有非常大的帮助。 博客好长,估计大家看的都晕了,不知道咋回事,在手机浏览器上看的时候,每段代码片底下都有好大一段空白,不知道是手机浏览器问题还是CSDN代码片的问题。在这里呢,我们总结一下,这样更方面大家对ScaleAnimation有框架性的理解: 1、调用AnimationUtils.loadAnimation生成Animation对象,这步逻辑应该是比较清晰的,也很容易理解 2、将第一步生成的Animation对象保存在当前的View控件中,方便随时取用,最主要的就是在Vsync信号到来时,draw流程中给我们开了一个口子,它会先调用父类Animation的getTransformation方法,而在父类的getTransformation方法中又会调用子类实现的applyTransformation方法,这就是我们最主要实现scale变换的入口了,我们根据父类的成员变量mInterpolator插值器获取到当前的加速度值,然后对视图应用坐标变换,数据最后交由系统绘图时使用,把我们的意图显示出来。 好了,头好痛,歇一下吧,同学们,老师有点累了,你们自学一下哈,下课!!