Android 動畫原理

鴨脖發表於2012-07-16

簡介:

        Android 平臺提供了一套完整的動畫框架,使得開發者可以用它來開發各種動畫效果。Android 動畫框架詳解由原理篇和例項篇兩部分組成。本文是第一部分原理篇,主要分析 Tween 動畫的實現原理, 最後簡單介紹在 Android 中如何通過播放 Gif 檔案來實現動畫。第二部分例項篇將在原理篇的基礎上,向您展示一個動畫例項的實現。

 

Android 平臺提供了一套完整的動畫框架,使得開發者可以用它來開發各種動畫效果,本文將向讀者闡述 Android 的動畫框架是如何實現的。 任何一個框架都有其優勢和侷限性,只有明白了其實現原理,開發者才能知道哪些功能可以利用框架來實現,哪些功能須用其他途徑實現。Android 平臺提供了兩類動畫,一類是 Tween 動畫,即通過對場景裡的物件不斷做影像變換 ( 平移、縮放、旋轉 ) 產生動畫效果;第二類是 Frame 動畫,即順序播放事先做好的影像,跟電影類似。本文是由兩部分組成的有關 Android 動畫框架詳解的第一部分原理篇, 主要分析 Tween 動畫的實現原理, 最後簡單介紹在 Android 中如何通過播放 Gif 檔案來實現動畫。我們先看一下動畫示例來一點感性認識。

Android 動畫使用示例

使用動畫示例程式的效果是點選按鈕,TextView 旋轉一週。讀者也可以參看 Apidemos 中包 com.example.android.apis.animationview 下面的 Transition3d 和 com.example.android.apis.view 下面的 Animation1/Animation2/Animation3 示例程式碼。

清單 1. 程式碼直接使用動畫

  1. package com.ray.animation;   
  2. import android.app.Activity;   
  3. import android.os.Bundle;   
  4. import android.view.View;   
  5. import android.view.View.OnClickListener;   
  6. import android.view.animation.AccelerateDecelerateInterpolator;   
  7. import android.view.animation.Animation;   
  8. import android.view.animation.RotateAnimation;   
  9. import android.widget.Button;   
  10.   
  11. public class TestAnimation extends Activity implements OnClickListener  
  12. {   
  13.     public void onCreate(Bundle savedInstanceState)  
  14.     {   
  15.         super.onCreate(savedInstanceState);   
  16.         setContentView(R.layout.main);   
  17.         Button btn =(Button)findViewById(R.id.Button);   
  18.         btn.setOnClickListener(this);   
  19.     }  
  20.   
  21.     public void onClick(View v)  
  22.     {   
  23.         Animation anim=null;   
  24.         anim=new?RotateAnimation(0.0f,+360.0f);   
  25.        anim.setInterpolator(new AccelerateDecelerateInterpolator());   
  26.        anim.setDuration(3000);   
  27.        findViewById(R.id.TextView01).startAnimation(anim);   
  28.     }   
  29. }   


 

使用 XML 檔案方式,在開啟 Eclipse 中新建的 Android 工程的 res 目錄中新建 anim 資料夾,然後在 anim 目錄中新建一個 myanim.xml( 注意檔名小寫 ),內容如下 :


圖 1. 使用 xml 檔案方式

  1. <?xml version="1.0" encoding="utf-8"?>  
  2.  <set xmlns:android="http://schemas.android.com/apk/res/android">  
  3.  <rotate   
  4.     android:interpolator="@android:anim/acclerate_decelerate_interpolator"  
  5.     android:formDegress="0"  
  6.     android:toDegress="+360"  
  7.     android:duration="3000" />  
  8.       
  9. <!--rotate 旋轉動畫效果  
  10.     屬性:  
  11.    interpolator 指定一個動畫的插入器,用來控制動畫的速度變化  
  12.    fromDegress  動畫起始時物件的角度  
  13.    toDegress    動畫結束時物件的旋轉角度,正代表順時針  
  14.    duration     動畫的持續時間,以毫秒為單位-->  
  15.   
  16. /set>  


 

