NativeScriptでサクッとAndroidのSurfaceViewを実装する方法


NativeScriptで、タッチした場所に●(丸)を描画するプログラムを書いてみます。
AbsoluteLayoutなどを使っても出来るとは思いますが、今回あえてNativeScript標準ではサポートされていないAndroidのSurfaceViewを使って実装してみました。Androidでのみ動きます。

NativeScriptからネイティブのAndroid APIを呼ぶサンプルとしても参考にしてもらえれば。

参考)
https://docs.nativescript.org/runtimes/android/generator/extend-class-interface

環境

NativeScript 3.1 + Angular 4
Android 7.1で動作確認

完成画面

事前準備

tns-platform-declarations というTypeScriptの型定義ファイルを入れておきましょう。
IDE上でAndroidのAPIの補完やチェックが効くようになって開発が捗ります。

npm i tns-platform-declarations --save-dev

HTML

まず、HTMLには <Placeholder> という部品を配置します。

component.html
<Placeholder (creatingView)="creatingView($event)">

creatingViewイベントに、ビューを作成するイベントハンドラをバインドします。

TypeScript

コンポーネントのTypeScriptを見ていきましょう。

まず、SurfaceViewを継承したMySurfaceView クラスを実装していきます。

component.ts
/**
 * カスタムSurfaceView
 */
class MySurfaceView extends android.view.SurfaceView {

  constructor(context: android.content.Context) {
    super(context);
    // to transform the Android native class to a JavaScript object
    return global.__native(this);
  }

コンストラクタで return global.__native(this); しているのがポイント。
new したときの戻り値として、JavaScriptのオブジェクトではなく、ネイティブのJavaのオブジェクトが得られるようになります。

component.ts
  init(){
    var self = this;
    let holder = this.getHolder();

    holder.addCallback( new android.view.SurfaceHolder.Callback({

      surfaceCreated(holder: android.view.SurfaceHolder){
        self.drawCircle(100, 100);
        console.log('surface Created');
      },

      surfaceChanged(){},

      surfaceDestroyed(holder: android.view.SurfaceHolder){}

    }) );

    this.setOnTouchListener( new android.view.View.OnTouchListener({
      onTouch(v: android.view.View, ev: android.view.MotionEvent){
        if( ev.getAction() == android.view.MotionEvent.ACTION_DOWN ){
          self.drawCircle(ev.getX() , ev.getY() );
        }
        return true;
      }
    } ) );

    console.log('init');
  }

次に、コンストラクタで初期化後に呼ばれる init メソッドを実装します。
ここでは、SurfaceHolderのコールバックと、タッチイベントを取得する
OnTouchListenerをバインドしています。
SurfaceHolder.Callbackは、インターフェースですが、new することで
匿名関数として即インスタンス化しています。便利ですね!
surfaceCreated、surfaceChanged、surfaceDestroyedは
実装しなければいけないイベントメソッド達です。
以下のドキュメント、シンプルでわかりやすかったです。
http://qiita.com/croquette0212/items/24dc2b6de3730e831aab

component.ts
  drawCircle(x: number, y: number){
    let holder = this.getHolder();
    let c = holder.lockCanvas();
    c.drawColor( new Color("#000000").android );
    let p = new android.graphics.Paint();
    p.setStyle( android.graphics.Paint.Style.FILL );
    p.setColor( new Color("#FF0000").android );
    c.drawCircle(x, y, 50, p);
    holder.unlockCanvasAndPost(c);
  }

}

drawCircleメソッドは、引数でもらった座標に、赤の円を書く描画メソッドになります。
以下のページを参考にしました。
http://donsuka-kk.hatenablog.com/entry/20121114/1352870204

これでMySurfaceViewの準備ができました。
ではいよいよ、画面にSurfaceViewを紐付けます。

component.ts
export class Component {

  creatingView(args: any) {
    args.view = new MySurfaceView(args.context);
    console.log('creatingView done');
  }
}

MySurfaceViewのインスタンスを args.view へ紐付ければ完了です。

ビルドして動かしてみましょう!
タッチすると、タッチしたところに赤い丸が描画されます。

所感: NativeScriptすごい

SurfaceViewのプログラミングは初めてでしたが、NativeScriptでSurfaceViewへの描画、タッチイベントをハンドリングする実装まで出来ました。最も時間がかかったのは、NativeScriptからJavaのSurfaceViewクラスをExtendして、TypeScriptへのブリッジインターフェースを実装する箇所で、
これは今回かなり勘所がわかったので、次からは悩まなそうです。

SurfaceViewの操作自体は巷のAndroid向け開発資料をいくつか見て、ほぼそのままTypeScriptでサクッと実装できたのが感動的でした。

Javaで書かなくてはいけないはずのAndroid APIに、TypeScriptからプロパティアクセスしたり、メソッド呼出したり、クラス継承したり出来るのは本当に楽チン。感動しました。