「グラフィックス(1)-Viewクラスへの描画」 では、グラフィックの描画にViewクラスを使っていましたが、これをSurfaceViewクラスを使って実現する事もできます。
SurfaceViewは、Viewクラスを継承したクラスですが、Viewクラスよりも高速に描画ができ、ゲームプログラミング等に適しています。
通常のウィジェットと違い、別スレッドで直接GUI操作をおこなう事もできます。
今回は「グラフィックス(1)-Viewクラスへの描画」で紹介した、リスト1〜リスト3のプログラムを、 SurfaceViewで実現するにはどのようにしたらよいか、についてみていきます。
SurfaceViewによるグラフィックスの基本形-静止した画像の描画
「グラフィックス(1)-Viewクラスへの描画」のリスト1では、 Viewを使って、静止した画像を描画していましたが、これをSurfaceViewを使って、実現してみます。
リスト4(SurfaceViewSample1.java)
| 01 | package gudon.sample.surfaceview1; |
| 02 | |
| 03 | import android.app.Activity; |
| 04 | import android.content.Context; |
| 05 | import android.graphics.Canvas; |
| 06 | import android.graphics.Color; |
| 07 | import android.graphics.Paint; |
| 08 | import android.os.Bundle; |
| 09 | import android.view.SurfaceHolder; |
| 10 | import android.view.SurfaceView; |
| 11 | |
| 12 | public class SurfaceViewSample1 extends Activity { |
| 15 | public void onCreate(Bundle savedInstanceState) { |
| 16 | super.onCreate(savedInstanceState); |
| 17 | |
| 18 | setContentView(new CustomSurfaceView(this)); |
| 21 | |
| 22 | class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback { |
| 23 | |
| 24 | public CustomSurfaceView(Context context) { |
| 27 | getHolder().addCallback(this); |
| 28 | } |
| 31 | public void surfaceChanged (SurfaceHolder holder, int format, int width,int height) { |
| 32 | // SurfaceViewが変化(画面の大きさ,ピクセルフォーマット)した時のイベントの処理を記述 |
| 35 | @Override |
| 36 | public void surfaceCreated(SurfaceHolder holder) { |
| 37 | // SurfaceViewが作成された時の処理(初期画面の描画等)を記述 |
| 38 | Canvas canvas = holder.lockCanvas(); |
| 39 | |
| 40 | // この間にグラフィック描画のコードを記述する。 |
| 41 | |
| 42 | Paint paint = new Paint(); |
| 43 | paint.setColor(Color.GREEN); |
| 44 | canvas.drawRect(0, 0, 50, 50, paint); |
| 45 | |
| 46 | // この間にグラフィック描画のコードを記述する。 |
| 47 | |
| 48 | holder.unlockCanvasAndPost(canvas); |
| 51 | @Override |
| 52 | public void surfaceDestroyed(SurfaceHolder holder) { |
| 53 | // SurfaceViewが廃棄された時の処理を記述 |
| 54 | } |
SurfaceViewを操作するには、SurfaceHolderオブジェクトを使います。
例えば、27行目ではgetHolderメソッドでSurfaceHolderオブジェクトを取得して、SurfaceHolderオブジェクトのaddCallbackメソッドを使って、SurfaceViewの状態が変化した時に呼び出される、SurfaceHolder.Callbackインターフェースを実装したクラスを、指定しています。
通常は、この例のようにSurfaceViewクラス自身に、SurfaceHolder.Callbackインターフェースを実装する事が多いと思います。
SurfaceHolder.Callbackインターフェースを実装するには、以下の3つのコールバックメソッドを、オーバーライドする必要があります。
· surfaceChanged
SurfaceViewが変化(画面の大きさ,ピクセルフォーマット)した時のイベントの処理を記述する。
· surfaceCreated
SurfaceViewが作成された時の処理(初期画面の描画等)を記述する。
· surfaceDestroyed
リソースの開放等、SurfaceViewが廃棄された時の処理を記述する。
SurfaceViewに、グラフィックを描画するには、38行目〜48行目のようにSurfaceHolderのlockCanvasメソッドを使って、 他のスレッドからCanvasオブジェクトを操作されないように、Canvasをロックした上で、Canvasオブジェクトを取得し、 描画をおこないます。
SurfaceViewでのCanvasへの描画の方法は、Viewクラスでおこなった描画の方法とまったく同じです。
描画が終わったら、unlockCanvasAndPostメソッドを使って、すみやかにCanvasオブジェクトを開放します。
SurfaceHolderオブジェクトは、ここでは、surfaceCreatedメソッドの引数として渡されているものを使っていますが、 27行目のようにgetHolderメソッドを使って取得する事もできます。
SurfaceViewによるユーザからのイベントに対する応答
SurfaceViewに対するユーザからのイベントの処理も、基本的にはViewでの処理とまったく同じです。
リスト2の、Viewによるイベント処理のプログラムの例を、SurfaceViewを使って実現してみます。
リスト5(SurfaceViewSample2.java)
| 001 | package gudon.sample.surfaceview2; |
| 002 | |
| 003 | import android.app.Activity; |
| 004 | import android.content.Context; |
| 005 | import android.graphics.Canvas; |
| 006 | import android.graphics.Color; |
| 007 | import android.graphics.Paint; |
| 008 | import android.os.Bundle; |
| 009 | import android.view.KeyEvent; |
| 010 | import android.view.MotionEvent; |
| 011 | import android.view.SurfaceHolder; |
| 012 | import android.view.SurfaceView; |
| 013 | |
| 014 | public class SurfaceViewSample2 extends Activity { |
| 017 | public void onCreate(Bundle savedInstanceState) { |
| 018 | super.onCreate(savedInstanceState); |
| 019 | |
| 020 | setContentView(new CustomSurfaceView(this)); |
| 023 | |
| 024 | class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback { |
| 025 | |
| 026 | private static final float rectWidth = 50; |
| 027 | private static final float rectHeight = 50; |
| 028 | |
| 029 | private float x0 = 0, y0 = 0; |
| 030 | private float dx = 0, dy = 0; |
| 031 | private float xOffset = 0, yOffset = 0; |
| 032 | |
| 033 | public CustomSurfaceView(Context context) { |
| 034 | super(context); |
| 035 | |
| 036 | setFocusable(true); |
| 037 | getHolder().addCallback(this); |
| 038 | } |
| 041 | public void surfaceChanged(SurfaceHolder holder, int format, int width, |
| 042 | int height) { |
| 043 | // SurfaceViewが変化(画面の大きさ,ピクセルフォーマット)した時のイベントの処理を記述 |
| 044 | } |
| 047 | public void surfaceCreated(SurfaceHolder holder) { |
| 048 | // SurfaceViewが作成された時の処理(初期画面の描画等)を記述 |
| 049 | doDraw(holder); |
| 050 | } |
| 053 | public void surfaceDestroyed(SurfaceHolder holder) { |
| 054 | // SurfaceViewが廃棄された時の処理を記述 |
| 057 | private void doDraw(SurfaceHolder holder) { |
| 058 | Canvas canvas = holder.lockCanvas(); |
| 059 | onDraw(canvas); |
| 060 | holder.unlockCanvasAndPost(canvas); |
| 063 | @Override |
| 064 | protected void onDraw(Canvas canvas) { |
| 065 | canvas.drawColor(Color.BLACK); |
| 066 | |
| 067 | Paint paint = new Paint(); |
| 068 | paint.setColor(Color.GREEN); |
| 069 | canvas.drawRect(xOffset + dx, yOffset + dy, xOffset + dx + rectWidth, |
| 070 | yOffset + dy + rectHeight, paint); |
| 073 | @Override |
| 074 | public boolean onKeyDown(int keyCode, KeyEvent event) { |
| 075 | switch (keyCode) { |
| 076 | case KeyEvent.KEYCODE_DPAD_LEFT: |
| 077 | xOffset--; |
| 078 | break; |
| 079 | case KeyEvent.KEYCODE_DPAD_RIGHT: |
| 080 | xOffset++; |
| 081 | break; |
| 082 | case KeyEvent.KEYCODE_DPAD_UP: |
| 083 | yOffset--; |
| 084 | break; |
| 085 | case KeyEvent.KEYCODE_DPAD_DOWN: |
| 086 | yOffset++; |
| 091 | doDraw(getHolder()); |
| 092 | return true; |
| 095 | @Override |
| 096 | public boolean onTouchEvent(MotionEvent event) { |
| 097 | float x = event.getX(); |
| 098 | float y = event.getY(); |
| 099 | switch (event.getAction()) { |
| 100 | case MotionEvent.ACTION_DOWN: |
| 103 | break; |
| 104 | case MotionEvent.ACTION_MOVE: |
| 105 | dx = x - x0; |
| 106 | dy = y - y0; |
| 107 | break; |
| 108 | case MotionEvent.ACTION_UP: |
| 109 | xOffset += (x - x0); |
| 110 | yOffset += (y - y0); |
| 117 | doDraw(getHolder()); |
| 118 | return true; |
Viewクラスの処理と異なる点は、Viewクラスでは画面の更新時にonDrawメソッドが自動的に呼ばれ、クラフィックの描画がおこなわれていましたが、SurfaceViewでは呼び出されません。
また、画像を再描画したい場合には、invalidateメソッドを実行すれば、onDrawメソッドが呼び出されていましたが、 SurfaceViewでは、invalidateメソッドを実行しても、onDrawメソッドは呼び出されません。
このプログラムでは、doDrawメソッドを定義して画像の更新処理をおこないたい場合は、このメソッドを呼び出すようにしています。
また、Viewクラスの描画処理をイメージしやすいようにonDrawメソッドを定義して、doDrawメソッドからonDrawメソッドを呼び出していますが、 onDrawメソッドが自動的に呼び出されるわけではないので、onDrawメソッドを定義せずに、doDrawメソッド内で直接グラフィックの描画をおこなってもかまいません。
SurfaceViewバージョンの「壁にあたって跳ね返る図形のアニメーション」
SurfaceViewで定期的に画面を書き換える例を示すために、 リスト3の「壁にあたって跳ね返る図形のアニメーション」を, SurfaceViewを使って実現してみます。
リスト6(SurfaceViewSample3.java)
| 01 | package gudon.sample.surfaceview3; |
| 02 | |
| 03 | import android.app.Activity; |
| 04 | import android.content.Context; |
| 05 | import android.graphics.Canvas; |
| 06 | import android.graphics.Color; |
| 07 | import android.graphics.Paint; |
| 08 | import android.os.Bundle; |
| 09 | import android.util.Log; |
| 10 | import android.view.SurfaceHolder; |
| 11 | import android.view.SurfaceView; |
| 12 | |
| 13 | public class SurfaceViewSample3 extends Activity { |
| 14 | |
| 15 | @Override |
| 16 | public void onCreate(Bundle savedInstanceState) { |
| 17 | super.onCreate(savedInstanceState); |
| 18 | |
| 19 | setContentView(new CustomSurfaceView(this)); |
| 20 | } |
| 23 | class CustomSurfaceView extends SurfaceView implementsSurfaceHolder.Callback,Runnable{ |
| 24 | Thread thread; |
| 25 | boolean isAttached; |
| 26 | |
| 27 | |
| 28 | private static final float rectWidth = 50; |
| 29 | private static final float rectHeight = 50; |
| 30 | |
| 31 | private float dx = 5, dy = 5; |
| 32 | private float screenWidth, screenHeight; |
| 33 | private float xOffset = 0, yOffset = 0; |
| 34 | |
| 35 | public CustomSurfaceView(Context context) { |
| 36 | super(context); |
| 37 | |
| 38 | getHolder().addCallback(this); |
| 41 | @Override |
| 42 | public void surfaceChanged(SurfaceHolder holder, int format, int width, |
| 43 | int height) { |
| 44 | screenWidth = width; |
| 45 | screenHeight = height; |
| 46 | } |
| 49 | public void surfaceCreated(SurfaceHolder holder) { |
| 50 | isAttached=true; |
| 51 | thread = new Thread(this); |
| 52 | thread.start(); |
| 55 | @Override |
| 56 | public void surfaceDestroyed(SurfaceHolder holder) { |
| 57 | isAttached = false; |
| 58 | while (thread.isAlive()); |
| 61 | @Override |
| 62 | public void run() { |
| 63 | while (isAttached) { |
| 64 | Log.v("SurfaceViewSample3","run"); |
| 65 | |
| 66 | if (xOffset < 0 || xOffset + rectWidth > screenWidth) |
| 67 | dx = -dx; |
| 68 | if (yOffset < 0 || yOffset + rectHeight > screenHeight) |
| 69 | dy = -dy; |
| 70 | xOffset += dx; |
| 73 | doDraw(getHolder()); |
| 74 | } |
| 77 | private void doDraw(SurfaceHolder holder){ |
| 78 | Canvas canvas = holder.lockCanvas(); |
| 79 | |
| 80 | // この間にグラフィック描画のコードを記述する。 |
| 81 | |
| 82 | Paint paint = new Paint(); |
| 83 | paint.setColor(Color.GREEN); |
| 84 | |
| 85 | canvas.drawColor(Color.BLACK); |
| 86 | canvas.drawRect(xOffset, yOffset, xOffset + rectWidth, yOffset |
| 87 | + rectHeight, paint); |
| 88 | |
| 89 | // この間にグラフィック描画のコードを記述する。 |
| 90 | |
| 91 | holder.unlockCanvasAndPost(canvas); |
| 92 | } |
リスト3では、画面のサイズを取得するのに、View#onSizeChangedイベントを使っていましたが、 SurfaceHolder.CallbackインターフェースにはsurfaceChangedメソッドがあるので、ここでは、これを利用しています。
また、リスト3では、定期的に画面を更新するためのタイマーとして、HandlerクラスのsendEmptyMessageDelayedメソッドを使用していましたが、 SurfaceViewは、別スレッドから直接GUI操作ができるので、(SurfaceViewクラスにRunnableインターフェースをインプリメントして)javaで標準に使われているThreadクラスを使っています。
プログラム終了時にスレッドを終了させないと、終了後もスレッドが生き残ってしまうので、この処理をsurfaceDestroyedメソッドに実装しています。
リスト3では、この処理にonDetachedFromWindowイベントを利用していましたが、onDetachedFromWindowイベントはSurfaceViewでは発生しないようです。
58行目のwhileループは必要無いかもしれませんが、一応、確実にスレッドが終了した後にsurfaceDestroyedメソッドが終了するように、待ち時間を持たせるためにいれてあります。
64行目のLog出力は、プログラム終了時にスレッドが停止するか、確認するためのものです。
85行目で、リスト3には無かったcanvas#drawColorメソッドを追加して、背景を黒色に塗りつぶしていますが、 これは、Viewクラスでは前の画面がクリアされた後に画面が描画されるのに対して、 SurfaceViewでは前の画面が残ったままになってしまうため、前の画面を消去するためにいれてあります。