AndroidでSurfaceViewを使用する

こんにちは、Khabravchans!

Androidで2Dグラフィックスを使用する場合、Canvasを使用してレンダリングを実行できます。 これを行う最も簡単な方法は、Viewから継承したクラスを使用することです。 onDraw()メソッドを記述し、パラメータとして提供されるキャンバスを使用して、必要なすべてのアクションを実行するだけです。 ただし、このアプローチには欠点があります。 onDraw()メソッドはシステムによって呼び出されます。 invalidate()メソッドを手動で使用して、再描画の必要性をシステムに伝えることができます。 ただし、invalidate()を呼び出しても、onDraw()メソッドがすぐに呼び出されるとは限りません。 したがって、(たとえば、あらゆるゲームで)常に描画する必要がある場合、上記の方法を考慮する価値はほとんどありません。



別のアプローチがあります-SurfaceViewクラスを使用します。 公式ガイドを読んでいくつかの例を検討した後、私はロシア語で短い記事を書くことにしました。 この記事は初心者を対象としています。 ここでは、複雑で難しいテクニックについて説明しません。



SurfaceViewクラス



SurfaceViewクラスの機能は、描画用の別の領域を提供することです。アクションは、別のアプリケーションストリームで取り出す必要があります。 したがって、アプリケーションは、システムがビュー要素の階層全体をレンダリングする準備ができるまで待機する必要はありません。 補助ストリームは、SurfaceViewのキャンバスを使用して、必要な速度でレンダリングできます。



実装全体は、主に2つのポイントになります。

  1. SurfaceViewから継承したクラスを作成し、SurfaceHolder.Callbackインターフェイスを実装します
  2. レンダリングを制御するストリームを作成します。


クラス作成



前述のように、SurfaceViewを拡張し、SurfaceHolder.Callbackインターフェイスを実装する独自のクラスが必要です。 このインターフェイスは、3つのメソッド、surfaceCreated()、surfaceChanged()、およびsurfaceDestroyed()を提供します。これらは、ペイント、変更、および破壊のための領域を作成するときに呼び出されます。

キャンバスを使ってペイントする作業は、作成したクラスから直接実行されるのではなく、SurfaceHolderオブジェクトを使用して実行されます。 getHolder()メソッドを呼び出すことで取得できます。 レンダリング用のキャンバスを提供するのはこのオブジェクトです。

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { public MySurfaceView(Context context) { super(context); getHolder().addCallback(this); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }
      
      





クラスコンストラクターでSurfaceHolderオブジェクトを取得し、addCallback()メソッドを使用して、対応するコールバックを受信することを示します。

surfaceCreated()メソッドでは、原則として、レンダリングを開始する必要がありますが、surfaceDestroyed()では、逆に終了します。 今のところ、これらのメソッドの本体を空のままにして、レンダリングを担当するフローを実装します。



レンダリングストリームの実装



Threadから継承したクラスを作成します。 コンストラクターでは、画面に描画する画像を読み込むためにSurfaceHolderとResourcesの2つのパラメーターが必要です。 また、クラスでは、描画が進行中であることを示すフラグ変数と、この変数を設定するメソッドが必要です。 もちろん、run()メソッドをオーバーライドする必要があります。

その結果、次のクラスを取得します。

 class DrawThread extends Thread{ private boolean runFlag = false; private SurfaceHolder surfaceHolder; private Bitmap picture; private Matrix matrix; private long prevTime; public DrawThread(SurfaceHolder surfaceHolder, Resources resources){ this.surfaceHolder = surfaceHolder; //  ,    picture = BitmapFactory.decodeResource(resources, R.drawable.icon); //      matrix = new Matrix(); matrix.postScale(3.0f, 3.0f); matrix.postTranslate(100.0f, 100.0f); //    prevTime = System.currentTimeMillis(); } public void setRunning(boolean run) { runFlag = run; } @Override public void run() { Canvas canvas; while (runFlag) { //         //    long now = System.currentTimeMillis(); long elapsedTime = now - prevTime; if (elapsedTime > 30){ //    30  -    //     2 . //   -   prevTime = now; matrix.preRotate(2.0f, picture.getWidth() / 2, picture.getHeight() / 2); } canvas = null; try { //   Canvas    canvas = surfaceHolder.lockCanvas(null); synchronized (surfaceHolder) { canvas.drawColor(Color.BLACK); canvas.drawBitmap(picture, matrix, null); } } finally { if (canvas != null) { //  .     surfaceHolder.unlockCanvasAndPost(canvas); } } } } }
      
      





結果が非常に新鮮に見えないようにするために、ダウンロードした画像を3回増やし、画面の中央にわずかに移動します。 これは、変換マトリックスを使用して行います。 また、30ミリ秒ごとに1回だけ、画像をその中心の周りに2度回転させます。 もちろん、キャンバス自体への描画は別の方法で行う方が良いですが、この場合は画面をクリアして画像を描画するだけです。 そのため、そのままにしておくことができます。



描画の開始と終了



さて、レンダリングを制御するスレッドを作成した後、SurfaceViewクラスの編集に戻りましょう。 surfaceCreated()メソッドで、ストリームを作成して開始します。 そして、メソッドsurfaceDestroyed()でその作業を終了します。 その結果、MySurfaceViewクラスは次の形式を取ります。

 public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { private DrawThread drawThread; public MySurfaceView(Context context) { super(context); getHolder().addCallback(this); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { drawThread = new DrawThread(getHolder(), getResources()); drawThread.setRunning(true); drawThread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; //    drawThread.setRunning(false); while (retry) { try { drawThread.join(); retry = false; } catch (InterruptedException e) { //   ,       } } } }
      
      





ストリームの作成は、surfaceCreated()メソッドで実行する必要があることに注意してください。 公式ドキュメントのLunarLanderの例では、レンダリングストリームの作成は、SurfaceViewから継承されたクラスのコンストラクターで行われます。 しかし、このアプローチでは、エラーが発生する場合があります。 デバイスのHomeキーを押してアプリケーションを最小化してから再度開くと、IllegalThreadStateExceptionが発生します。



アプリケーションアクティビティは次のようになります。

 public class SurfaceViewActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new MySurfaceView(this)); } }
      
      







プログラムの結果は次のようになります(回転のため、画像は少しぼやけていますが、デバイス上ではかなり許容できるように見えます):





おわりに



上記のグラフィックスのレンダリング方法は、単純なビューをキャンバスに描画するよりも多少複雑ですが、場合によってはより望ましい方法です。 アプリケーションが頻繁にグラフィックを再描画する必要がある場合、違いに気付くことができます。 補助フローは、このプロセスをよりよく制御するのに役立ちます。

結論として、記事の作成時に使用したリソースへのリンクを提供したいと思います。

  1. 公式ドキュメントのLunarLanderゲームの例

  2. SurfaceViewの使用を説明する記事




All Articles