Kotlinで、マルチタッチ時に各指の位置/状態を取得

**2018/12/18追記**
Down時とUp時のコードに誤りがあったので、修正しました。

# やりたいこと

**「マルチタッチで、各指のダウン、移動、アップをそれぞれで取りたい！」**

…だったのですが、中々そのものズバリなサンプルが無かったので、色々組み合わせて作ってみました。

# コード

最終的には以下のようなコードになりました。

```kotlin
package touchtest

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import kotlinx.coroutines.experimental.GlobalScope
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch

class MainActivity : AppCompatActivity() {

    private var touchIDs: MutableList<Int> = mutableListOf();

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

    override fun onTouchEvent(event: MotionEvent?): Boolean {

        var pid = event!!.getPointerId(event!!.actionIndex)
        var type = ""
        var x = 0.0f
        var y = 0.0f
        when(event!!.actionMasked){
            MotionEvent.ACTION_POINTER_DOWN , MotionEvent.ACTION_DOWN -> {
                type="down"
                touchIDs.add(pid)
                val idx = event!!.findPointerIndex(pid)
                x = event!!.getX(idx)
                y = event!!.getY(idx)
                output(pid, type, x, y)
            }
            MotionEvent.ACTION_MOVE -> {
                type="move"
                for (id in touchIDs){
                    var idx = event!!.findPointerIndex(id)
                    pid = event!!.getPointerId(idx)
                    x = event!!.getX(idx)
                    y = event!!.getY(idx)
                    output(pid, type, x, y)
                }
                return false
            }
            MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP -> {
                type="up"
                touchIDs.remove(pid)
                val idx = event!!.findPointerIndex(pid)
                x = event!!.getX(idx)
                y = event!!.getY(idx)
                output(pid, type, x, y)
            }
            else -> return false
        }

        return super.onTouchEvent(event)
    }

    override fun onPause() {
        touchIDs.clear()
        super.onPause()
    }

    fun output(id: Int, type: String, x: Float, y: Float) = launch {
        Log.d("TAG", type + "," + id + "," + x + "," + y)
    }

}

```

# ポイント

## シングルタッチとマルチタッチで通り道が違う

一本目の指がダウンした時は```MotionEvent.ACTION_DOWN```が、二本目以降は```MotionEvent.ACTION_POINTER_DOWN```が呼ばれます。アップ時も同様です。

なのでコード中では

```kotlin
            MotionEvent.ACTION_POINTER_DOWN , MotionEvent.ACTION_DOWN -> {
                // ...略
            }
```

というようにして、どちらも同じ処理を通るようにしています。

```MotionEvent.ACTION_POINTER_DOWN```はAPI Level 5からの実装のようなので、互換性のためにこのような仕様になっているのでしょうか…

## event.x/yは常に最初の指の座標が格納されている

例えid3番の指がDown、Upした時であっても、```event.x```と```event.y```の値は常に最初の指の情報が入っているようです。
javaだと```getX(int id)```でidを指定して座標を取って来るのですが、オーバロードとして```getX()```で最初の座標を取って来れるようです。そう考えると納得の挙動なのですが、kotlinでは```getX()```のショートカットとして```x```があるので、見え方がややこしいですね…

## IDは必ずしもイベント毎に発行される訳ではない

僕自身がFlashやUnityの出身なので、マルチタッチ時には各タッチがオブジェクト化されていて、ID的なものからそのオブジェクトを拾ってきて…みたいな構造が頭にあったのですが、Androidのマルチタッチは少し考え方が違いました。

IDが重要である事には変わらないのですが、

```kotlin
            MotionEvent.ACTION_POINTER_DOWN , MotionEvent.ACTION_DOWN -> {
                // ...
                touchIDs.add(pid)
                // ...
            }

            // ...略

            MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP -> {
                // ...
                touchIDs.remove(event!!.getPointerId(event!!.actionIndex))
                // ...
            }
```

とあるように、IDはダウン時とアップ時に自分で取得して、自分で管理しなくてはいけません。FlashやUnityではこの辺りがまとまってオブジェクトになっている感じなので、混乱しました…

その上で、

```kotlin
            MotionEvent.ACTION_MOVE -> {
                type="move"
                for (id in touchIDs){
                    var idx = event!!.findPointerIndex(id)
                    pid = event!!.getPointerId(idx)
                    x = event!!.getX(idx)
                    y = event!!.getY(idx)
                    output(pid, type, x, y)
                }
                return false
            }
```

というように、改めてIDからポイントを取得する必要があるようです。

## 途中でpause状態になった時

デバッグ中に意図せずホームボタンを押して、ホームに戻ってしまった時があり、再度アプリを立ち上げてタッチを続けたらすぐに落ちてしまった…という事がありました。

原因としては、恐らくマルチタッチで触っているうちの1つの指がホームボタンに触れてしまった事で配列に格納しているIDの情報がそのまま残っていまい、次にタッチした時に存在しないIDを参照した事によって例外で落ちてしまったのではないか…と考えています。

その対策として

```shell
    override fun onPause() {
        touchIDs.clear()
        super.onPause()
    }
```

pause時にIDを格納している配列をクリアしています。

ちなみに、```findPointerIndex(id)```はネイティブコードを呼んでいるようで、ダイアログ等何も出ずにいきなりアプリが落ちました。

# 参考

[マルチタッチイベントを取得する](https://seesaawiki.jp/w/moonlight_aska/d/%A5%DE%A5%EB%A5%C1%A5%BF%A5%C3%A5%C1%A5%A4%A5%D9%A5%F3%A5%C8%A4%F2%BC%E8%C6%C0%A4%B9%A4%EB)
[Androidでマルチタッチを使う](https://qiita.com/tobira-code/items/a4e97ee775921da3334e)