從設計角度學習Android動畫

前言

一般來說,如果不是項目中經(jīng)常需要用到很多的動畫,大家可能只是對Android動畫的原理有一點點了解,比如Android的view動畫只是修改繪制,所以點擊事件還是留在原來的位置,比如,屬性動畫修改的是具體的屬性,所以點擊事件位置會隨著動畫的的改變而改變,似乎沒什么難以理解的。那么,你能回答以下問題嗎:

  1. 我們設定動畫執(zhí)行事件3s,那么3s鐘具體會刷新多少次?
  2. 假設3s內要刷新100次,那么在view中該如何執(zhí)行?循環(huán)嗎?
  3. 是誰負責計算3s內的每一個時間點的分配?
  4. 要是3s執(zhí)行沒有結束,view被回收了怎么辦?
  5. 如果是屬性動畫,每次刷新都會遍歷整顆view樹嗎?會有性能問題嗎?

以上問題是站在一個對動畫原理什么都不了解的情況下提出的,現(xiàn)在我們站在Android動畫設計者的角度來思考該如何實現(xiàn)這套動畫系統(tǒng)。

View補間動畫的設計

1. 動畫進度的控制

對于補間動畫來說,我們能知道開始時間 mStartTime, 以及持續(xù)時間 mDuration,那么我們將一次動畫過程看成從0到1的過程,在動畫持續(xù)時間內,任意時間的進度nomalizedTime 計算方式為:

normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration;

如果是一個平移動畫,向右平移30px,我們只需要 用 normalizedTime * 30 就可以得到當前時間應該平移多少距離了。這樣來看,似乎不需要動畫進度控制啊,這些邏輯都是通用的,其實不是如此。剛才提到的只是隨時間線性變化的動畫,如果我想得到加速平移的動畫呢?所以我們需要一個特定的角色能夠將正常的動畫進度轉換成我們需要的進度:

public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

TimeInterpolator就是這個角色,所以我們可以繼承它實現(xiàn)自己的動畫進度控制。

2. 動畫如何“動”起來

現(xiàn)在,有了能夠控制動畫進度的方式了,我們得想辦法“動”起來。

1) 用屁股想(這樣不用考慮可行性)

由于補間動畫只是在繪制時的效果,所以我們肯定得在view的draw()方法上下功夫,那么如果我們在draw()里面用循環(huán)繪制呢?比如在draw() 里面循環(huán)100次,每次執(zhí)行完休眠很短時間,這樣似乎可以達到動起來效果,或者我們不再draw()里面,而是在外面用循環(huán)100次調用draw()方法呢?
當然了,用屁股想的基本不可行,第一,我們無法確定應該循環(huán)多少次;第二,每次循環(huán)后休眠喚醒對性能影響太大,而且基本不可能做到動畫的平滑。

2) 用大腦想

既然循環(huán)調用draw()不可行,那么我們換其他方式調用呢?這個時候我們想想draw()還能被怎么調用?沒錯,我們可以調用invalidate()方法,這樣我們在draw()里面判斷有沒有動畫要執(zhí)行,有的話執(zhí)行動畫效果,然后調用invalidate(),這樣本次動畫效果就會在下一幀展示出來,下一次幀繪制時draw()里面檢測動畫還沒結束,又一次重復這個過程,這樣就完美的“動”了起來,無需多余的循環(huán)操作。當然,我們的動畫只是針對單個view的,我么只需要重繪這一部分區(qū)域就可以了,可以這樣調用:

 parent.invalidate(mLeft, mTop, mRight, mBottom);

這樣是一種優(yōu)化方式。

3. 如何實現(xiàn)動畫效果

