[kotlin] surfaceViewで簡単お絵描き android アプリを作る

#今回やること
surfaceViewを使って簡単お絵描きアプリを作る。
リセットボタンと色を変えられる機能をつける。
こんな感じ ↓
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/93b5fb33-973a-fdcf-3ea6-56514351ba8f.gif" width=400>

作るファイルは３個(MainActivity.kt, CustomSurfaceView.kt, activity_main.xml)だけ
※以下少し長くなります。

#レイアウトファイルにsurfaceViewをのせる
android studio のパレットからsurfaceView(widgetsとかにある)をドラッグする。
色変更ボタンとリセットボタンを配置する

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <Button
        android:id="@+id/blackBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="くろ"
        app:layout_constraintEnd_toStartOf="@+id/redBtn"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/redBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="あか"
        app:layout_constraintEnd_toStartOf="@+id/greenBtn"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/blackBtn"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/greenBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="みどり"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/redBtn"
        app:layout_constraintTop_toTopOf="parent" />

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/resetBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/redBtn" />

    <Button
        android:id="@+id/resetBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="リセット"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
<img width="450" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520167/bb014973-8d28-621d-c1c8-694aaafd6bf6.png">

#CustomSurfaceViewを作る
###コンストラクタ（初期値など）
とりあえずコピペでok

```kotlin:CustomSurfaceView.kt

class CustomSurfaceView: SurfaceView, SurfaceHolder.Callback{
    private var surfaceHolder: SurfaceHolder? = null
    private var paint: Paint? = null
    private var path: Path? = null
    var color: Int? = null
    var prevBitmap: Bitmap? = null
    private var prevCanvas: Canvas? = null
    private var canvas: Canvas? = null

    var width: Int? = null
    var height: Int? = null

    constructor(context: Context, surfaceView: SurfaceView) : super(context) {
        surfaceHolder = surfaceView.holder

        /// display の情報（高さ 横）を取得
        val size = Point().also {
            (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.apply {
                getSize(
                    it
                )
            }
        }

        /// surfaceViewのサイズ
        width = size.x
        height = size.y

        /// 背景を透過させ、一番上に表示
        surfaceHolder!!.setFormat(PixelFormat.TRANSPARENT)
        surfaceView.setZOrderOnTop(true)

        /// コールバック
        surfaceHolder!!.addCallback(this)

        /// ペイント関連の設定
        paint = Paint()
        color = Color.BLACK
        paint!!.color = color as Int
        paint!!.style = Paint.Style.STROKE
        paint!!.strokeCap = Paint.Cap.ROUND
        paint!!.isAntiAlias = true
        paint!!.strokeWidth = 15F
    }
}
```
`surfaceView`と`SurfaceHolder.Callback`を継承する。
widthとheightに関しては本当は`ViewTreeObserver`とか使ってsurfaceViewのサイズに合わせた方がいいが今回はこっちでも問題ないので簡単なスクリーンサイズを使用。
`setZOrderTop(true)`と`setFormat`がないと線とか何も表示されないので注意。（自分はこれに気付かず２日くらいハマった）

###データクラス
描画する際のpathと色を保存するデータクラスを作る。
新しくファイル作ってもCustomSurfaceViewに書いてもok

```kotlin:CustomSurfaceView.kt
    //// pathクラスの情報とそのpathの色情報を保存する
    data class pathInfo(
        var path: Path,
        var color: Int
    )
```

###Implement
Implementする。
canvasとbitmapを初期化するメソッドを作る

```kotlin:CustomSurfaceView.kt
    /// surfaceViewが作られたとき
    override fun surfaceCreated(holder: SurfaceHolder?) {
        /// bitmap,canvas初期化
        initializeBitmap()
    }

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

    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
        /// bitmapをリサイクル
        prevBitmap!!.recycle()
    }


    /// bitmapとcanvasの初期化
    private fun initializeBitmap() {
        if (prevBitmap == null) {
            prevBitmap = Bitmap.createBitmap(width!!, height!!, Bitmap.Config.ARGB_8888)
        }

        if (prevCanvas == null) {
            prevCanvas = Canvas(prevBitmap!!)
        }

        prevCanvas!!.drawColor(0, PorterDuff.Mode.CLEAR)
    }
```

