自作SurfaceViewをxmlレイアウトで使う

自作したSurfaceViewをxmlで使うためにどのような変更をすればいいかをまとめておく。
理解して書いているわけではないので、試行錯誤の一つとして読んでいただければと思います。

#SurfaceViewをそのまま使うとき

今回はSurfaceViewの表示だけでなく、タッチの検出などもしたい状況を想定し、画面の幅と高さに加えてタッチした回数や場所を表示するSurfaceView自作した場合を考えます。
起動時と何回かタッチした後の画面は以下のようになります。
　　<img width="300" alt="Screenshot_1575687627.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/a2547eef-f236-f0b9-8253-df490810633b.png">　　　　<img width="300" alt="Screenshot_1575687634.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/2596ba2e-249f-8bee-129f-d596f0eac9f2.png">

SurfaceViewのコードは以下のようにしました。

```MySurfaceView.kt
class MySurfaceView (context: Context): SurfaceView(context), SurfaceHolder.Callback, Runnable {

    private val holder1: SurfaceHolder
    private var thread: Thread? = null
    private var canvas: Canvas? = null

    private var count: Int = 0
    private var canvasWidth : Int = 0
    private var canvasHeight : Int = 0

    private var xTouched : Int? = null
    private var yTouched : Int? = null

    init{
        holder1 = getHolder()
        holder1.addCallback(this)
        thread = Thread(this)
        count = 0

    }

    override fun surfaceCreated(holder2: SurfaceHolder) {
        thread?.start()
        canvasWidth = getWidth()
        canvasHeight = getHeight()
    }

    override fun surfaceChanged(holder3: SurfaceHolder, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder4: SurfaceHolder) {
        thread = null
    }

    //タッチした時の処理
    override fun onTouchEvent(event: MotionEvent): Boolean {
        count++
        xTouched = event.x.toInt()
        yTouched = event.y.toInt()

        Log.v("CHECK", "Touched")
        return false
    }

    //キャンバスに描画
    override fun run() {
        //文字の詳細を設定
        val paint = Paint()
        paint.setTextSize(80.0F)
        paint.color = Color.BLACK
        paint.isAntiAlias = true

        while(thread != null) {
            //描画の準備
            canvas = holder1.lockCanvas()

            canvas?.drawColor(Color.WHITE)

            canvas?.drawText("幅:$canvasWidth 高さ:$canvasHeight", 10.0F, 80.0F * 1, paint)
            canvas?.drawText("タッチ回数:$count", 10.0F, 80.0F * 2, paint)

            if (xTouched != null) {
                canvas?.drawText("タッチ位置:($xTouched, $yTouched)", 10.0F, 80.0F * 3, paint)
            }

            //描画する
            holder1.unlockCanvasAndPost(canvas)
        }
    }
}
```

setContentViewに自作SurfaceViewを直接設定するとちゃんと表示できます。

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var mMySfV : MySurfaceView? = n![Screenshot_1575688981.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/58816fb6-6d3c-4ed9-7819-a34e0d99fab6.png)
ull

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mMySfV = MySurfaceView(this)
        setContentView(mMySfV)

    }
}
```

#xmlレイアウトで表示
以下のページを参考にまずはSurfaceViewを表示する。
[XMLレイアウトでSurfaceViewを用いる](https://www.ipentec.com/document/android-surface-view-using-in-xml-layout)

とりあえず、xmlを以下のようにし、Activityを変更すると画面が変わる。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <SurfaceView
        android:id="@+id/SurfaceViewMain"
        android:layout_width="fill_parent"
        android:layout_height="300dp">
    </SurfaceView>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World" />

</TableLayout>
```

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var mMySfV : MySurfaceView? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mMySfV = MySurfaceView(this)
        setContentView(R.layout.activity_main)

    }
}
```

<img width ="500" alt ="Screenshot_1575688981.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/54e5baf0-e9ba-3629-1069-defdb98010bb.png">

このSurfaceViewが配置されている画面に自作SurfaceViewを表示するためには自作SurfaceViewはSurfaceViewを受け渡して生成するようにする。そしてHolderは受け取ったSurfaceViewのHolderを使う。
onCreate()でこれをやるとうまくいかないので、onStartで行う。

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    private var mMySfV : MySurfaceView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
    }

    override fun onStart(){
        super.onStart()

        val mSfV = findViewById<SurfaceView>(R.id.SurfaceViewMain)
        mMySfV = MySurfaceView(this, mSfV)
    }
}
```

