SurfaceViewを用いた画面描画 - Android

SurfaceViewを用いて画面描画するコードを紹介します。

プロジェクトの作成

Android アプリケーションプロジェクトを作成します。
[New Android Application]ダイアログボックスが表示されますので、以下を設定します。
  • Application Name: "SurfaceView"
  • Project Name: "SurfaceView"
  • Package Name: "com.iPentec.surfaceview"
  • Minimum Required SDK: "API 8: Android 2.2 (Froyo)"
  • Target SDK: "API 17: Android 4.2 (Jelly Bean)"
  • Compile With: "API 17: Android 4.2 (Jelly Bean)"
  • Theme: "Holo Light with Dark Action Bar"

コード

下記のコードを記述します。

MainActivity.java

メインのアクティビティのコードです。
package com.iPentec.surfaceview;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    //setContentView(R.layout.activity_main); //コメントアウト
    setContentView(new MySurfaceView(this));  //追加
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
  }
}
解説
  //setContentView(R.layout.activity_main); //コメントアウト
  setContentView(new MySurfaceView(this));  //追加
が変更部分です。今回はリソースのXMLレイアウトを使わずに直接SurfaceViewをフォームに配置するため、"setContentView(R.layout.activity_main)"をコメントアウトし、"setContentView(new MySurfaceView(this));"を追記します。

MySurfaceView.java

クラスを追加して以下のコードを記述します。
package com.iPentec.surfaceview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import android.view.SurfaceView;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;

/**
 * TODO: document your custom view class.
 */
public class MySurfaceView extends SurfaceView  implements SurfaceHolder.Callback, Runnable {
    
  public MySurfaceView(Context context) { 
    super(context); 
  } 

  @Override
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
  }

  @Override
  public void surfaceCreated(SurfaceHolder holder) {
  }

  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
  }

  @Override
  public void run() {
  }
}

実行結果

プロジェクトを実行します。下図の何も表示されない画面が表示されます。

図形を描画する

上記のプログラムにコードを追加して図形を描画します。

MySurfaceView.java

package com.iPentec.surfaceview;

import android.content.*;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.*;
import android.os.Bundle;
import android.util.*;

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
  private Thread thread;
  private SurfaceHolder holder;

  public MySurfaceView(Context context){
    super(context);
    
    holder = getHolder();
    holder.addCallback(this);
    holder.setFixedSize(getWidth(), getHeight());

  }
  
  @Override
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
    thread = new Thread(this);
    thread.start();
}
  
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
  }
  
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    thread=null;
  }
  
  @Override
  public void run() {
    while (thread != null) {
      doDraw(holder);
    }
  }
  
  private void doDraw(SurfaceHolder holder) {
    Canvas canvas = holder.lockCanvas();
    if (canvas != null){
      Paint paint = new Paint();
      paint.setColor(Color.GREEN);
      canvas.drawColor(Color.BLACK);
      canvas.drawCircle(100, 100, 20, paint);
      holder.unlockCanvasAndPost(canvas);
    }
  }
}

解説

SurfaceView内のrunメソッドでループを作成し、doDrawメソッドを呼び出します。doDrawメソッド内にキャンバスへの描画処理を記述します。上記の例では、描画色を緑にし、(x,y)=(100,100)の位置に半径20の円を描画します。
lockCanvas後にcanvasが取得できずnullになるケースがあるため、lockCanvas()呼出し後にcanvasのnullチェックをしています。(チェックをしない場合、アプリの切り替えやホームボタンでアプリがバックグラウンドに回る際にcanvasがnullのためNullPointerExceptionで落ちるケースがありました。)

実行結果

プロジェクトを実行します。(x,y)=(100,100)の位置に緑色の円が描画されました。

連続して図形を描画する

連続して図形を描画することでアニメーションの動作を実装します。
上記のプログラムのMySurfaceView.javaにコードを追加します。

MySurfaceView.java

package com.iPentec.surfaceview;

import android.content.*;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.*;
import android.os.Bundle;
import android.util.*;

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
  private Thread thread;
  private SurfaceHolder holder;
  
  private float dx = 5, dy = 5;
  private float screenWidth, screenHeight;
  private static final float rectWidth = 40; 
  private static final float rectHeight = 40; 
  private int ballx=100;
  private int bally=100;
  
  public MySurfaceView(Context context){
    super(context);
    
    holder = getHolder();
    holder.addCallback(this);
    holder.setFixedSize(getWidth(), getHeight());
  }
  
  @Override
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
    screenWidth = w; 
    screenHeight = h; 
 
    thread = new Thread(this);
    thread.start();
  }
  
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
  }
  
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    thread=null;
  }
  
  @Override
  public void run() {
    while (thread != null) {
      if (ballx-rectWidth < 0 || ballx + rectWidth > screenWidth)
        dx = -dx;
      if (bally-rectHeight < 0 || bally + rectHeight > screenHeight)
        dy = -dy;
      ballx += dx;
      bally += dy;
      
      doDraw(holder);
    }
  }
  
  private void doDraw(SurfaceHolder holder) {
    Canvas canvas = holder.lockCanvas();
    if (canvas != null){
      Paint paint = new Paint();
      paint.setColor(Color.GREEN);
      canvas.drawColor(Color.BLACK);
      canvas.drawCircle(ballx, bally, 20, paint);
      holder.unlockCanvasAndPost(canvas);
    }
  }
}

