Android Animation 執行原理

YXP發表於2018-04-09

在 Android 的 Animation 支援下,衍生出了 ScaleAnimation / AphlaAnimation / 3D Animation …… 及各種自定義 Animation 。Animation 優秀的設計簡化和方便了動畫的定製(當然也有一些不通用的程式碼)。

Animation 都是在 View 層就完成的,所以程式碼原理比較易懂。以下程式碼分析是建立在 AOSP 6.0 的原始碼上。

一、呼叫鏈路

關鍵鏈路 api :在 View 執行 Animation 時候需要的 api

// 初始化 Animation 中的 Transformation (變化)
void initializeInvalidateRegion(int left, int top, int right, int bottom);
// 獲取 Animation 執行後應該重繪的區域
void getInvalidateRegion(int left, int top, int right, int bottom, RectF invalidate, Transformation transformation);
// 根據當前時間獲取 Animation 中的 Transformation (變化)
boolean getTransformation(long currentTime, Transformation outTransformation,  float scale);
複製程式碼

呼叫路徑

Animation 的繪製是跟隨 View 的繪製週期。關鍵呼叫鏈路如下(中間省略大部分):

ViewRootImpl.performDraw() → ViewGroup.drawChild() → View.draw()

來到 View 的 draw 方法,與 Animation 相關的關鍵思路:

  1. Vsync 週期,呼叫 View draw 方法,當前 View Animation 不為空;
  2. Animation 未初始化,通過 initialize() 方法重置內部屬性,initializeInvalidateRegion() 方法,初始化 Animation 第一幀(受 FillBefore 影響),繼而發起 onAnimationStart() 通知;
  3. 根據當前時間通過 Animation.getTransformation() 方法,獲取屬性在時間軸上的變化和 Animation 是否結束。
  4. Animation 還需要繼續進行,根據 Animation 是否會引起繪製區域改變,請求父親相應重繪 api,以便下一幀 Animation 能繼續進行。
  5. 當 Animation 會改變矩陣,則將矩陣應用到 Canvas 或者是 RenderNode。
  6. 後續正常 draw 繪製流程,包括裁剪可用區域,背景、內容、前景。

關鍵程式碼和註釋 View.java

