天天看点

Android动画全解析(二)

     上一节课,我们讲了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非法。

Android动画全解析(二)

     好,到这里呢,我们对工程当的基本的构成有了一个初步的认识,下面我们就来分析它的实现。我们操作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在执行过程中,每次涉及到方法调用,都会往当前线程的方法栈中压入一个栈帧,栈帧由局部变量表、操作数栈、动态链接、返回地址构成,可以参考下图:

Android动画全解析(二)

     而变量的内存是直接分配在堆中的,方法调用时,通过栈中的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插值器获取到当前的加速度值,然后对视图应用坐标变换,数据最后交由系统绘图时使用,把我们的意图显示出来。      好了,头好痛,歇一下吧,同学们,老师有点累了,你们自学一下哈,下课!!