minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

Android は独自の2Dグラフィックライブラリ、および
OpenGL ES 1.0 を使ったハイパフォーマンスの3Dグラフィックをサポートしています。

二つの選択

2Dグラフィックを描画するとき、大きく分けて二つの方法があります。

  1. View

    View オブジェクトにグラフィックやアニメーションを描画します。
    この方法では、View が持つ一般的な描画規則に基づいて描画を行います。

  2. Canvas

    Canvas に直接グラフィックを描画します。
    この方法では、draw() や drawPicture() のメソッドを状況に応じて呼び出す必要があります。

View に描く方法は、動的に変化しないシンプルなグラフィックを描きたいときには最適な選択となります。

Canvas に描く方法は、アプリケーションが定期的な再描画を要求するときは良い選択となります。
一般的に多くのビデオゲームは Canvas に描画する必要があるでしょう。
しかし、そのためにいくつかの手順が必要となります。

Activity と同じスレッドの中でカスタム View を作成し、invalidate() を呼び出します。
そしてそのカスタム View に onDraw() コールバックを実装します。
それとは別に、別スレッド上から SurfaceView を使う方法もあります。

View への描画

この場合、単に View のバックグラウンドに描画するだけです。
ImageView の場合は、content に対して描画します。

Canvas への描画

Canvas を使うと、Window に配置される Bitmap へ直接描画することができます。
以下に、その手順を説明します。

まずは、Bitmap とそれに対応する Canvas インスタンスを作成します。

Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

これで、この Canvas に対する描画処理が Bitmap に反映されるようになります。
また、Bitmap の内容を Canvas に反映させることもできます。
これには、Canvas.drawBitmap(Bitmap, ...) メソッドを使います。

Canvas クラスは、drawBitmap, drawRect, drawText など様々な描画メソッドを持っています。
他に、Drawable というインターフェイスがあります。
このインターフェイスを持つ実装クラス(例えば ShapeDrawable)に対して処理を行った後
Drawable.draw(Canvas) を呼び出すことによって、その内容を Canvas に反映させることができます。

View を使った Canvas への描画

もしアプリケーションがそれほど多くのフレームレート数を要求しない場合(例えばチェスゲームなど)、
そのカスタム View に対して onDraw() メソッドを実装してその中で Canvas に描画することを考えましょう。

onDraw() メソッドは最初に Android フレームワークから呼び出されます。
その後、再描画が必要になったら invalidate() メソッドを呼び出します。
すると Android フレームワークは Canvas 内の全領域に描かれた内容がもはや有効でないと判断し
再度 onDraw() メソッドを呼び出すでしょう。
※ しかしこれは「すぐに」呼び出されることは保障されていません

onDraw(Canvas) の中で、この Canvas インスタンスに対して描画処理を行いましょう。
このメソッドが終わると、Android フレームワークは Canvas の内容をBitmap に反映させ、画面に表示します。

メインスレッド以外から invalidate() メソッドを呼び出す場合は、代わりに postInvalidate() を呼び出しましょう

SurfaceView を使った Canvas への描画

SurfaceView は、View の特殊なサブクラスです。

この View は、アプリケーションのセカンダリスレッドに対して描画処理を提供する目的で使われます。
Android では通常、全ての描画処理をメインスレッドで行わなければなりません。
これは、他の描画処理やイベント処理など全てのシステム処理を含んでいます。

つまり描画に時間が掛かる場合、これによってこれらのシステム処理が全て待機状態になってしまいます。
SurfaceView を使うことで、描画処理を別スレッドで処理できるようになります。

使い方

まず、SurfaceView のサブクラスを作成します。
このクラスには SurfaceHolder.Callback インターフェイスを実装する必要があります。
このインターフェイスには、Surface が作成・変更・破棄されるときにそれぞれ呼ばれる
コールバック関数が定義されています。
前述したように、これらのコールバック関数は常にセカンダリスレッド上で実行されるようにします。

描画をするときは、まず lockCanvas() を呼び出して Canvas を得ます。
そして描画処理を行い、完了したら unlockCanvasAndPost(canvas) を呼び出します。
これによって、その内容が画面上に反映されます。

一つ注意して下さい。
unlockCanvasAndPost() すると、この Canvas の内容は失われます。
つまり、次に lockCanvas() を呼び出したときに得られる Canvas の内容が
これと同じだということは保障されていません。
そのため、Canvas には毎回全ての描画処理を施して下さい。

サンプルコード