// 啟動 Animation
public void startAnimation(Animation animation) {
    // 設定開始時間為下一幀
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    // 觸發重繪
    invalidateParentCaches();
    invalidate(true);
}
// 設定 Animation
public void setAnimation(Animation animation) {
    mCurrentAnimation = animation;

    if (animation != null) {
        // 在螢幕是暗的情況下,如果 Animation 設定了下一幀啟動,由於不會觸發重繪,所以需要設定
        // 啟動時間為當前時間,否則 Animation 的啟動時間會等到螢幕亮起後被設定
        if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
            && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
            animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
        }
        animation.reset();
    }
}

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    
    /* 省略... */

    boolean more = false;
    final boolean childHasIdentityMatrix = hasIdentityMatrix();
    
    /* 省略... */

    Transformation transformToApply = null;
    boolean concatMatrix = false;
    final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
    // 獲取 Animation
    final Animation a = getAnimation();
    if (a != null) {
        // 進行餘下的 Animation
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        // parent.getChildTransformation() 獲取的矩陣就是 applyLegacyAnimation() 方法時
        // 操作的矩陣。(這個寫法真的很奇怪,因為 transformation 本身就可以直接跟著當前 view,程式碼中也
        // 沒有其他和父親耦合的邏輯)
        transformToApply = parent.getChildTransformation();
    } else {
        
        /* 省略... */
        
    if (transformToApply != null
        /* 省略... */
            if (transformToApply != null || !childHasIdentityMatrix) {
                /* 省略... */
                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            // 如果使用的是硬體繪製,將 Transform 變化結果應用到 renderNode
                         	renderNode
                             .setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // 如果使用的是軟體繪製,將 Transform 變化結果應用到 Canvas
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
	/* 省略... */
}
                
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
    if (!initialized) {
        // 初始化 Animation
        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);
        // 觸發 View onAnimationStart 回撥
        onAnimationStart();
    }
	
    final Transformation t = parent.getChildTransformation();
    // 獲取 Animation 對於矩陣的變化,more 代表 Animation 未結束
    boolean more = a.getTransformation(drawingTime, t, 1f);
    /* 省略... */
    
    if (more) {
        if (!a.willChangeBounds()) {
            // 當 Animation 不會涉及到本身檢視大小的改變時,重繪 view 本身大小區域
            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 {
             // 當 Animation 涉及到本身檢視大小的改變時,重繪 Animation 給出的重繪區域
            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;
}

複製程式碼

二、內部實現細節

有一些不為人知的邏輯,必須要通過了解內部實現!下面涉及的程式碼均在 Animation.java。

關鍵屬性

// 如果當 FillEnabled = true, FillBefore 的設定才能生效
void setFillEnabled(boolean fillEnabled);
// 在動畫結束之後保持最後一幀
void setFillAfter(boolean fillAfter);
// 在動畫開始之前將第一幀應用在 View 上
void setFillBefore(boolean fillBefore);
// 設定動畫開始時的延時,每一次迴圈都會延時
void setStartOffset(long startOffset);
// 設定重複模式,支援 RESTART 重新開始 / REVERSE 不斷反轉
void setRepeatMode(int repeatMode);
// 設定重複次數
void setRepeatCount(int repeatCount);
// 設定差值器,決定動畫在時間軸的變化率
void setInterpolator(Interpolator i);
複製程式碼

內部實現原理

View 給出的時間和開始時間和延時時間進行相減,在與設定的時長進行歸一化,將歸一化時間交給差值器處理,獲得變化率,再交給子類的 applyTransformation 去根據這個變化率對矩陣等屬性進行任意的變化。

關鍵 api

// 該方法是每次幀重繪都會呼叫
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 {
        // 瞬時變化
        normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
    }
	// 判斷動畫是否已經結束
    final boolean expired = normalizedTime >= 1.0f;
    mMore = !expired;
	// 如果沒有設定 FillEnable,歸一化時間一定會在[0,1]的範圍,也就是如果 FillBefore 設定成 false了
    // 仍然會提前繪製第一幀
    if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
	// 當歸一化時間在[0, 1],或者 FillBefore = true 或者 FillAfter = true,進行動畫內容填充
    if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            fireAnimationStart();
            mStarted = true;
            if (USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
        }
		// 將時間限制在[0,1]
        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();
        }
    }
	// OneMoreTime 用於繪製最後一幀,也就是 mPreviousTransformation。具體原因請看 
    // initializeInvalidateRegion() 方法
    if (!mMore && mOneMoreTime) {
        mOneMoreTime = false;
        return true;
    }

    return mMore;
}

// 真正根據變化率進行屬性變化的 api
protected void applyTransformation(float interpolatedTime, Transformation t) {
}

// 根據給定的區域進行初始化,名字是這樣,但真正做的,也就儲存了一下更新區域,在之後,有物件來獲取
// 更新區域時再使用。
public void initializeInvalidateRegion(int left, int top, int right, int bottom) {
    final RectF region = mPreviousRegion;
    region.set(left, top, right, bottom);
    // Enlarge the invalidate region to account for rounding errors
    region.inset(-1.0f, -1.0f);
    if (mFillBefore) {
        // 如果設定了 FillBefore,那麼就提前進行首幀動畫的應用,而不通過 getTransformation()方法
        // mPreviousTransformation 在這裡很關鍵,原因請看 getInvalidateRegion()
        final Transformation previousTransformation = mPreviousTransformation;
        applyTransformation(mInterpolator.getInterpolation(0.0f), previousTransformation);
    }
}

