Android的Tween動畫的實現框架

cauchyweierstrass的程式設計之路發表於2014-12-07

在寫程式的時候遇到了Tween動畫幾個問題:

1,  執行動畫的時候點選事件仍然在動畫開始的位置?

2,  XXXAnimation的構造引數裡面的值具體是什麼意思?

3,  平移動畫中fromXValue和toXValue旋轉動畫中fromDegrees和toDegrees取負值有什麼不同??(相信很多人也有疑惑)

4,  RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四個引數是怎麼確定旋轉原點的?確定的旋轉原點在哪裡?

Android動畫分為:

Tween Animation  View動畫也叫補間動畫

Drawable Animation  也叫Frame 幀動畫

Property Animation(3.0以後加入)

主要研究Tween動畫

我在寫程式的時候經常由於引數設定不當(主要是從多少度旋轉為多少度,有時是負的度數)得不到想要的效果。因此打算把動畫的實現框架研究一下。

研究之前請看這篇文章:Android中影像變換Matrix的原理  瞭解一下Matrix矩陣的相關知識。明白圖形的各種轉換就是要得到對應的變換矩陣。

首先說一下動畫的大概繪製框架過程,不然由於我寫的比較亂可能看暈了。

呼叫startAnimation會設定與View關聯的animation,然後會重繪檢視,重繪檢視的時候呼叫到drawChild,這時獲取與View繫結的Animation,不為null了,只要動畫時間沒有結束就會通過繪製的時間獲得變換矩陣,然後將畫布原點平移到檢視的左上角?(是不是這樣?)繪製新的一幀。繪製完又會重繪,然後獲取新的一幀的轉換矩陣…..迴圈下去,直到動畫結束就不再重繪檢視了。

回到View的onDraw函式裡面,onDraw函式做了如下工作。

1. Draw the background

2. If necessary, save the canvas’ layers toprepare for fading

3. Draw view’s content

4. Draw children

5. If necessary, draw the fading edges andrestore layers

6. Draw decorations (scrollbars forinstance)

當是ViewGroup的時候會執行第四步,dispatchDraw(canvas);

@Override
protected void dispatchDraw(Canvas canvas) {
// LayoutAnimationController比較熟悉,是讓ViewGroup的子控制元件有動畫效果,以前沒發現竟然也是在這裡發生的。
final LayoutAnimationController controller = mLayoutAnimationController;
		...
       // We will draw our child's animation, let's reset the flag
//下面對子View動畫進行處理。
        mPrivateFlags &= ~DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
        boolean more = false;
        final long drawingTime = getDrawingTime();
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
		...
    }

肯定會執行到drawChild(canvas, child, drawingTime); 在該函式顧名思義就是繪製子控制元件。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        boolean more = false;

        final int cl = child.mLeft;
        final int ct = child.mTop;
        final int cr = child.mRight;
        final int cb = child.mBottom;

        final int flags = mGroupFlags;

        Transformation transformToApply = null;
//取得該View繫結的動畫
        final Animation a = child.getAnimation();
        boolean concatMatrix = false;
//如果該View有了動畫那麼就會進入if判斷執行,沒有動畫就僅僅繪製該控制元件。
        if (a != null) {
            if (mInvalidateRegion == null) {
                mInvalidateRegion = new RectF();
            }
            final RectF region = mInvalidateRegion;

            final boolean initialized = a.isInitialized();
            if (!initialized) {
//呼叫Animation的初始化函式,在這面會解析Animation的各個引數。對不同的xy型別和值進行轉換。
                a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
                a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
                child.onAnimationStart();
            }

            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
//取得變換(平移,旋轉或縮放等)資訊,傳進去的drawingTime代表了繪製的時間毫秒值,取得的結果放進mChildTransformation裡面。
//mChildTransformation是一個圖形轉換資訊的類,包含了一個矩陣Matrix,和alpha值。Matrix就是圖形轉換矩陣。
//more是該函式的返回值,檢視程式碼很容易分析出來:如果動畫沒有結束就一隻返回true,知道動畫結束返回false。
            more = a.getTransformation(drawingTime, mChildTransformation);
            transformToApply = mChildTransformation;
//預設返回true
            concatMatrix = a.willChangeTransformationMatrix();
//more==true進入迴圈
            if (more) {
			//more==true when the animation is not over
                if (!a.willChangeBounds()) {
                    if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
                            FLAG_OPTIMIZE_INVALIDATE) {
                        mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
                    } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
                        mPrivateFlags |= DRAW_ANIMATION;
//動畫沒有結束就會不停呼叫invalidate函式對動畫view進行重繪
                        invalidate(cl, ct, cr, cb);
                    }
                } else {
                    a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
                    mPrivateFlags |= DRAW_ANIMATION;
                    final int left = cl + (int) region.left;
                    final int top = ct + (int) region.top;
//動畫沒有結束就會不停呼叫invalidate函式對動畫view進行重繪
                    invalidate(left, top, left + (int) region.width(), top + (int) region.height());
                }
            }
        } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
                FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
            final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
            if (hasTransform) {
                final int transformType = mChildTransformation.getTransformationType();
                transformToApply = transformType != Transformation.TYPE_IDENTITY ?
                        mChildTransformation : null;
                concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
            }
        }
		...
        child.computeScroll();