首先,我們要支持不同的效果,而且還得支持用戶自定義動畫效果,那么每個效果肯定一般都有自己的實現(xiàn)類,我們要在自己的實現(xiàn)類內部實現(xiàn)動畫效果,要這樣做有兩種方式:

  • 暴露Canvas對象
    如果我們的實現(xiàn)類能拿到這個view的canvas對象,那么我們就能實現(xiàn)任意效果了,但這樣的設計模式有缺陷:
    首先僅僅canvas對象是不夠的,必須得有view的其他信息,比如寬,高等等,這樣才能設計出合理的動畫效果,但這樣暴露的信息太多,而且Canvas暴露出去后,我們如果要查view的繪制信息就不僅僅是從draw()方法能查的了,可能任意的動畫實現(xiàn)都會改變這個view,這樣非常不合理。
  • 修改特殊屬性
    既然第一種方案暴露的信息太多,那么我們就減少暴露的信息。因此,這里引入了一個暴露的信息類:
public class Transformation {
    /**
     * Indicates a transformation that has no effect (alpha = 1 and identity matrix.)
     */
    public static final int TYPE_IDENTITY = 0x0;
    /**
     * Indicates a transformation that applies an alpha only (uses an identity matrix.)
     */
    public static final int TYPE_ALPHA = 0x1;
    /**
     * Indicates a transformation that applies a matrix only (alpha = 1.)
     */
    public static final int TYPE_MATRIX = 0x2;
    /**
     * Indicates a transformation that applies an alpha and a matrix.
     */
    public static final int TYPE_BOTH = TYPE_ALPHA | TYPE_MATRIX;

    protected Matrix mMatrix;
    protected float mAlpha;
    protected int mTransformationType;
}

可以看到,這里只暴露了Matrix,用來做位移,旋轉,縮放之類的效果,暴露了mAlpha,實現(xiàn)透明度效果,我們所謂的各種動畫,都是修改這兩個值,值的生效都是在view的draw()方法中,這樣就很好的避免了上一種方案導致的Canvas對象到處飛的問題。

現(xiàn)在,有了動畫進度控制,有了設置通過Transformation修改draw的方法,那么我們具體的動畫實現(xiàn)則長這樣:

public abstract class Animation implements Cloneable {
    /**
     * The interpolator used by the animation to smooth the movement.
     */
    Interpolator mInterpolator;
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
    }
}

主要是這個getTransformation()方法,這個方法中,我們通過mInterpolator以及currentTime來計算當前動畫進度,然后修改 outTransformation,最后在view的draw()方法中拿到這個修改后的Transformation,讓動畫中的設置生效,這樣,動畫的一幀就完成了。

4.小結

有了上面的基礎,我們能夠回答剛開始提出的一些問題了:

  • 我們設定動畫執(zhí)行事件3s,那么3s鐘具體會刷新多少次?
    我們是在每一幀結束后安排下一幀的刷新,用的都是Android自帶的view刷新機制,所以,只要你的動畫不是耗時的,按照1s刷新60 幀計算,3s應該刷新180次,當然,這是理論值。
  • 假設3s內要刷新100次,那么在view中該如何執(zhí)行?循環(huán)嗎?
    并沒有任何循環(huán),完全都是正常的Android view刷新機制,只不過通常是刷新一次,這里是連續(xù)刷新而已。
  • 是誰負責計算3s內的每一個時間點的分配?
    并沒有任何人來分配時間,只有Interpolator根據(jù)當前時間來計算動畫進度。
  • 要是3s執(zhí)行沒有結束,view被回收了怎么辦?
    從原理上來說,Android的view動畫本質上是前一幀安排下一幀,沒有一個統(tǒng)一中心去安排,所以view被銷毀后,這個安排工作自然繼續(xù)不下去了,完全正常的view銷毀,在view的onDetachedFromWindowInternal()方法中會把這個view置空,不存在任何泄露可能,我們也不需要手動取消動畫。

Android屬性動畫

1. 從需求談起

上面我們分析了View的補間動畫,我們發(fā)現(xiàn)從機制上來說,該模式動畫只允許修改很少的東西,而且影響的僅僅是繪制時的位置,甚至連點擊事件還留在原來的位置,這樣的動畫是無法滿足現(xiàn)有的各種UI需求的,我們需要的是真正能修改view的各項屬性的動畫。
那么,問題來了,僅僅修改view的屬性嗎? 假設我有一個坐標,view的具體形態(tài)會受到這個坐標影響,但我希望動畫作用在這個坐標上,從而起到動畫效果怎么辦? 因此這套動畫設計不能著眼于view,而是著眼于一切的值,本質是隨著時間改變來改變這個值,view的屬性只是值的一種而已。

2. 動畫進度的控制

其實理論上補間動畫的控制方式完全能夠移植到這里來,沒有任何不適的地方。但是,理論是理論,主要是因為我們需要支持更多的效果: 比如一個值從1到10,然后又到5,而且我們希望1到10快,10到5慢,這樣的話TimeInterpolator 的 getInterpolation()其實就沒辦法達到這種效果了,因為它只能夠簡單的根據(jù)當前整個動畫進度(從0到1)來做調整,做不了我們這邊分段效果。好像還是不好理解,我們用一個例子講解下。
假設我們的動畫是這樣的:

ValueAnimator.ofInt(0,10,5)
         .setDuration(3*1000)
         .start();

效果就是動畫的值會隨時間推進,均勻的從0到10,然后又到5?,F(xiàn)在我們看下在某個時間點,這個值具體是怎么確定的呢?。
我們看張圖:


動畫進度

可以看到,我們傳了3個值,導致整個動畫被分成了兩部分,我們想要和補間動畫一樣簡單的計算當前的值就不行了,不過我們可以這樣做:

  • 判斷當前進度在哪一部分,比如當前是0.75f,那么我們在第二部分
  • 計算在第二部分進度: 0.75f - 0.5f = 0.25f ,對于這個0.25f, 我們還可以和補間動畫類似,調用TimeInterpolator修改
  • 計算具體的值,現(xiàn)在我們有了起始為10 ,終點為5,進度為0.25f,那么我們完全可以計算當前具體的值了,當然具體怎么計算我們引入一個新的角色:
public interface TypeEvaluator<T> {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);

}

現(xiàn)在,我們總結下上面的流程,不難發(fā)現(xiàn)關鍵是3個標紅的點,這是用來區(qū)分當前動畫在哪一部分的,我們將其定義為關鍵幀:

public abstract class Keyframe implements Cloneable {
    /**
     * 當前關鍵幀所在的動畫進度
     */
    float mFraction;

    /**
     * 關鍵幀之前的動畫進度控制器
     */
    private TimeInterpolator mInterpolator = null;

    /**
     * 獲取當前關鍵幀代表的值,比如上圖的0,10,5
     *
     */
    public abstract Object getValue();
}

然后我們需要一個角色對這些關鍵幀進行管理,根據(jù)動畫進度計算具體的值:

public interface Keyframes extends Cloneable {

    /**
     * 設置合適的TypeEvaluator,決定了在一部分動畫期間,知道起止點和進度如何計算具體的值
     */
    void setEvaluator(TypeEvaluator evaluator);

    /**
     * 根據(jù)當前進度,計算出最終的值
     */
    Object getValue(float fraction);

    /**
     * 獲取所有的關鍵幀
     */
    List<Keyframe> getKeyframes();
}

現(xiàn)在,利用關鍵幀分割動畫,判斷當前動畫進度在哪部分動畫內,然后利用TypeEvaluator計算該部分內真正的值,這樣我們就完成了動畫進度的計算以及該進度下動畫的值的計算。

3. 動畫效果維持