今回BitmapはsurfaceViewがdestroyされたときにリサイクルする。bitmapはそのままにしておくとメモリーリークが発生する危険があるので使わなくなったらリサイクルしておく。

##描画メソッド

onTouchリスナーで感知して線を書くメソッドをつくる

```kotlin:CustomSurfaceView.kt
    private fun draw(pathInfo: pathInfo) {
        canvas = Canvas()

        /// ロックしてキャンバスを取得
        canvas = surfaceHolder!!.lockCanvas()

        //// キャンバスのクリア
        canvas!!.drawColor(0, PorterDuff.Mode.CLEAR)

        /// 前回のビットマップをキャンバスに描画
        canvas!!.drawBitmap(prevBitmap!!, 0F, 0F, null)

        //// pathを描画
        paint!!.color = pathInfo.color
        canvas!!.drawPath(pathInfo.path, paint!!)

        /// ロックを解除
        surfaceHolder!!.unlockCanvasAndPost(canvas)
    }

    /// 画面をタッチしたときにアクションごとに関数を呼び出す
    fun onTouch(event: MotionEvent) : Boolean{
        when (event.action) {
            MotionEvent.ACTION_DOWN -> touchDown(event.x, event.y)
            MotionEvent.ACTION_MOVE -> touchMove(event.x, event.y)
            MotionEvent.ACTION_UP -> touchUp(event.x, event.y)
        }
        return true
    }

    ///// path クラスで描画するポイントを保持
    ///    ACTION_DOWN 時の処理
    private fun touchDown(x: Float, y: Float) {
        path = Path()
        path!!.moveTo(x, y)
    }

    ///    ACTION_MOVE 時の処理
    private fun touchMove(x: Float, y: Float) {
        /// pathクラスとdrawメソッドで線を書く
        path!!.lineTo(x, y)
        draw(pathInfo(path!!, color!!))
    }

    ///    ACTION_UP 時の処理
    private fun touchUp(x: Float, y: Float) {
        /// pathクラスとdrawメソッドで線を書く
        path!!.lineTo(x, y)
        draw(pathInfo(path!!, color!!))
        /// 前回のキャンバスを描画
        prevCanvas!!.drawPath(path!!, paint!!)
    }
```

#surfaceViewをセット
上記で作ったCustomSurfaceViewをレイアウトのsurfaceViewにセットする

```MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /// CustomSurfaceViewのインスタンスを生成しonTouchリスナーをセット
        val customSurfaceView = CustomSurfaceView(this, surfaceView)
        surfaceView.setOnTouchListener { v, event ->
            customSurfaceView.onTouch(event)
        }
   }
```

とりあえずここまで来たら黒の線で絵を描けるようになってるはず。
最後にカラーチェンジとリセットを実装する

#色とリセットを追加
CustomSurfaceViewにメソッドを書き加える。

```kotlin:CustomSurfaceView.kt
    /// resetメソッド
    fun reset() {
        ///初期化とキャンバスクリア
        initializeBitmap()
        canvas = surfaceHolder!!.lockCanvas()
        canvas?.drawColor(0, PorterDuff.Mode.CLEAR)
        surfaceHolder!!.unlockCanvasAndPost(canvas)
    }

    /// color チェンジメソッド
    fun changeColor(colorSelected: String) {
        when (colorSelected) {
            "black" -> color = Color.BLACK
            "red" -> color = Color.RED
            "green" -> color = Color.GREEN
        }
        paint!!.color = color as Int
    }
```

最後にMainActivityにリスナーをセットして完成

```kotlin:MainActivity.kt
        /// カラーチェンジボタンにリスナーをセット
        /// CustomSurfaceViewのchangeColorメソッドを呼び出す
        blackBtn.setOnClickListener {
            customSurfaceView.changeColor("black")
        }
        redBtn.setOnClickListener {
            customSurfaceView.changeColor("red")
        }
        greenBtn.setOnClickListener {
            customSurfaceView.changeColor("green")
        }

        /// リセットボタン
        resetBtn.setOnClickListener {
            customSurfaceView.reset()
        }
```

完成！！

#おわり
少し長くなったが最後までついてきていただいた方はお絵描きアプリができてるはず。
最初はsurfaceViewの使い方がよくわからず戸惑ったが使ってみるとおもしろいことできそうだなって感じた。
自分のスマホで動くのは楽しくていいね！ Android最高。