MySurfaceViewは最初の部分を変更するだけでよい。

```MySurfaceView.kt
class MySurfaceView (context: Context, surfaceview : SurfaceView):
 SurfaceView(context), SurfaceHolder.Callback, Runnable { //コンストラクタも変更


    private val sv : SurfaceView     //追加
    private val holder1: SurfaceHolder
    private var thread: Thread? = null
    private var canvas: Canvas? = null

    private var count: Int = 0
    private var canvasWidth : Int = 0
    private var canvasHeight : Int = 0

    private var xTouched : Int? = null
    private var yTouched : Int? = null

    init{
        sv = surfaceview            //追加
        holder1 = sv.holder         //変更
        holder1.addCallback(this)
        thread = Thread(this)
        count = 0

    }
    
    //以下は同じ
```

すると以下のように表示される。残念ながら、widthやheightが正しく取得できておらず、タッチにも反応しない。
<img width = "500" alt = "Screenshot_1575689889.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/4bd2ace2-10a5-a719-cf90-0bada06affc6.png">

#widthの取得とタッチの検出

widthやheightはsurfaceChangedの中で取得できる。実験してみるとgetWidth()やgetHeight()では取得できない。

```MySurfaceView

class MySurfaceView (context: Context, surfaceview : SurfaceView): SurfaceView(context), SurfaceHolder.Callback, Runnable {

    //同じ

    override fun surfaceChanged(holder3: SurfaceHolder, format: Int, width: Int, height: Int) {
        canvasWidth = width
        canvasHeight = height
    }

    //同じ
}
```

MySurfaceViewは画面に配置されてるわけではないから、タッチで反応しないのは当然である。自然な方法が何かはわからないが、xmlに配置しているSurfaceViewでタッチを感知して、そのなかでMySurfaceViewでやりたかった処理を行えばよい。
MySurfaceViewのonTouchEventを変更しておく。

```MySurfaceView.kt
class MySurfaceView (context: Context, surfaceview : SurfaceView): SurfaceView(context), SurfaceHolder.Callback, Runnable {

    //タッチした時の処理
    //override fun onTouchEvent(event: MotionEvent): Boolean {
    fun touched(event: MotionEvent) : Boolean {
        count++
        xTouched = event.x.toInt()
        yTouched = event.y.toInt()

        return false
    }
}
```

Activityは以下のように変更する。

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    private var mMySfV : MySurfaceView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
    }

    //以下を追加する
    override fun onStart(){
        super.onStart()

        val mSfV = findViewById<SurfaceView>(R.id.SurfaceViewMain)
        mMySfV = MySurfaceView(this, mSfV)

       mSfV.setOnTouchListener(object : View.OnTouchListener{
           override fun onTouch(v: View?, event:MotionEvent?):Boolean{
               return mMySfV?.touched(event!!)!!
           }
       })
    }
}
```

以上の変更でうまくいく。
　　　<img width = "300" alt ="Screenshot_1575716538.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/aa253c3b-6862-7410-a347-0052b02c050c.png">　　　　<img width = "300" alt="Screenshot_1575716525.png" src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/3823e93c-5887-23b7-526a-a1012086f7ef.png">

#最初の動機
もともとは、[『炎のAndroid開発道場』](https://www.amazon.co.jp/%E7%82%8E%E3%81%AEAndroid%E9%96%8B%E7%99%BA%E9%81%93%E5%A0%B4-%E3%82%88%E3%81%A1%E3%82%88%E3%81%A1Android%E3%82%B5%E3%83%B3%E3%83%87%E3%83%BC%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E4%BC%9A-%E5%88%9D%E5%BF%83%E8%80%85%E6%95%99%E8%82%B2%E5%A7%94%E5%93%A1%E4%BC%9A/dp/4897979013)の演習にあった「15パズル」をxmlに配置する方法を調べたのが動機だった。オリジナルなものだと以下のような画面になる。

<img width = "500" alt = "Screenshot_1575041838.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/e10ad852-e971-d828-e927-c34451ca9969.png">

上で説明したことをのこの場合に適用すると、xmlでこの自作SurfaceViewが使える。
<img width = "300" alt = "Screenshot_1575634762.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/576fcebf-d5b6-2ae3-4616-80a67ef32a09.png">　　　　<img width = "300" alt = "Screenshot_1575638134.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/6628b210-7e85-a619-1f85-41afcc38a4d2.png">