android中使用2D動畫 — SurfaceView 轉載
Posted on 2011-07-21 09:45 oathleo 閱讀(2929) 評論(0) 編輯 收藏 所屬分類: Androidandroid中使用2D動畫 — SurfaceView
通過之前介紹的如何自定義View, 我們知道使用它可以做一些簡單的動畫效果。它通過不斷循環的執行View.onDraw方法,每次執行都對內部顯示的圖形做一些調整,我們假設 onDraw方法每秒執行20次,這樣就會形成一個20幀的補間動畫效果。但是現實情況是你無法簡單的控制View.onDraw的執行幀數,這邊說的執 行幀數是指每秒View.onDraw方法被執行多少次,這是為什么呢?首先我們知道,onDraw方法是由系統幫我們調用的,我們是通過調用View的 invalidate方法通知系統需要重新繪制View,然后它就會調用View.onDraw方法。這些都是由系統幫我們實現的,所以我們很難精確去定 義View.onDraw的執行幀數,這個就是為什么我們這邊要了解SurfaceView了,它能彌補View的一些不足。
首先我們先寫一個自定義View實現動畫效果,AnimateViewActivity.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package com.android777.demo.uicontroller.graphics; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.view.View; public class AnimateViewActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView( new AnimateView( this )); } class AnimateView extends View{ float radius = 10 ; Paint paint; public AnimateView(Context context) { super (context); paint = new Paint(); paint.setColor(Color.YELLOW); paint.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas canvas) { canvas.translate( 200 , 200 ); canvas.drawCircle( 0 , 0 , radius++, paint); if (radius > 100 ){ radius = 10 ; } invalidate(); //通過調用這個方法讓系統自動刷新視圖 } } } |
運行上面的Activity,你將看到一個圓圈,它原始半徑是10,然后不斷的變大,直到達到100后又恢復到10,這樣循環顯示,視覺效果上說你將看到一個逐漸變大的圓圈。
上面就是一個簡單的自定義View實現的動畫效果,它能做的只是簡單的動畫效果,具有一些局限性。首先你無法控制動畫的顯示速度,目前它是以最快的 速度顯示,但是當你要更快,獲取幀數更高的動畫呢? 因為View的幀數是由系統控制的,所以你沒辦法完成上面的操作。如果你需要編寫一個游戲,它需要的幀數比較高,那么View就無能為力了,因為它被設計 出來時本來就不是用來處理一些高幀數顯示的。你可以把View理解為一個經過系統優化的,可以用來高效的執行一些幀數比較低動畫的對象,它具有特定的使用 場景,比如有一些幀數較低的游戲就可以使用它來完成:貪吃蛇、俄羅斯方塊、棋牌類等游戲,因為這些游戲執行的幀數都很低。但是如果是一些實時類的游戲,如 射擊游戲、塔防游戲、RPG游戲等就沒辦法使用View來做,因為它的幀數太低了,會導致動畫執行不順暢。所以我們需要一個能自己控制執行幀數的對 象,SurfaceView因此誕生了。
什么是SurfaceView呢?
為什么是SurfaceView呢?Surface的意思是表層,表面的意思,那么SurfaceView就是指一個在表層的View對象。為什么 說是在表層呢,這是因為它有點特殊跟其他View不一樣,其他View是繪制在表層外,而它就是充當表層對象。假設你要在一個球上畫畫,那么球的表層就當 做你的畫布對象,你畫的東西會擋住它的表層,我們默認沒使用SurfaceView,那么球的表層就是空白的,如果我們使用了SurfaceView,我 們可以理解為我們拿來的球本身表面就具有紋路,你是畫再紋路之上的,如果你畫的是半透明的,那么你將可以透過你畫的東西看到球面本身的紋路。SDK的文檔 說到:SurfaceView就是在Window上挖一個洞,它就是顯示在這個洞里,其他的View是顯示在Window上,所以View可以顯式在 SurfaceView之上,你也可以添加一些層在SurfaceView之上。
SurfaceView還有其他的特性,上面我們講了它可以控制幀數,那它是什么控制的呢?這就需要了解它的使用機制。一般在很多游戲設計中,我們都是開辟一個后臺線程計算游戲相關的數據,然后根據這些計算完的新數據再刷新視圖對象,由于對View執行繪制操作只能在UI線程上, 所以當你在另外一個線程計算完數據后,你需要調用View.invalidate方法通知系統刷新View對象,所以游戲相關的數據也需要讓UI線程能訪 問到,這樣的設計架構比較復雜,要是能讓后臺計算的線程能直接訪問數據,然后更新View對象那改多好。我們知道View的更新只能在UI線程中,所以使 用自定義View沒辦法這么做,但是SurfaceView就可以了。它一個很好用的地方就是允許其他線程(不是UI線程)繪制圖形(使用Canvas),根據它這個特性,你就可以控制它的幀數,你如果讓這個線程1秒執行50次繪制,那么最后顯示的就是50幀。
如何使用SurfaceView?
首先SurfaceView也是一個View,它也有自己的生命周期。因為它需要另外一個線程來執行繪制操作,所以我們可以在它生命周期的初始化階 段開辟一個新線程,然后開始執行繪制,當生命周期的結束階段我們插入結束繪制線程的操作。這些是由其內部一個SurfaceHolder對象完成的。 SurfaceHolder,顧名思義,它里面保存了一個隊Surface對象的引用,而我們執行繪制方法就是操作這個 Surface,SurfaceHolder因為保存了對Surface的引用,所以使用它來處理Surface的生命周期,說到底 SurfaceView的生命周期其實就是Surface的生命周期,因為SurfaceHolder保存對Surface的引用,所以使用 SurfaceHolder來處理生命周期的初始化。首先我們先看看建立一個SurfaceView的大概步驟,先看看代碼:
DemoSurfaceView.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package com.android777.demo.uicontroller.graphics; import android.content.Context; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class DemoSurfaceView extends SurfaceView implements Callback{ public DemoSurfaceView(Context context) { super (context); init(); //初始化,設置生命周期回調方法 } private void init(){ SurfaceHolder holder = getHolder(); holder.addCallback( this ); //設置Surface生命周期回調 } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } } |
上面代碼我們在SurfaceView的構造方法中執行了init初始化方法,在這個方法里,我們先獲取SurfaceView里的 SurfaceHolder對象,然后通過它設置Surface的生命周期回調方法,使用DemoSurfaceView類本身作為回調方法代理類。 surfaceCreated方法,是當SurfaceView被顯示時會調用的方法,所以你需要再這邊開啟繪制的線 程,surfaceDestroyed方法是當SurfaceView被隱藏會銷毀時調用的方法,在這里你可以關閉繪制的線程。上面的例子運行后什么也不 顯示,因為還沒定義一個執行繪制的線程。下面我們修改下代碼,使用一個線程繪制一個逐漸變大的圓圈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | package com.android777.demo.uicontroller.graphics; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class DemoSurfaceView extends SurfaceView implements Callback{ LoopThread thread; public DemoSurfaceView(Context context) { super (context); init(); //初始化,設置生命周期回調方法 } private void init(){ SurfaceHolder holder = getHolder(); holder.addCallback( this ); //設置Surface生命周期回調 thread = new LoopThread(holder, getContext()); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { thread.isRunning = true ; thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { thread.isRunning = false ; try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 執行繪制的繪制線程 * @author Administrator * */ class LoopThread extends Thread{ SurfaceHolder surfaceHolder; Context context; boolean isRunning; float radius = 10f; Paint paint; public LoopThread(SurfaceHolder surfaceHolder,Context context){ this .surfaceHolder = surfaceHolder; this .context = context; isRunning = false ; paint = new Paint(); paint.setColor(Color.YELLOW); paint.setStyle(Paint.Style.STROKE); } @Override public void run() { Canvas c = null ; while (isRunning){ try { synchronized (surfaceHolder) { c = surfaceHolder.lockCanvas( null ); doDraw(c); //通過它來控制幀數執行一次繪制后休息50ms Thread.sleep( 50 ); } } catch (InterruptedException e) { e.printStackTrace(); } finally { surfaceHolder.unlockCanvasAndPost(c); } } } public void doDraw(Canvas c){ //這個很重要,清屏操作,清楚掉上次繪制的殘留圖像 c.drawColor(Color.BLACK); c.translate( 200 , 200 ); c.drawCircle( 0 , 0 , radius++, paint); if (radius > 100 ){ radius = 10f; } } } } |
上面代碼編寫了一個使用SurfaceView制作的動畫效果,它的效果跟上面自定義View的一樣,但是這邊的SurfaceView可以控制動 畫的幀數。在SurfaceView中內置一個LoopThread線程,這個線程的作用就是用來繪制圖形,在SurfaceView中實例化一個 LoopThread實例,一般這個操作會放在SurfaceView的構造方法中。然后通過在SurfaceView中的SurfaceHolder的 生命周期回調方法中插入一些操作,當Surface被創建時(SurfaceView顯示在屏幕中時),開啟LoopThread執行繪 制,LoopThread會一直刷新SurfaceView對象,當SurfaceView被隱藏時就停止改線程釋放資源。這邊有幾個地方要注意下:
1.因為SurfaceView允許自定義的線程操作Surface對象執行繪制方法,而你可能同時定義多個線程執行繪制,所以當你獲取 SurfaceHolder中的Canvas對象時記得加同步操作,避免兩個不同的線程同時操作同一個Canvas對象,當操作完成后記得調用 SurfaceHolder.unlockCanvasAndPost方法釋放掉Canvas鎖。
2.在調用doDraw執行繪制時,因為SurfaceView的特點,它會保留之前繪制的圖形,所以你需要先清空掉上一次繪制時留下的圖形。(View則不會,它默認在調用View.onDraw方法時就自動清空掉視圖里的東西)。
3. 記得在回調方法:onSurfaceDestroyed方法里將后臺執行繪制的LoopThread關閉,這里是使用join方法。這涉及到線程如何關閉 的問題,多數人建議是通過一個標志位:isRunning來判斷線程是否該停止運行,如果你想關閉線程只需要將isRunning改成false即可,線 程會自動執行完run方法后退出。
總結:
通過上面的分析,現在大家應該會簡單使用SurfaceView了,總的歸納起來SurfaceView和View不同之處有:
1. SurfaceView允許其他線程更新視圖對象(執行繪制方法)而View不允許這么做,它只允許UI線程更新視圖對象。
2. SurfaceView是放在其他最底層的視圖層次中,所有其他視圖層都在它上面,所以在它之上可以添加一些層,而且它不能是透明的。
3. 它執行動畫的效率比View高,而且你可以控制幀數。
4. 因為它的定義和使用比View復雜,占用的資源也比較多,除非使用View不能完成,再用SurfaceView否則最好用View就可以。(貪吃蛇,俄羅斯方塊,棋牌類這種幀數比較低的可以使用View做就好)