其中的 java 程式碼如下:

  1. package com.ray.animation;   
  2. import android.app.Activity;   
  3. import android.os.Bundle;   
  4. import android.view.View;   
  5. import android.view.View.OnClickListener;   
  6. import android.view.animation.Animation;   
  7. import android.view.animation.AnimationUtils;   
  8. import android.widget.Button;   
  9. import android.widget.TextView;   
  10. public class TestAnimation extends Activity implements OnClickListener  
  11. {   
  12.    public void onCreate(Bundle savedInstanceState)  
  13.    {   
  14.        super.onCreate(savedInstanceState);   
  15.        setContentView(R.layout.main);   
  16.        Button btn =(Button)findViewById(R.id.Button01);   
  17.        btn.setOnClickListener(this);   
  18.    }   
  19.   
  20.    @Override   
  21.    public void onClick(View v)  
  22.    {   
  23.       Animation anim=AnimationUtils.loadAnimation(this,R.anim.my_rotate_action);   
  24.     findViewById(R.id.TextView01).startAnimation(anim);   
  25.    }   
  26. }   

 

Android   動畫框架原理

現有的 Android 動畫框架是建立在 View 的級別上的,在 View 類中有一個介面 startAnimation 來使動畫開始,startAnimation 函式會將一個 Animation 類別的引數傳給 View,這個 Animation 是用來指定我們使用的是哪種動畫,現有的動畫有平移,縮放,旋轉以及 alpha 變換等。如果需要更復雜的效果,我們還可以將這些動畫組合起來,這些在下面會討論到。

要了解 Android 動畫是如何畫出來的,我們首先要了解 Android 的 View 是如何組織在一起,以及他們是如何畫自己的內容的。每一個視窗就是一棵 View 樹,下面以我們寫的 android_tabwidget_tutorial.doc 中的 tab 控制元件的視窗為例,通過 android 工具 hierarchyviewer 得到的視窗 View Tree 如下圖 1 所示:


圖 2. 介面 View 結構圖
介面 View 結構圖 

圖 3. 介面 View 結構和顯示對應圖
介面 View 結構和顯示對應圖圖 

其實這個圖不是完整的,沒有把 RootView 和 DecorView 畫出來,RootView 只有一個孩子就是 DecorView,這裡整個 View Tree 都是 DecorView 的子 View,它們是從 android1.5/frameworks/base/core/res/res/layout/screen_title.xml 這個 layout 檔案 infalte 出來的,感興趣的讀者可以參看 frameworks\policies\base\phone\com\android\internal\policy\Imp\PhoneWindow.java 中 generateLayout 函式部分的程式碼。我們可以修改佈局檔案和程式碼來做一些比較 cool 的事情,如象 Windows 的縮小 / 關閉按鈕等。標題視窗以下部分的 FrameLayou 就是為了讓程式設計師通過 setContentView 來設定使用者需要的視窗內容。因為整個 View 的佈局就是一棵樹,所以繪製的時候也是按照樹形結構遍歷來讓每個 View 進行繪製。ViewRoot.java 中的 draw 函式準備好 Canvas 後會呼叫 mView.draw(canvas),其中 mView 就是呼叫 ViewRoot.setView 時設定的 DecorView。然後看一下 View.java 中的 draw 函式:

遞迴的繪製整個視窗需要按順序執行以下幾個步驟:

  1. 繪製背景;
  2. 如果需要,儲存畫布(canvas)的層為淡入或淡出做準備;
  3. 繪製 View 本身的內容,通過呼叫 View.onDraw(canvas) 函式實現,通過這個我們應該能看出來 onDraw 函式過載的重要性,onDraw 函式中繪製線條 / 圓 / 文字等功能會呼叫 Canvas 中對應的功能。下面我們會 drawLine 函式為例進行說明;
  4. 繪製自己的孩子(通常也是一個 view 系統),通過 dispatchDraw(canvas) 實現,參看 ViewGroup.Java 中的程式碼可知,dispatchDraw->drawChild->child.draw(canvas) 這樣的呼叫過程被用來保證每個子 View 的 draw 函式都被呼叫,通過這種遞迴呼叫從而讓整個 View 樹中的所有 View 的內容都得到繪製。在呼叫每個子 View 的 draw 函式之前,需要繪製的 View 的繪製位置是在 Canvas 通過 translate 函式呼叫來進行切換的,視窗中的所有 View 是共用一個 Canvas 物件
  5. 如果需要,繪製淡入淡出相關的內容並恢復儲存的畫布所在的層(layer)
  6. 繪製修飾的內容(例如滾動條),這個可知要實現滾動條效果並不需要 ScrollView,可以在 View 中完成的,不過有一些小技巧,具體實現可以參看我們的 TextViewExample 示例程式碼

