AndroidのSurfaceViewの使い方

SurfaceViewはダブルバッファリングにも対応しており、ImageViewよりも描画 が高速に実行できます。
GLSurfaceViewの方がさらに高速ですが、GLSurfaceViewはコードが複雑になり がちな為、そこまで描画処理がクリティカルではない簡単な処理には有用です。

githubにコードを置きました。

 

1. SurfaceView

SurfaceViewはImageView等と異なり、フレーム側からの描画要求に答えません (invalidateで描画が実行されない)。 よって描画用のスレッドを自前で用意する必要があります。

SurfaceViewはSurfaceHolderを持っており、SurfaceHolderはSurfaceViewに描 画に用いるCanvasを提供します。SurfaceHolderはコンストラクタで取得する と良いでしょう。

public class SampleSurfaceView extends SurfaceView {
  
  private SurfaceHolder mSurfaceHolder;
 
  public void initialized() {
    mSurfaceHolder = getHolder();
    mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
  }
 
  public SampleSurfaceView(Context context) {
    super(context);
    initialized();
  }
 
  public SampleSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialized();
  }
 
  public SampleSurfaceView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialized();
  }
 
}
 

ImageView等と同様に、コンストラクタを定義することで、xmlで定義されたパ ラメータ(レイアウト情報等)を取得することができます。

2. SurfaceHolder.Callback

SurfaceHolder.CallbackはSurfaceViewのサーフェイスが作成された、変更さ れた、破棄された際のコールバック関数群を提供するインターフェースです。

SurfaceViewを拡張したクラスで本インターフェースを実装するのが自然です (SurfaceHolderに関する制御を一極化する為)。 複数のSurfaceViewに対して共通したSurfaceHolder.Callbackを用いる場合に は拡張とインターフェースを別々にすれば良いでしょう。 インターフェース側では引数経由でSurfaceHolderが取得できるので、どの SurfaceViewから呼ばれたかが把握できます。

有効にするにはSurfaceHolder.addCallbackでインターフェースを登録します。

public class SampleSurfaceView extends SurfaceView implements
    SurfaceHolder.Callback {
  
  private SurfaceHolder mSurfaceHolder;
 
  public void initialized() {
    mSurfaceHolder = getHolder();
    mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
  }
 
<snip>
 
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
    /** Do nothing. */
  }
  
  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width,
      int height) {
    /** Do something. */
  }
  
  /**
   * A method of surfaceChanged will be called after surfaceCreated called
   * orientation of SurfaceView changed before surfaceChanged.
   */
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    /** Do something. */
  }
  
}

今回はextends SurfaceViewとimplements SurfaceHolder.Callbackが同じクラ スなので、コールバック群の引数に設定されるSurfaceholderは使用しません。

3. SurfaceViewとSurfaceHolder.Callbackのライフサイクル

SurfaceViewを使用する上で極めて重要なのが、どのタイミングでコールバッ ク郡が呼ばれるかです。
どのタイミングで描画を始めれば良いのかが明確ではないです。

Re:Kayo-System Co.,Ltd.様のAndroid2.3と4.1のActivityの挙動の違いが極め て良い実験をしており、Activityを含めたSurfaceViewのライフサイクルにつ いて言及してます。

ActivityクラスのonCreate内でsetContentVieを呼び出すことで、各Viewのコ ンストラクタが呼ばれます。

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main); /** Call View's constructor. */
  }

SurfaceViewのコンストラクタが実行され、ActivityのonResumeが実行される と、ようやくSurfaceView.Callbackが呼ばれます。 ここで初めてSurfaceChangedが呼ばれ、SurfaceViewのサーフェースの幅と高 さが分かります。

  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width,
      int height) {
    /** 幅と高さを元に、描画のレイアウトを設定。 */
    /** これ以降、描画処理が可能になる。 */
  }

よって描画処理を実装するにあたり、surfaceChanged内で描画用のスレッドを 開始し、surfaceDestroy内でスレッドを停止すると良いことになります。

3. SurfaceView上のonTouchEvent

onTouchEvent等のイベントはSurfaceView上の座標となります。つまり端末の 座標に対して、SurfaceViewの設置座標を引いた座標となります。

例えば、SurfaceViewが二つ並んでいた場合、それぞれのSurfaceViewでx = 0, y = 0の座標を持つことになります。 それぞれのSurfaceView上でイベント処理が完結することになります。

4. draw(Canvas canvas)

SurfaceViewでdraw(Canvas canvas)メソッドをオーバライドした場合、レイア ウトエディタはdraw(Canvas canvas)を使用するらしく、エラーになってしま います。
SurfaceViewではdraw(Canvas canvas)をオーバライドしない方が良いです。

5. SurfaceViewの使用例

SurfaceView上に描画順序が決まったレイヤを作成します。 各レイヤではsurfaceChanged経由での描画オブジェクトのサイズ変更、draw経 由での描画オブジェクトの描画を実行します。