//分析簡單情況下檢視都是在可視範圍內,sx和sy應該等於0??
        final int sx = child.mScrollX;
        final int sy = child.mScrollY;
		...
        final boolean hasNoCache = cache == null;
        final int restoreTo = canvas.save();
        if (hasNoCache) {
            canvas.translate(cl - sx, ct - sy);
        } else {
        //here translate the canvas's zuobiao???? @auth:qhyuan
//將畫布平移到(cl,ct)點,cl和ct是childView的左上角到螢幕(0,0)點的距離。這點非常重要,在重繪動畫的時候畫布的左邊在發生變化!!!並不是一直在螢幕的(0,0)點。
//平移後的座標體系和最初不一樣,一般情況下座標的原點會移動至View的左上角。
            canvas.translate(cl, ct);
            if (scalingRequired) {
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }
        float alpha = 1.0f;
        if (transformToApply != null) {
            if (concatMatrix) {
                int transX = 0;
                int transY = 0;
                if (hasNoCache) {
                    transX = -sx;
                    transY = -sy;
                }
//兩個引數為0
                canvas.translate(-transX, -transY);
// transformToApply是從Animation取得的轉換資訊類,取得變換矩陣。這個變換矩陣在不同時刻都不一樣,因為傳過去的drawingTime不一樣。
//對畫布進行變換矩陣轉換,實現動畫效果。
                canvas.concat(transformToApply.getMatrix());
                canvas.translate(transX, transY);
                mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
            }
		...
            if (alpha < 1.0f && hasNoCache) {
                final int multipliedAlpha = (int) (255 * alpha);
                if (!child.onSetAlpha(multipliedAlpha)) {
                    canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
                            Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                } else {
                    child.mPrivateFlags |= ALPHA_SET;
                }
            }
        } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
            child.onSetAlpha(255);
        }
        ...
        return more;
    }

接下來看一下Animation類:動畫類裡面有兩個重要的函式protected void applyTransformation(float interpolatedTime,Transformation t)

和public boolean getTransformation(long currentTime, TransformationoutTransformation);

Transformation類包含了一個變換矩陣和alpha值。

applyTransformation函式:傳入一個差值時間,會填充一個Transformation類。會在getTransformation函式裡面呼叫,Animation類的applyTransformation是個空實現,具體的XXXAnimation在繼承自Animation時會實現applyTransformation函式。

getTransformation函式:會在drawChild函式裡面呼叫。

public boolean getTransformation(long currentTime, Transformation outTransformation) {
// currentTime 會在drawChild函式中通過getDrawTime傳過來
        if (mStartTime == -1) {
            mStartTime = currentTime;
        }

        final long startOffset = getStartOffset();
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
//歸一化時間,這樣normalizedTime是介於0和1之間的值。
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }
// expired表示“過期”,如果歸一化時間大於1 ,expired == true,即expire表示動畫結束了
        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) {
                if (mListener != null) {
//記錄開始動畫
                    mListener.onAnimationStart(this);
                }
                mStarted = true;
            }

            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

            if (mCycleFlip) {
                normalizedTime = 1.0f - normalizedTime;
            }