// 獲取動畫改變後需要重繪的區域
public void getInvalidateRegion(int left, int top, int right, int bottom,
                                RectF invalidate, Transformation transformation) {
	
    final RectF tempRegion = mRegion;
    final RectF previousRegion = mPreviousRegion;

    invalidate.set(left, top, right, bottom);
    // 將矩陣的變化應用到當前矩陣,確定矩陣對於大小的影響
    transformation.getMatrix().mapRect(invalidate);
    // Enlarge the invalidate region to account for rounding errors
    invalidate.inset(-1.0f, -1.0f);
    tempRegion.set(invalidate);
    // 將矩陣所需大小與之前儲存的更新區域取並集
    invalidate.union(previousRegion);

    previousRegion.set(tempRegion);
	// mTransformation 只是一個 temp 輔助交換邏輯
    final Transformation tempTransformation = mTransformation;
    final Transformation previousTransformation = mPreviousTransformation;
	// 交換 mPreviousTransformation 和當前 transformation. 這個 api 是 
    // view.applyLegacyAnimation() 方法時獲取 Animation 變化的時機,即獲取後馬上就繪製的。而這裡是
    // 把前一幀交換出去繪製,留下了當前幀,所以上述的 mOneMoreTime 的來由就在這裡,onMoreTime 把餘下的
    // 一幀展示。
    tempTransformation.set(transformation);
    transformation.set(previousTransformation);
    previousTransformation.set(tempTransformation);
}
複製程式碼

到這裡,整個 Animation 的關鍵程式碼已經分析結束了,類少易懂,作用大。裡面的設計可以說很好了,但是仍然有些差強人意的地方,就像是 mPreviousAnimation 和 mOneMoreTime(如果不真正咬文嚼字,重寫的時候就可能會遇到各種坑),其實不需要這樣做也能把動畫的幀完全顯示的。

三、設計模式分析

Animation 使用了模板方法的設計模式,定義了一個基類和關鍵動畫鏈路,由實現的子類去完成關鍵的動畫方法,讓整個動畫變得易於擴充,隱蔽實現細節。

Intepolator 差值器使用了策略模式,將差值器的演算法交給實現類完成,讓動畫在時間抽上的效果得以豐富多樣化。比如衍生出了 LinearIntepolator / AccelerateInterpolator / PathInterpolatorCompatBase(cubic-bezier)...

四、繼承類分析

AplhaAnimation

AplhaAnimation 繼承自 Animation,進行透明度動畫,關鍵屬性:mFromAlpha / mToAlpha。通過變化率改變 Transformation 中的 alpha 值。

// 重寫父類 Animation 方法
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    final float alpha = mFromAlpha;
    // 根據變化率求出當前 alpha 值,在設定給 Transformation。
    t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
}
複製程式碼

Transformation 中的 alpha 怎麼生效的?跟進 draw 方法裡看看!主要呼叫到的方法是,當硬體加速時,使用的是 RenderNode.setAlpha();當軟體加速時,是 canvas.saveLayoutAplha()方法,以及 paint.setAlpha() 方法。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    /* 省略... */
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
    /* 省略... */
    
    float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
    if (transformToApply != null
        || alpha < 1
        || !hasIdentityMatrix()
        || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
        if (transformToApply != null || !childHasIdentityMatrix) {
    		/* 省略... */

            if (transformToApply != null) {
                if (concatMatrix) {
                    // 應用動畫矩陣到 canvas 或者 RenderNode
                    if (drawingWithRenderNode) {
                        renderNode.setAnimationMatrix(transformToApply.getMatrix());
                    } else {
                        canvas.translate(-transX, -transY);
                        canvas.concat(transformToApply.getMatrix());
                        canvas.translate(transX, transY);
                    }
                    parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                }
				// 獲取在動畫 Transformation 中設定的 alpha 值
                float transformAlpha = transformToApply.getAlpha();
                if (transformAlpha < 1) {
                    alpha *= transformAlpha;
                    parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                }
            }
    		/* 省略... */
        }

        // 當 alpha 需要設定時
        if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
    		/* 省略... */
            if (!drawingWithDrawingCache) {
                final int multipliedAlpha = (int) (255 * alpha);
                if (!onSetAlpha(multipliedAlpha)) {
                    if (drawingWithRenderNode) {
                        // 如果是硬體加速,則通過 RenderNode.setAlpha 方法設定透明度
                        renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
                    } else if (layerType == LAYER_TYPE_NONE) {
                        // 如果是軟體繪製,則通過 Canvas.saveLayerApla 方法時啟用畫布新層時
                        // 設定透明度。
                        canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
                                              multipliedAlpha);
                    }
    /* 省略... */
    if (!drawingWithDrawingCache) {
    	/* 省略... */
    } else if (cache != null) {
		// 當需要繪製快取的內容時
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
    		/* 省略... */
            // 透明度設定到畫筆上
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // 合併layerPaint 本身設定的透明度和將要設定的透明度,設定到畫筆上
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }
    /* 省略... */
}
複製程式碼

ScaleAnimation

ScaleAnimation 繼承自 Animation ,縮放動畫的實現。關鍵通過 Matrix.setScale 完成。

@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);
    }
}
複製程式碼