SurfaceView を使ったサンプルコード(着陸船ゲーム)を紹介します。
これは Android のサンプルコードを簡略化したものです。

LunarView.java

class LunarView extends SurfaceView implements SurfaceHolder.Callback {
    
    class LunarThread extends Thread {
        private SurfaceHolder mSurfaceHolder;
        private Drawable mLanderImage;
        private Bitmap mBackgroundImage;
        private boolean mRun = false;
        
        public LunarThread(SurfaceHolder surfaceHolder, Context context) {
            mSurfaceHolder = surfaceHolder;
            mContext = context;
            
            Resources res = context.getResources();
            mLanderImage = context.getResources().getDrawable(R.drawable.lander_plain);
            
        }
        public void doStart() {
            synchronized (mSurfaceHolder) {
                // 各種初期値の設定
                setState(STATE_RUNNING);
            }
        }
        public void pause() {
            synchronized (mSurfaceHolder) {
                if (mMode == STATE_RUNNING) setState(STATE_PAUSE);
            }
        }
        public synchronized void restoreState(Bundle savedState) {
            synchronized (mSurfaceHolder) {
                setState(STATE_PAUSE);
                // savedState を使って各種値を設定する
            }
        }
        @Override
        public void run() {
            while (mRun) {
                Canvas c = null;
                try {
                    c = mSurfaceHolder.lockCanvas(null);
                    synchronized (mSurfaceHolder) {
                        if (mMode == STATE_RUNNING) updatePhysics();
                        doDraw(c);
                    }
                } finally {
                    if (c != null) {
                        mSurfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }
        public Bundle saveState(Bundle map) {
            synchronized (mSurfaceHolder) {
                // 現在の状態を、map に保存(put)する
            }
        }
        public void setRunning(boolean b) {
            mRun = b;
        }
        /** View のサイズが変わったときに呼ばれる */
        public void setSurfaceSize(int width, int height) {
            mBackgroundImage = mBackgroundImage.createScaledBitmap(mBackgroundImage, width, height, true);
        }
        boolean doKeyDown(int keyCode, KeyEvent msg) {
            synchronized (mSurfaceHolder) {
                // キーが押されたときの処理
                return false;
            }
        }
        /** run ループ内から呼ばれる */
        private void doDraw(Canvas canvas) { 
            // 背景および各種ラインの描画
            canvas.drawBitmap(mBackgroundImage, 0, 0, null);
            canvas.drawLine(...);
            mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop + mLanderHeight);
            mLanderImage.draw(canvas);
        }
    }
    
    private Context mContext;
    private LunarThread thread;
    public LunarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
        thread = new LunarThread(holder, context);
        setFocusable(true); // キーイベントを受け取るために必要
    }
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent msg) {
        return thread.doKeyDown(keyCode, msg); // キーイベントハンドラの委譲
    }
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        if (!hasWindowFocus) thread.pause();
    }
    /** Surface が作成されたときに呼ばれる (implements SurfaceHolder.Callback) */
    public void surfaceCreated(SurfaceHolder holder) {
        thread.setRunning(true);
        thread.start();
    }
    /** Surface が変化したときに呼ばれる (implements SurfaceHolder.Callback) */
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        thread.setSurfaceSize(width, height);
    }
    /** Surface が破棄されるときに呼ばれる (implements SurfaceHolder.Callback) */
    public void surfaceDestroyed(SurfaceHolder holder) {
        // スレッドの終了フラグをセットし、完了するまで待つ
        boolean retry = true;
        thread.setRunning(false);
        while (retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {}
        }
    }    
}

LunarLander.java

public class LunarLander extends Activity {
    private LunarThread mLunarThread;
    private LunarView mLunarView;
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_START:
                mLunarThread.doStart();
                return true;
            ...
        }
        return false;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.lunar_layout);
        
        mLunarView = (LunarView)findViewById(R.id.lunar);
        mLunarThread = mLunarView.getThread();
        if (savedInstanceState == null) {
            // 初期起動時
            mLunarThread.setState(LunarThread.STATE_READY);
        } else {
            // 再開時は savedInstanceState から状態を取得
            mLunarThread.restoreState(savedInstanceState);
        }
    }
    @Override
    protected void onPause() {
        super.onPause();
        mLunarView.getThread().pause();
    }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mLunarThread.saveState(outState);
    }
}

lunar_layout.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <com.example.android.lunarlander.LunarView
      android:id="@+id/lunar"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
</FrameLayout>

以下、ファイルやメソッド単位の説明です。