/**
 * Interface between SampleSurfaceView and SampleSurfaceViewLayer.
 */
public interface SampleSurfaceViewInterface {
  
  public void surfaceChanged(SurfaceHolder holder, int format, int width,
      int height);
  
  public void draw(Canvas canvas);
  
}

スレッドはActivityで作成し、そのスレッドで実行するRunnableを surfaceChanged内で登録するようにしました。

/**
 * SampleSurfaceView is a sample for SurfaceView and SurfaceHolder.Callback.
 * This has multiple draw layer for overlaying multiple draw objects and use
 * draw thread which is out of SurfaceView. If you have only one SurfaceView,
 * draw thread should be in SurfaceView.
 */
public class SampleSurfaceView extends SurfaceView implements
    SurfaceHolder.Callback {
  
  /** SurfaceHolder for Surface.Callback. */
  private SurfaceHolder mSurfaceHolder;
  
  /** Draw thread. */
  private SampleThread mThread;
  
  /** Runnable registered to draw thread, which calls draw(). */
  private Runnable mRunnable;
  
  /** Layer List on SurfaceView. */
  private List<SampleSurfaceViewInterface> mLayerInterface;
  
  /**
   * With SurfaceHolder.addCallback method SurfaceHolder.Callback like
   * surfaceCreated will be called.
   */
  private void initialized() {
    mSurfaceHolder = getHolder();
    mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
    mSurfaceHolder.addCallback(this);
    mThread = null;
    mRunnable = new Runnable() {
      @Override
      public void run() {
        draw();
      }
    };
    mLayerInterface = new ArrayList<SampleSurfaceViewInterface>();
  }
  
  public SampleSurfaceView(Context context) {
    super(context);
    SampleLog.d("SampleSurfaceView(Context context)");
    initialized();
  }
  
  /**
   * A constructor of SurfaceView(Context context, AttributeSet attrs) will be
   * called from setContentView() in Activity.
   */
  public SampleSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    SampleLog.d("SampleSurfaceView(Context context, AttributeSet attrs)");
    initialized();
  }
  
  public SampleSurfaceView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    SampleLog
        .d("SampleSurfaceView(Context context, AttributeSet attrs, int defStyle)");
    initialized();
  }
  
  /** This method must be called before surfaceChanged */
  public void setThread(SampleThread thread) {
    mThread = thread;
  }
  
  public void addLayerInterface(SampleSurfaceViewInterface layerInterface) {
    synchronized (mLayerInterface) {
      mLayerInterface.add(layerInterface);
    }
  }
  
  public void removeLayer(SampleSurfaceViewInterface layerInterface) {
    synchronized (mLayerInterface) {
      mLayerInterface.remove(layerInterface);
    }
  }
  
  /** A method of surfaceCreated will be called after surface created. */
  @Override
  public void surfaceCreated(SurfaceHolder holder) {
    SampleLog.d("surfaceCreated(SurfaceHolder holder");
    /** Do nothing. */
  }
  
  /**
   * A method of surfaceChanged will be called after surfaceCreated called or
   * orientation of SurfaceView changed.
   */
  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width,
      int height) {
    SampleLog
        .d("surfaceChanged(SurfaceHolder holder, int format, int width, int height)");
    
    /** mThread must be set before this. */
    assert (mThread != null);
    
    /** Resize multiple layers on SurfaceView. */
    synchronized (mLayerInterface) {
      for (SampleSurfaceViewInterface layerInterface : mLayerInterface)
        layerInterface.surfaceChanged(holder, format, width, height);
    }
    
    /** Enable mRunnable which calls draw() */
    mThread.addRunnable(mRunnable);
  }
  
  /**
   * A method of surfaceChanged will be called after surfaceCreated called
   * orientation of SurfaceView changed before surfaceChanged.
   */
  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
    SampleLog.d("surfaceDestroyed(SurfaceHolder holder)");
    mThread.removeRunnable(mRunnable);
  }
  
  /**
   * A draw method should be called from draw thread. I think that one draw
   * thread should be responsible for drawing multiple SurfaceView. So, draw
   * thread should be implemented out of SurfaceView. If override draw(Canvas
   * canvas), layout editor on eclipse will be error.
   */
  public void draw() {
    Canvas canvas = mSurfaceHolder.lockCanvas();
    if (canvas == null)
      return;
    
    /** Clear canvas. */
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    
    /** Draw multiple layers on SurfaceView. */
    synchronized (mLayerInterface) {
      for (SampleSurfaceViewInterface layerInterface : mLayerInterface)
        layerInterface.draw(canvas);
    }
    
    mSurfaceHolder.unlockCanvasAndPost(canvas);
  }
  
  /**
   * Each SurfaceView has its onTouchEvent. event.getX() and event.getY() are
   * positions on SurfaceView not display.
   */
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    SampleLog.d("event.getX() = " + event.getX() + ", event.getY() = "
        + event.getY());
    return true;
  }
  
}