當一個 ChildView 要重畫時,它會呼叫其成員函式 invalidate() 函式將通知其 ParentView 這個 ChildView 要重畫,這個過程一直向上遍歷到 ViewRoot,當 ViewRoot 收到這個通知後就會呼叫上面提到的 ViewRoot 中的 draw 函式從而完成繪製。View::onDraw() 有一個畫布引數 Canvas, 畫布顧名思義就是畫東西的地方,Android 會為每一個 View 設定好畫布,View 就可以呼叫 Canvas 的方法,比如:drawText, drawBitmap, drawPath 等等去畫內容。每一個 ChildView 的畫布是由其 ParentView 設定的,ParentView 根據 ChildView 在其內部的佈局來調整 Canvas,其中畫布的屬性之一就是定義和 ChildView 相關的座標系,預設是橫軸為 X 軸,從左至右,值逐漸增大,豎軸為 Y 軸,從上至下,值逐漸增大 , 見下圖 :


圖 4. 視窗座標系
視窗座標系 

Android 動畫就是通過 ParentView 來不斷調整 ChildView 的畫布座標系來實現的,下面以平移動畫來做示例,見下圖 4,假設在動畫開始時 ChildView 在 ParentView 中的初始位置在 (100,200) 處,這時 ParentView 會根據這個座標來設定 ChildView 的畫布,在 ParentView 的 dispatchDraw 中它發現 ChildView 有一個平移動畫,而且當前的平移位置是 (100, 200),於是它通過呼叫畫布的函式 traslate(100, 200) 來告訴 ChildView 在這個位置開始畫,這就是動畫的第一幀。如果 ParentView 發現 ChildView 有動畫,就會不斷的呼叫 invalidate() 這個函式,這樣就會導致自己會不斷的重畫,就會不斷的呼叫 dispatchDraw 這個函式,這樣就產生了動畫的後續幀,當再次進入 dispatchDraw 時,ParentView 根據平移動畫產生出第二幀的平移位置 (500, 200),然後繼續執行上述操作,然後產生第三幀,第四幀,直到動畫播完。具體演算法描述如清單 2:

清單 2. 演算法

  1. dispatchDraw()   
  2. {   
  3.     ....   
  4.     Animation a = ChildView.getAnimation()   
  5.     Transformation tm = a.getTransformation();   
  6.     Use tm to set ChildView's Canvas;   
  7.     Invalidate();   
  8.     ....   
  9. }   



圖 5. 平移動畫示意圖
平移動畫示意圖 

以上是以平移動畫為例子來說明動畫的產生過程,這其中又涉及到兩個重要的型別,Animation 和 Transformation,這兩個類是實現動畫的主要的類,Animation 中主要定義了動畫的一些屬性比如開始時間、持續時間、是否重複播放等,這個類主要有兩個重要的函式:getTransformation 和 applyTransformation,在 getTransformation 中 Animation 會根據動畫的屬性來產生一系列的差值點,然後將這些差值點傳給 applyTransformation,這個函式將根據這些點來生成不同的 Transformation,Transformation 中包含一個矩陣和 alpha 值,矩陣是用來做平移、旋轉和縮放動畫的,而 alpha 值是用來做 alpha 動畫的(簡單理解的話,alpha 動畫相當於不斷變換透明度或顏色來實現動畫),以上面的平移矩陣為例子,當呼叫 dispatchDraw 時會呼叫 getTransformation 來得到當前的 Transformation,這個 Transformation 中的矩陣如下:


圖 6. 矩陣變換圖
矩陣變換圖 

所以具體的動畫只需要過載 applyTransformation 這個函式即可,類層次圖如下:


圖 7. 動畫類繼承關係圖
動畫類繼承關係圖 

使用者可以定義自己的動畫類,只需要繼承 Animation 類,然後過載 applyTransformation 這個函式。對動畫來說其行為主要靠差值點來決定的,比如,我們想開始動畫是逐漸加快的或者逐漸變慢的,或者先快後慢的,或者是勻速的,這些功能的實現主要是靠差值函式來實現的,Android 提供了 一個 Interpolator 的基類,你要實現什麼樣的速度可以過載其函式 getInterpolation,在 Animation 的 getTransformation 中生成差值點時,會用到這個函式。

從上面的動畫機制的分析可知某一個 View 的動畫的繪製並不是由他自己完成的而是由它的父 view 完成,所有我們要注意上面 TextView 旋轉一週的動畫示例程式中動畫的效果並不是由 TextView 來繪製的,而是由它的父 View 來做的。findViewById(R.id.TextView01).startAnimation(anim) 這個程式碼其實是給這個 TextView 設定了一個 animation,而不是進行實際的動畫繪製,程式碼如下 :

  1. public void startAnimation(Animation animation)   
  2. {   
  3.   
  4.     animation.setStartTime(Animation.START_ON_FIRST_FRAME);   
  5.   
  6.     setAnimation(animation); invalidate();   
  7.   
  8. }  


 

其他的動畫機制的程式碼感興趣的讀者請自己閱讀,希望通過原理的講解以後看起來會輕鬆點,呵呵。

以上就是 Android 的動畫框架的原理,瞭解了原理對我們的開發來說就可以清晰的把握動畫的每一幀是怎樣生成的,這樣便於開發和除錯。它把動畫的播放 / 繪製交給父 View 去處理而不是讓子 View 本身去繪製,這種從更高的層次上去控制的方式便於把動畫機制做成一個易用的框架,如果使用者要在某個 view 中使用動畫,只需要在 xml 描述檔案或程式碼中指定就可以了,從而把動畫的實現和 View 本身內容的繪製(象 TextView 裡面的文字顯示)分離開了,起到了減少耦合和提高易用性的效果。

 

動畫實現示例

在這個例子中,將要實現一個繞 Y 軸旋轉的動畫,這樣可以看到 3D 透視投影的效果,程式碼如下 ( 清單 4):

清單 3. 實現一個繞 Y 軸旋轉的動畫

  1. package com.example.android.apis.animation;   
  2. import android.view.animation.Animation;   
  3. import android.view.animation.Transformation;   
  4. import android.graphics.Camera;   
  5. import android.graphics.Matrix;   
  6. /**  
  7. * An animation that rotates the view on the Y axis between two specified angles.  
  8. * This animation also adds a translation on the Z axis (depth) to improve the effect.  
  9. */   
  10. public class Rotate3dAnimation extends Animation   
  11. {   
  12.    private final float mFromDegrees;   
  13.    private final float mToDegrees;   
  14.    private final float mCenterX;   
  15.    private final float mCenterY;   
  16.    private final float mDepthZ;   
  17.    private final boolean mReverse;   
  18.    private Camera mCamera;   
  19.    /**  
  20.     * Creates a new 3D rotation on the Y axis. The rotation is defined by its  
  21.     * start angle and its end angle. Both angles are in degrees. The rotation  
  22.     * is performed around a center point on the 2D space, definied by a pair  
  23.     * of X and Y coordinates, called centerX and centerY. When the animation  
  24.     * starts, a translation on the Z axis (depth) is performed. The length  
  25.     * of the translation can be specified, as well as whether the translation  
  26.     * should be reversed in time.  
  27.     *  
  28.     * @param fromDegrees the start angle of the 3D rotation  
  29.     * @param toDegrees the end angle of the 3D rotation  
  30.     * @param centerX the X center of the 3D rotation  
  31.     * @param centerY the Y center of the 3D rotation  
  32.     * @param reverse true if the translation should be reversed, false otherwise  
  33.     */   
  34.    public Rotate3dAnimation(float fromDegrees, float toDegrees,   
  35.                  float centerX,       float centerY,  
  36.                  float depthZ,       boolean reverse)   
  37.    {   
  38.        mFromDegrees = fromDegrees;   
  39.        mToDegrees = toDegrees;   
  40.        mCenterX = centerX;   
  41.        mCenterY = centerY;   
  42.        mDepthZ = depthZ;   
  43.        mReverse = reverse;   
  44.    }   
  45.   
  46.    @Override   
  47.    public void initialize(int width, int height, int parentWidth, int parentHeight)   
  48.    {   
  49.        super.initialize(width, height, parentWidth, parentHeight);   
  50.        mCamera = new Camera();   
  51.    }   
  52.   
  53.    @Override   
  54.    protected void applyTransformation(float interpolatedTime, Transformation t)  
  55.    {   
  56.        final float fromDegrees = mFromDegrees;   
  57.        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);   
  58.        final float centerX = mCenterX;   
  59.        final float centerY = mCenterY;   
  60.        final Camera camera = mCamera;   
  61.        final Matrix matrix = t.getMatrix();   
  62.        camera.save();   
  63.          
  64.        if (mReverse)  
  65.        {   
  66.            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);   
  67.        }   
  68.        else   
  69.        {   
  70.            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));   
  71.        }   
  72.          
  73.        camera.rotateY(degrees);   
  74.        camera.getMatrix(matrix);   
  75.        camera.restore();   
  76.        matrix.preTranslate(-centerX, -centerY);   
  77.        matrix.postTranslate(centerX, centerY);   
  78.    }   
  79. }   


 

在這個例子中我們過載了 applyTransformation 函式,interpolatedTime 就是 getTransformation 函 數傳下來的差值點,在這裡做了一個線性插值演算法來生成中間角度:float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime); Camera 類是用來實現繞 Y 軸旋轉後透視投影的,我們只需要其返回的 Matrix 值 , 這個值會賦給 Transformation 中的矩陣成員,當 ParentView 去為 ChildView 設定畫布時,就會用它來設定座標系,這樣 ChildView 畫出來的效果就是一個繞 Y 軸旋轉同時帶有透視投影的效果。利用這個動畫便可以作出像立體翻頁等比較酷的效果。如何使用這個 animation 請見 ApiDemos 程式包 com.example.android.apis.animation 中的 Transition3d.java 程式碼。

 

Android 中顯示 Gif 格式圖

有關這一部分,本文將不做詳細介紹。 感興趣的讀者請參看 Apidemos 中 com.example.android.apis.graphics 下面的 BitmapDecode.java 中的示例程式碼。

這裡先簡單說明一下,它的實現是通過 Movie 這個類來對 Gif 檔案進行讀取和解碼的,同時在 onDraw 函式中不斷的繪製每一幀圖片完成的,這個示例程式碼在 onDraw 中呼叫 invalidate 來反覆讓 View 失效來讓系統不斷呼叫 SampleView 的 onDraw 函式;至於選出哪一幀圖片進行繪製則是傳入系統當前時間給 Movie 類,然後讓它根據時間順序來選出幀圖片。反覆讓 View 失效的方式比較耗資源,繪製效果允許的話可以採取延時讓 View 失效的方式來減小 CPU 消耗。

目前使用這個方式播放一些 Gif 格式的動畫時會出現花屏的現象,這是因為 Android 中使用的 libgif 庫是比較老的版本,新的 tag 不支援,所以導致花屏,解決辦法有製作 Gif 圖片時別使用太新的 tag 或完善 android 中對應的 libgif 庫。

 

結束語

本文介紹了 Android 動畫框架的基本原理,可以幫助開發者深入理解 Android 的動畫是如何實現的,從而能夠充分利用 android 現有框架來做出夠眩、夠酷的動畫效果。


 

參考資料

學習

相關文章