上一节课,我们讲了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方式来实现的,它的定义是l,我们把它的布局文件代码贴出来,方便后边的分析:
可以看到,它只定义了一个节点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, ContantValueplex[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 = Resources().getAnimation(id);return createAnimationFromXml(context, parser);} catch (XmlPullParserException ex) {NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" HexString(id));rnf.initCause(ex);throw rnf;} catch (IOException ex) {NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" HexString(id));rnf.initCause(ex);throw rnf;} finally {if (parser != null) parser.close();}}
整个方法的代码非常简洁,首先调用Resources().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 = Depth();while (((type()) != XmlPullParser.END_TAG || Depth() > depth)&& type != XmlPullParser.END_DOCUMENT) {if (type != XmlPullParser.START_TAG) {continue;}String name = Name();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: " + Name());}if (parent != null) {parent.addAnimation(anim);}}return anim;}
我们当中的逻辑就是解析xml属性了,我们应该稍微熟悉一些。就是通过while循环,判断每个节点是什么,然后根据它的类型进行具体的解析和数据封装。首先调用Depth()获取当前parser的深度,这里也需要说一下,这个深度的意思是当前的xml对应根下面包含几个子节点,而不是把每一个子节点取出来,得到它的深度,然后对比得到一个最大深度值,大家要正确理解这个意思。比如放在我们当前的unzoom_out的例子中,取到的xml只包含一个scale节点,所以它的深度就是0,表示包含一个子节点,如果并行的还定义了两个translate、rotate节点的话,那么深度就是2,表示有三个直接的子节点。对于所有的xml动画定义来说,这里一般都是0,因为我们在xml中定义动画,只能有一个子节点,即使是定义set,也只能在set中包含其他节点,而在根节点下还是一个。然后通过while循环开始解析,先跳过起始标志XmlPullParser.START_TAG,调用String name = Name()取每一个节点的名字,在我们当前的例子中,取出来的也就是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 = Resources();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.le();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));Boolean(com.android.internal.R.styleable.Animation_fillEnabled, mFillEnabled));Boolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));Boolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));Int(com.android.internal.R.styleable.Animation_repeatCount, mRepeatCount));Int(com.android.internal.R.styleable.Animation_repeatMode, RESTART));Int(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL));Int(com.android.internal.R.styleable.Animation_background, 0));Boolean(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/l文件中,这是系统提供的动画属性,它的定义代码如下:
这里将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 = Resources().getAnimation(id);return createInterpolatorFromXml(context, parser);} catch (XmlPullParserException ex) {NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" HexString(id));rnf.initCause(ex);throw rnf;} catch (IOException ex) {NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" HexString(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 = Depth();while (((type()) != XmlPullParser.END_TAG || Depth() > depth)&& type != XmlPullParser.END_DOCUMENT) {if (type != XmlPullParser.START_TAG) {continue;}AttributeSet attrs = Xml.asAttributeSet(parser);String name = Name();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: " + Name());}}return interpolator;}
这里呢还是生把xml文件对应解析成一个XmlResourceParser对象,然后去查询每个节点下的名字,从解析过程中,我们也可以看到,我们在xml当中定义Interpolator的时候,只能使用系统提供的这几种,其他系统未提供的,是没有对应的xml的,如果用户乱写,则这里会抛出RuntimeException("Unknown interpolator name: " + Name())解析异常。我们本例中定义的是一个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, Width(), Height());a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);onAnimationStart();}final Transformation t = ChildTransformation();boolean more = a.getTransformation(drawingTime, t, 1f);if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {if (parent.mInvalidationTransformation == null) {parent.mInvalidationTransformation = new Transformation();}invalidationTransform = parent.Transformation(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 requestsparent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;parent.invalidate(mLeft, mTop, mRight, mBottom);}} else {if (parent.mInvalidateRegion == null) {parent.mInvalidateRegion = new RectF();}final RectF region = parent.InvalidateRegion(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 requestsparent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;final int left = mLeft + (int) region.left;final int top = mTop + (int) p;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,我们来看一下这个方法的实现: @Overridepublic 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 = TypedValueplexToFraction(data, size, psize);} else if (type == TypedValue.TYPE_DIMENSION) {targetSize = TypedValueplexToDimension(data, DisplayMetrics());} 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,最后调用Interpolation(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 durationnormalizedTime = 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 = Interpolation(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 = Interpolation(normalizedTime)、applyTransformation(interpolatedTime, outTransformation)这两句,第一句就是获取当前的加速度值,类变量mInterpolator在前面第一节构造Animation对象的初始化过程中已经详细说明了,大家如果没看懂,请回头看一下。取出当前的加速度后,作为参数传入applyTransformation方法当中。我们来看一下applyTransformation方法的代码实现: @Overrideprotected 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在执行过程中,每次涉及到方法调用,都会往当前线程的方法栈中压入一个栈帧,栈帧由局部变量表、操作数栈、动态链接、返回地址构成,可以参考下图: /*** 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}*/@Overrideprotected void applyTransformation(float interpolatedTime, Transformation t) {final float alpha = mFromAlpha;t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));}@Overridepublic boolean willChangeTransformationMatrix() {return false;}@Overridepublic boolean willChangeBounds() {return false;}/*** @hide*/@Overridepublic 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插值器获取到当前的加速度值,然后对视图应用坐标变换,数据最后交由系统绘图时使用,把我们的意图显示出来。 好了,头好痛,歇一下吧,同学们,老师有点累了,你们自学一下哈,下课!!
本文发布于:2024-02-04 11:28:59,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170706145555160.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |