1.view
view在api中的結構
java.lang.Object
android.view.View
直接子類:
AnalogClock, ImageView, KeyboardView, ProgressBar, SurfaceView, TextVie, ViewGroup, ViewStub
間接子類:
AbsListView, AbsSeekBar, AbsSpinner, AbsoluteLayout, AdapterView<T extends Adapter>, AppWidgetHostView, AutoCompleteTextView, Button, CheckBox, CheckedTextView, Chronometer, CompoundButton, DatePicker, DialerFilter, DigitalClock,EditView, ExpandableListView, ExtractEditText, FrameLayout, GLSurfaceView, Gallery, GestureOverlayView, GridView, HorizontalScrollView, ImageButton, ImageSwitcher, LinearLayout, ListView, MediaController, MultiAutoCompleteTextView, QuickContactBadge, RadioButton, RadioGroup, RatingBar, RelativeLayout, ScrollView, SeekBar, SlidingDrawer, Spinner, TabHost, TabWidget, TableLayout, TableRow, TextSwitcher, TimePicker, ToggleButton, TwoLineListItem, VideoView, ViewAnimator, ViewFlipper, ViewSwitcher, WebView, ZoomButton, ZoomControls
由此可見View類屬於Android開發繪制中的顯示老大,任何與繪制有關系的控件都是它的子類。在這篇文章中我主要講View 與SurFaceView 使用線程刷新屏幕繪制方面的知識。開發中如何去選擇使用View還是SurFaceView。我相信讀過我前幾篇博客的朋友應該知道我在刷新屏幕的時候使用invalidate()方法來重繪,下面我詳細的說明一下Andooid刷新屏幕的幾種方法。
第一種: 在onDraw方法最后調用invalidate()方法,它會通知UI線程重繪 這樣 View會重新調用onDraw方法,實現刷新屏幕。 這樣寫看起來代碼非常簡潔漂亮,但是它也同時存在一個很大的問題,它和游戲主線程是分開的 它違背了單線程模式,這樣操作繪制的話是很不安全的,舉個例子 比如程序先進在Activity1中 使用invalidate()方法來重繪, 然后我跳到了Activity2這時候Activity1已經finash()掉 可是Activity1中 的invalidate() 的線程還在程序中,Android的虛擬機不可能主動殺死正在運行中的線程所以這樣操作是非常危險的。因為它是在UI線程中被動掉用的所以很不安全。
invalidate() 更新整個屏幕區域
invalidate(Rect rect) 更新Rect區域
invalidate(l, t, r, b) 更新指定矩形區域
- public void onDraw(Canvas canvas){
- DosomeThing();
- invalidate();
- }
第二種:使用postInvalidate();方法來刷新屏幕 ,調用后它會用handler通知UI線程重繪屏幕,我們可以 new Thread(this).start(); 開啟一個游戲的主線程 然后在主線程中通過調用postInvalidate();方法來刷新屏幕。postInvalidate();方法 調用后 系統會幫我們調用onDraw方法 ,它是在我們自己的線程中調用 通過調用它可以通知UI線程刷新屏幕 。由此可見它是主動調用UI線程的。所以建議使用postInvalidate()方法通知UI線程來刷新整個屏幕。
postInvalidate(left, top, right, bottom) 方法 通過UI線程來刷新規定矩形區域。
- @Override
- public void run() {
- while (mIsRunning) {
- try {
- Thread.sleep(100);
- postInvalidate();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
View中用到的雙緩沖技術
重繪的原理是 程序根據時間來刷新屏幕 如果有一幀圖形還沒有完全繪制結束 程序就開始刷新屏幕這樣就會造成瞬間屏幕閃爍 畫面很不美觀,所以雙緩沖的技術就誕生了。它存在的目的就是解決屏幕閃爍的問題,下面我說說在自定義View中如何實現雙緩沖。
首先我們需要創建一張屏幕大小的緩沖圖片,我說一下第三個參數 ARGB 分別代表的是 透明度 紅色 綠色 藍色
Bitmap.Config ARGB_4444 ARGB 分別占四位
Bitmap.Config ARGB_8888 ARGB 分別占八位
Bitmap.Config RGB_565 沒有透明度(A) R占5位 G 占6位 B占5位
一般情況下我們使用ARGB_8888 因為它的效果是最好了 當然它也是最占內存的。
- mBufferBitmap = Bitmap.createBitmap(mScreenWidth,mScreenHeight,Config.ARGB_8888);
創建一個緩沖的畫布,將內容繪制在緩沖區mBufferBitmap中
- Canvas mCanvas = new Canvas();
- mCanvas.setBitmap(mBufferBitmap);
最后一次性的把緩沖區mBufferBitmap繪制在屏幕上,怎么樣 簡單吧 呵呵。
- @Override
- protected void onDraw(Canvas canvas) {
- /**這里先把所有須要繪制的資源繪制到mBufferBitmap上**/
- /**繪制地圖**/
- DrawMap(mCanvas,mPaint,mBitmap);
- /**繪制動畫**/
- RenderAnimation(mCanvas);
- /**更新動畫**/
- UpdateAnimation();
- if(isBorderCollision) {
- DrawCollision(mCanvas,"與邊界發生碰撞");
- }
- if(isAcotrCollision) {
- DrawCollision(mCanvas,"與實體層發生碰撞");
- }
- if(isPersonCollision) {
- DrawCollision(mCanvas,"與NPC發生碰撞");
- }
- /**最后通過canvas一次性的把mBufferBitmap繪制到屏幕上**/
- canvas.drawBitmap(mBufferBitmap, 0,0, mPaint);
- super.onDraw(canvas);
- }
由此可見view屬於被動刷新, 因為我們做的任何刷新的操作實際上都是通知UI線程去刷新。所以在做一些只有通過玩家操作以后才會刷新屏幕的游戲 並非自動刷新的游戲 可以使用view來操作。
2.SurfaceView
從API中可以看出SurfaceView屬於View的子類 它是專門為制作游戲而產生的,它的功能非常強大,最重要的是它支持OpenGL ES庫,2D和3D的效果都可以實現。創建SurfaceView的時候需要實現SurfaceHolder.Callback接口,它可以用來監聽SurfaceView的狀態,SurfaceView的改變 SurfaceView的創建 SurfaceView 銷毀 我們可以在相應的方法中做一些比如初始化的操作 或者 清空的操作等等。
使用SurfaceView構建游戲框架它的繪制原理是繪制前先鎖定畫布 然后等都繪制結束以后 在對畫布進行解鎖 最后在把畫布內容顯示到屏幕上。
代碼中是如何實現SurfaceView
首先需要實現 Callback 接口 與Runnable接口
- public class AnimView extends SurfaceView implements Callback,Runnable
獲取當前mSurfaceHolder 並且把它加到CallBack回調函數中
- SurfaceHolder mSurfaceHolder = getHolder();
- mSurfaceHolder.addCallback(this);
通過callBack接口監聽SurfaceView的狀態, 在它被創建的時候開啟游戲的主線程,結束的時候銷毀。這里說一下在View的構造函數中是拿不到view有關的任何信息的,因為它還沒有構建好。 所以通過這個監聽我們可以在surfaceCreated()中拿到當前view的屬性 比如view的寬高 等等,所以callBack接口還是非常有用處的。
- @Override
- public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
- int arg3) {
- // surfaceView的大小發生改變的時候
- }
- @Override
- public void surfaceCreated(SurfaceHolder arg0) {
- /**啟動游戲主線程**/
- mIsRunning = true;
- mThread = new Thread(this);
- mThread.start();
- }
- @Override
- public void surfaceDestroyed(SurfaceHolder arg0) {
- // surfaceView銷毀的時候
- mIsRunning = false;
- }
在游戲主線程循環中在繪制開始 先拿到畫布canvas 並使用mSurfaceHolder.lockCanvas()鎖定畫布,等繪制結束以后 使用mSurfaceHolder.unlockCanvasAndPost(mCanvas)解鎖畫布, 解鎖畫布以后畫布上的內容才會顯示到屏幕上。
- @Override
- public void run() {
- while (mIsRunning) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- //在這里加上線程安全鎖
- synchronized (mSurfaceHolder) {
- /**拿到當前畫布 然后鎖定**/
- mCanvas =mSurfaceHolder.lockCanvas();
- Draw();
- /**繪制結束后解鎖顯示在屏幕上**/
- mSurfaceHolder.unlockCanvasAndPost(mCanvas);
- }
- }
- }
由此可見SurfaceView 屬於主動刷新 ,重繪過程完全是在我們自己的線程中完成 , 由於游戲中肯定會執行各種絢麗的動畫效果如果使用被動刷新的View就有可能就會阻塞UI線程,所以SurfaceView 更適合做游戲。
效果圖
最近有朋友反映說運行起來有點卡 我解釋一下, 卡的主要原因是我的地圖文件太大了,當然還有模擬器不給力的原因。我每繪制一塊地圖就須要使用裁剪原圖,頻繁的切割如此大的圖片肯定會造成卡頓的情況。同學們在制作的時候將沒用的地圖塊去掉,保留只需要的地圖塊這樣會流暢很多喔 。
優化游戲主線程循環
同學們先看看這段代碼,Draw()方法繪制結束讓線程等待100毫秒在進入下一次循環。其實這樣更新游戲循環是很不科學的,原因是Draw()方法每一次更新所耗費的時間是不確定的。舉個例子 比如第一次循環Draw() 耗費了1000毫秒 加上線程等待100毫秒 整個循環耗時1100毫秒,第二次循環Draw() 耗時2000毫秒 加上線程等待時間100毫秒 整個循環時間就是2100毫秒。很明顯這樣就會造成游戲運行刷新時間時快時慢,所以說它是很不科學的。
- public void run() {
- while (mIsRunning) {
- //在這里加上線程安全鎖
- synchronized (mSurfaceHolder) {
- /**拿到當前畫布 然后鎖定**/
- mCanvas =mSurfaceHolder.lockCanvas();
- Draw();
- /**繪制結束后解鎖顯示在屏幕上**/
- mSurfaceHolder.unlockCanvasAndPost(mCanvas);
- }
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
在貼一段科學的控游戲制循環代碼,每次循環游戲主線程 在Draw()方法前后計算出Draw()方法所消耗的時間,然后在判斷是否達到我們規定的刷新屏幕時間,下例是以30幀刷新一次屏幕,如果滿足則繼續下次循環如果不滿足使用Thread.yield(); 讓游戲主線程去等待 並計算當前等待時間直到等待時間滿足30幀為止在繼續下一次循環刷新游戲屏幕。
這里說一下Thread.yield(): 與Thread.sleep(long millis):的區別,Thread.yield(): 是暫停當前正在執行的線程對象 ,並去執行其他線程。Thread.sleep(long millis):則是使當前線程暫停參數中所指定的毫秒數然后在繼續執行線程。
- /**每30幀刷新一次屏幕**/
- public static final int TIME_IN_FRAME = 30;
- @Override
- public void run() {
- while (mIsRunning) {
- /**取得更新游戲之前的時間**/
- long startTime = System.currentTimeMillis();
- /**在這里加上線程安全鎖**/
- synchronized (mSurfaceHolder) {
- /**拿到當前畫布 然后鎖定**/
- mCanvas =mSurfaceHolder.lockCanvas();
- Draw();
- /**繪制結束后解鎖顯示在屏幕上**/
- mSurfaceHolder.unlockCanvasAndPost(mCanvas);
- }
- /**取得更新游戲結束的時間**/
- long endTime = System.currentTimeMillis();
- /**計算出游戲一次更新的毫秒數**/
- int diffTime = (int)(endTime - startTime);
- /**確保每次更新時間為30幀**/
- while(diffTime <=TIME_IN_FRAME) {
- diffTime = (int)(System.currentTimeMillis() - startTime);
- /**線程等待**/
- Thread.yield();
- }
- }
- }
最后由於代碼較多我就不貼在博客中了 , 下面給出Demo源碼的下載地址歡迎大家下載閱讀互相學習,互相研究,互相討論 雨松MOMO希望可以和大家一起進步。