//通過歸一化的時間得到插值時間,類似於一個函式f(t)根據歸一化的時間得到插值時間。
//插值時間的作用就是得到變化速率改變的效果,例如線性插值就是f(t)=t
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//呼叫applyTransformation函式,具體實現在繼承自Animation的類中實現
//簡單地說就是傳入插值時間,然後該函式根據插值時間填充具體的轉換矩陣,不同的時刻對應不同的轉換矩陣,通過該轉換矩陣就會繪製出在不同位置的圖形。
            applyTransformation(interpolatedTime, outTransformation);
        }
//如果動畫結束了會執行下面
        if (expired) {
            if (mRepeatCount == mRepeated) {
                if (!mEnded) {
                    mEnded = true;
                    if (mListener != null) {
                        mListener.onAnimationEnd(this);
                    }
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                if (mListener != null) {
                    mListener.onAnimationRepeat(this);
                }
            }
        }

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }
//通過分析發現總是返回true,除非動畫結束了。
//always return true until the expired==true(the animation is over) @auth:qhyuan
        return mMore;
    }

applyTransformation函式在Animation裡面預設是空實現,需要在子類中實現,也就是說自定義動畫需要實現applyTransformation函式。

插值類也很簡單,是一個介面,只有一個函式。

public interface Interpolator {
   float getInterpolation(float input);
}

常用的子類有:

AccelerateDecelerateInterpolator 在動畫開始與結束的地方速率改變比較慢,在中間的時候加速

AccelerateInterpolator  在動畫開始的地方速率改變比較慢,然後開始加速

AnticipateInterpolator 開始的時候向後然後向前甩

AnticipateOvershootInterpolator 開始的時候向後然後向前甩一定值後返回最後的值

BounceInterpolator   動畫結束的時候彈起

CycleInterpolator 動畫迴圈播放特定的次數,速率改變沿著正弦曲線

DecelerateInterpolator 在動畫開始的地方快然後慢

LinearInterpolator   以常量速率改變

OvershootInterpolator    向前甩一定值後再回到原來位置

也可以自定義interpolator,只需要實現getInterpolation函式就可以了。

前面簡單的介紹了一下動畫繪製所涉及的一些函式,接下來以執行平移動化為例將動畫的執行過程走一遍:

1.定義完XXXAnimation後執行View的startAnimation

public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
//設定Animation域
        setAnimation(animation);
//請求重繪檢視
        invalidate();
}

setAnimation函式如下:

public void setAnimation(Animation animation) {
//將animation設定給View的mCurrentAnimation屬性
        mCurrentAnimation = animation;
        if (animation != null) {
            animation.reset();
        }
}

2.然後請求重繪檢視會執行onDraw函式,最後必然會執行到dispatchDraw函式,又會執行到drawChild(canvas,child, drawingTime); drawingTime函式是這次繪製的時間毫秒值。drawChild函式前面解釋過。

3.首先獲取View所關聯的Animation,然後呼叫Animation的初始化函式,在這面會解析Animation的各個引數。對不同的xy型別和值進行轉換,initialize函式也會在Animation的子類中實現。TranslateAnimation中的initialize函式如下,在該函式裡面直接呼叫resolveSize函式解析構造引數中的值。

public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);
        mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);
        mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight);
        mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight);
    }

resolveSize函式如下,根據是絕對座標、相對控制元件自身還是相對父控制元件和具體的值解析出解析後的值,很容易看出絕對座標,直接返回對應值,相對自身和相對父控制元件就是用相對值結餘0到1之間的值乘以子控制元件或者父檢視寬高的值。

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;
        }
}

4.然後就根據重繪的時間毫秒值通過getTransformation函式獲得對應的轉換矩陣。在這個函式裡面會先呼叫interpolatedTime = mInterpolator.getInterpolation(normalizedTime);根據時間獲得插值時間。

然後呼叫applyTransformation函式,TranslateAnimation的該函式具體如下:

protected void applyTransformation(float interpolatedTime, Transformation t) {
        float dx = mFromXDelta;
        float dy = mFromYDelta;
//開始X座標值和結束值不一樣
        if (mFromXDelta != mToXDelta) {
//某個時刻(插值時間)對應的dx,如果是線性插值interpolatedTime和normalizedTime是一樣的。
            dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
        }
        if (mFromYDelta != mToYDelta) {
            dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
        }
//對轉換矩陣進行重新設定,將位移差設定進轉換矩陣中。平移動化其實也是變換矩陣和原來的座標點的相乘。
        t.getMatrix().setTranslate(dx, dy);
}

只要該動畫沒有結束getTransformation函式會一直返回true。然後判斷返回true又會呼叫invalidate函式,接下來就是重複2,3,4的步驟了,但是重複執行的時候繪製時間不一樣,於是獲得的轉換矩陣不一樣,得到的新的檢視的位置就不一樣。如果返回false說明動畫執行完成了,就不在重繪檢視了。繪製控制元件時前面說過在繪製檢視的時候會呼叫canvas.translate(cl – sx, ct – sy);簡單地說就是將畫布的座標體系從螢幕左上角移動至動畫檢視的左上角。然後每次動畫的時候canvas.concat(transformToApply.getMatrix());對話不的矩陣轉換操作,然後繪圖就實現了對動畫的轉換。

整個過程稍微有點複雜,有些函式我還沒看的很明白,不過大致的思路就是這樣。

回頭看開始提出的幾個問題:

1,  執行動畫的時候其實並不是該控制元件本身在改變,而是他的父View完成的。startAnimation(anim)其實是給這個View設定了一個animation,而不是進行實際的動畫繪製。他的位置其實根本沒有改變,還是有layout所指定的位置決定的。

2,  引數的意思在程式碼裡面的註釋解釋過了。

3,  fromXValue和toXValue旋轉動畫中fromDegrees和toDegrees取正負值是有區別的,具體要看程式碼裡面,轉換矩陣是怎麼生成的。比如平移動化裡面:

dx = mFromXDelta + ((mToXDelta - mFromXDelta) *interpolatedTime);

插值時間從0變到1,假設現在時相對自身的型別,控制元件本身寬度為100,mFromXDelta這些值在初始化的時候已經解析成了實際值(從0~1.0這種相和相對自身還是相對父View變成了畫素寬高度)

從0變到1.0f的效果是:從當前位置向右平移100,原因是第一幀影像的dx為0,最後一幀dx為100

從-1.0f變到0的效果是:從當前位置的左邊100處向右平移到當前位置,原因是第一幀影像的dx為-100,最後一幀的dx為0

旋轉動畫中通過指定開始結束角度的正負實現順時針和逆時針旋轉是類似的道理。大家可以自行感悟一下下面四個動畫的區別,來看一下正負始末值對旋轉的影響。

rotateAnimation= new RotateAnimation(0, 90, Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT,0);
rotateAnimation= new RotateAnimation(90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
rotateAnimation= new RotateAnimation(-90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
rotateAnimation = new RotateAnimation(0, -90,Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT, 0);

4,  RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四個引數的問題:

由於前面說過了,動畫的時候,畫布平移到了View的左上角,因此對於旋轉動畫來說參考的座標原點始終是View左上角。而旋轉的旋轉點是可以任意指定的,該旋轉點參考的座標應該是View左上角。

手機螢幕中心處有一個點,現在我們想以螢幕最左邊的那一邊的中點為圓心旋轉360度,程式碼應該是:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, 0);//注意是-0.5f

以螢幕左上角旋轉:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, -0.5f);//注意是負的。

以自己中心旋轉:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);

以自己最下面的中點旋轉:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 1.0f);

其他旋轉點以此類推。我以前只是大致看過這些引數,所以在寫圍繞螢幕左上角旋轉的程式碼時,想當然寫成了:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT, 0,Animation.RELATIVE_TO_PARENT, 0);

我本來想相對於父View來說(0,0)就是螢幕的左上角那一個點。結果當然不正確,測試一下也可以看出這個時候的旋轉點仍然是View的左上角,是不是和RELATIVE_TO_PARENT沒有半點關係?

總結一下就是說:後面的四個引數只是能算出來相對於畫布座標的距離,僅此而已,並不能看到是相對父View的值就忘了畫布的原點在哪。

相關文章