解説

Runメソッド内の
  if (ballx-rectWidth < 0 || ballx + rectWidth > screenWidth)
    dx = -dx;
  if (bally-rectHeight < 0 || bally + rectHeight > screenHeight)
    dy = -dy;
  ballx += dx;
  bally += dy;
にて円を描く位置を求めています。円のx,y座標が画面端を超える場合(0以下、または画面幅を超える)場合は符号を反転しふきを変えています。rectWidth, rectHeightはボールの大きさです。drawCircleに与える描画座標は円の中心の座標のため、rectWidthが0の場合、円の中心が画面端を超えるまで円の進行方向が反転しません。

実行結果

プロジェクトを実行します。円が動きます。画面端で円の進行方向が反転し跳ね返ったように見えます。


補足 - コールバックとスレッド描画を別クラスにして実装するコード

上記のコードは、SurfaceViewとコールバックを受けるクラスと、スレッドクラスを同一のクラスにして実装していますが、規模が大きくなった場合、それぞれ別クラスにしたい場合が出てきます。SurfaceView, コールバック, スレッドクラスを分けて実装したコードが以下になります。

MainActivity.java

MainActivityのクラスです。こちらは変更はありません。
package com.iPentec.simplesurfaceviewthread;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        setContentView(new MySurfaceView(this));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }    
}

MySurfaceView.java

SurfaceViewのクラスです。SurfaceHoloderを取得し、addCallback()メソッドを呼び出しコールバック先のクラス(MySurfaceViewCallback)を設定します。
package com.iPentec.simplesurfaceviewthread;

import android.content.*;
import android.view.*;

public class MySurfaceView extends SurfaceView {
  //private Thread thread;
  private SurfaceHolder holder;

  public MySurfaceView(Context context){
    super(context);
     
    MySurfaceViewCallback msvc = new MySurfaceViewCallback();
    holder = getHolder();
    holder.addCallback(msvc);
    holder.setFixedSize(getWidth(), getHeight());
  }
}

MySurfaceViewCallback.java

SurfaceViewからのコールバックを受け取るクラスです。surfaceChangedイベントが発生するとき、画面描画用のスレッドを生成します。
package com.iPentec.simplesurfaceviewthread;

import android.view.SurfaceHolder;

public class MySurfaceViewCallback implements SurfaceHolder.Callback{
  private Thread thread;
  
  @Override
  public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) {
  
    MyDrawThread mdt = new MyDrawThread();
    mdt.holder = holder;
    mdt.screenWidth = w; 
    mdt.screenHeight = h; 
    thread = new Thread(mdt);
    thread.start();
  }
   
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
  }
   
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    thread=null;
  }
}

MyDrawThread.java

スレッドクラスです。SurfaceHolderを用いてCanvasを取得し画面描画をします。
package com.iPentec.simplesurfaceviewthread;

import android.view.SurfaceHolder;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;


public class MyDrawThread implements Runnable{
  public SurfaceHolder holder;
  
  private float dx = 5, dy = 5;
  public float screenWidth, screenHeight;
  private static final float rectWidth = 40; 
  private static final float rectHeight = 40; 
  private int ballx=100;
  private int bally=100;
  
  @Override
  public void run() {
    //while (thread != null) {
    while (true) {
      if (ballx-rectWidth < 0 || ballx + rectWidth > screenWidth)
        dx = -dx;
      if (bally-rectHeight < 0 || bally + rectHeight > screenHeight)
        dy = -dy;
      ballx += dx;
      bally += dy;
       
      doDraw(holder);
    }
  }
   
  private void doDraw(SurfaceHolder holder) {
    Canvas canvas = holder.lockCanvas();
    if (canvas != null){
      Paint paint = new Paint();
      paint.setColor(Color.GREEN);
      canvas.drawColor(Color.BLACK);
      canvas.drawCircle(ballx, bally, 20, paint);
      holder.unlockCanvasAndPost(canvas);
    }
  }
}

実行結果

実行結果は先のプログラムと同じになります。
著者
iPentecのプログラマー、最近はAIの積極的な活用にも取り組み中。
とっても恥ずかしがり。
最終更新日: 2024-01-04
作成日: 2013-03-19
iPentec all rights reserverd.