有了補間動畫的基礎,我們已經(jīng)有了結論:通過循環(huán)讓動畫動起來是非常不靠譜的 。view是通過在draw()方法中完成當前幀動畫,然后調用invalidate()方法安排下一次draw()的調用,這樣讓動畫持續(xù)動起來的。但是,屬性動畫是針對一個值的,而不是view的,這種方式明顯不靠譜,我們不可能去修改view的draw()。
雖說補間動畫的方式我們這里用不了,但是它提供了一個很好的思路:只要在完成動畫的當前幀后想辦法讓下一幀得到繪制就可以了 。比如補間動畫在draw()方法中完成當前幀的繪制,同時調用invalidate()讓下一幀動畫在屏幕下次刷新時得到了調用。
這個時候隆重的介紹下Android中的編舞者:Choreographer, 這個類能夠監(jiān)聽Android底層發(fā)出的垂直同步信號,從而刷新屏幕,具體的可以看下這篇文章 Android繪制原理之刷新機制, 用這個編舞者就能達到我們想要的效果,他能監(jiān)聽下一次的刷新信號:

 private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            //下一次Android刷新幀時會回調這個doFrame()方法
           // 這里執(zhí)行當前幀動畫
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                //繼續(xù)安排下一次監(jiān)聽,然后下一幀刷新時還會執(zhí)行這個callback,從而
                //保證了動畫不停的執(zhí)行下去。
                getProvider().postFrameCallback(this);
            }
        }
    };

有了這個mFrameCallback,我們在第一次開始時向Choreographer注冊這個回調監(jiān)聽,動畫執(zhí)行完這個mFrameCallback會自動再一次注冊,那么下一此屏幕需要刷新時這個回調中動畫還能繼續(xù)執(zhí)行,就能保證動畫不停的執(zhí)行下去了。

4. 動畫效果的實現(xiàn)

上面我們談的其實是如何在一段時間內不斷計算某個值,最終都是我們計算出來這個值在當前幀應該是多少,現(xiàn)在,我們應該把這個值設置到我們的目標字段上了。舉例來說,我們要修改的是一個View的Height,那么我們怎么告知這個動畫框架,我們最終需要修改view的Height呢?將View傳入動畫框架可以指定要修改的view是哪個,但正常做法沒辦法做到告知要修改的是哪個屬性,這個時候反射就是神器了,因為我們可以指定屬性的名字,然后通過反射去拿到這個屬性。 當然,Android還可以反射某個方法。

5. 小結

現(xiàn)在,經(jīng)過上面4步,我們能夠通過連續(xù)修改view的某些屬性從而讓view動起來了,那么,我們可以嘗試回答剛開始提出的問題了,前3個問題答案和補間動畫是一樣的,這里回答后面兩個問題:

  • 要是3s執(zhí)行沒有結束,view被回收了怎么辦?
    動畫框架持有view是通過弱引用,動畫進行是檢測到view被回收會自己停止動畫,不會有任何泄露。但是,如果你對動畫額外設置了匿名內部類的監(jiān)聽呢?像這樣:
bjectAnimator animator = ObjectAnimator.ofFloat(target, "rotationX", 0.0f, 360.0f, 180f)
                        .setDuration(30 * 1000);
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        super.onAnimationRepeat(animation);
                    }
                });
                animator.start();

由于匿名內部類監(jiān)聽器的存在,導致Activity不會被回收,因此在動畫持續(xù)時間內整個Activity都會處于泄露狀態(tài),但動畫結束后,動畫內部會清理掉監(jiān)聽,這個時候Activity就能正常被回收了,所以這個泄露是短暫的。當然,如果我們把動畫設置成無限循環(huán)模式,那么動畫永遠不會結束,Activity自然一直是泄露狀態(tài)了。歸根到底還是這個監(jiān)聽器壞事,如果不設置額外監(jiān)聽器是不會有任何內存泄露可能的。

  • 如果是屬性動畫,每次刷新都會遍歷整顆view樹嗎?會有性能問題嗎?
    從整個動畫框架來看,它只負責修改View的屬性(如果我們的動畫作用對象是View的話),沒有任何優(yōu)化,是否有優(yōu)化是看view機制的,基本上可以說代價還是很高的。

結語

本文此次介紹了Android中典型的兩種動畫機制,當然我沒有去介紹源碼實現(xiàn)流程,而是從設計角度上解決動畫框架的一個個棘手問題,我相信大家明白了各個問題是怎么解決的才是核心,具體的源碼只是對這些解決方案的一個組織和封裝而已,有了本文的基礎,大家看起源碼來應該是水到渠成的。
最后,由于個人能力原因,如果內容有誤,懇請指正。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容