AnimationSet

AnimationSet 繼承自 Animation,組合動畫的實現。通過 AnimationSet 可以實現多個同時執行,利用 StartOffset 也能實現序列,當然也有侷限,比如沒法設定迴圈、沒有整體的 repeatMode(自己倒騰一下就可以,比如搗騰下 normalizedTime)。

實現細節:遍歷孩子的 getTransformation() 方法,將 Transform 結果合成。

注意:(具體可以看程式碼)

  1. duration、 repeatMode、 fillBefore、 fillAfter 這些屬性是直接設定給子 Animation;
  2. repeatCount、fillEnabled 這些屬性不支援;
  3. startOffset、 shareInterpolator只能應用在 AnimationSet 本身;
  4. sharInterpolator 起效時候,子 Animation 的 sharInterpolator 失效;
public void initialize(int width, int height, int parentWidth, int parentHeight) {
    /* 省略 */
    // 這裡就可以看出哪些是被設定進孩子的
    for (int i = 0; i < count; i++) {
        Animation a = children.get(i);
        if (durationSet) {
            a.setDuration(duration);
        }
        if (fillAfterSet) {
            a.setFillAfter(fillAfter);
        }
        if (fillBeforeSet) {
            a.setFillBefore(fillBefore);
        }
        if (repeatModeSet) {
            a.setRepeatMode(repeatMode);
        }
        if (shareInterpolator) {
            a.setInterpolator(interpolator);
        }
        if (startOffsetSet) {
            long offset = a.getStartOffset();
            // 孩子在 AnimationSet 的 startOffset 上加上了自身的 startOffset
            a.setStartOffset(offset + startOffset);
            storedOffsets[i] = offset;
        }
        a.initialize(width, height, parentWidth, parentHeight);
    }
}

public boolean getTransformation(long currentTime, Transformation t) {
    final int count = mAnimations.size();
    final ArrayList<Animation> animations = mAnimations;
    final Transformation temp = mTempTransformation;

    boolean more = false;
    boolean started = false;
    boolean ended = true;

    t.clear();
	// 遍歷子 Animation 
    for (int i = count - 1; i >= 0; --i) {
        final Animation a = animations.get(i);

        temp.clear();
        more = a.getTransformation(currentTime, temp, getScaleFactor()) || more;
        // 合成子 Animation 的操作,Transformation.compose() 方法做的就是矩陣相乘,透明度相乘疊加
        // 裁剪區域疊加。
        t.compose(temp);

        started = started || a.hasStarted();
        ended = a.hasEnded() && ended;
    }
	
    if (started && !mStarted) {
        if (mListener != null) {
            mListener.onAnimationStart(this);
        }
        mStarted = true;
    }
	// 當所有的子動畫結束時,結束本身
    if (ended != mEnded) {
        if (mListener != null) {
            mListener.onAnimationEnd(this);
        }
        mEnded = ended;
    }

    return more;
}
複製程式碼

五、擴充實現示例

支援屬性 direction,指定初始執行方向為 FORWARDS / BACKWORDS,結合 RepeatMode 就能支援迴圈的四種模式。

實現方式:根據指定 direction 模式,取反向變化率,再進行變化。

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    // mDirection 為自定義屬性
    if (mDirection == BACKWORDS) {
        interpolatedTime = 1 - interpolatedTime;
    }
    // 動畫實現細節
}
複製程式碼

相關文章