天天看點

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插值器擷取到目前的加速度值,然後對視圖應用坐标變換,資料最後交由系統繪圖時使用,把我們的意圖顯示出來。      好了,頭好痛,歇一下吧,同學們,老師有點累了,你們自學一下哈,下課!!