Androidで一本指のタッチのみに対応したアプリを作るのはそれほど難しくはない。
しかし、マルチタッチ対応のアプリとなると少々厄介だ。
そこで今回は、マルチタッチの処理を行えるようなサンプルを作成しながら理解していく。

仕様はこのようにする。
  • 最初にタッチした指(以降"指A"とする)の処理を行う
  • 指Aを離さずに、次にタッチした指(以降"指B"とする)で二本指の処理を行う
  • 三番目以降のタッチは無視する
ポインタIDとポインタインデックスを理解する
まずはここからだ。
指をタッチしたり離したりするとonTouchEvent()が呼ばれる。
従って、タッチ処理を行いたい場合はそれをオーバーライドする必要がある。

ひな形はこうなる。
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int eventAction = event.getActionMasked();
        int pointerIndex = event.getActionIndex();
        int pointerId = event.getPointerId(pointerIndex);
        
        switch (eventAction) {
        case MotionEvent.ACTION_DOWN:
            break;
            
        case MotionEvent.ACTION_POINTER_DOWN:
            break;
            
        case MotionEvent.ACTION_POINTER_UP:
            break;

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            break;
            
        case MotionEvent.ACTION_MOVE:
            break;
        
        return true;
    }
順に見ていこう。
冒頭はこのイベントに関する情報を収集している。
イベントアクションは説明するまでもなくイベントの種類だ。

ポインタインデックスとポインタIDとは何だろう。
どちらも同じ意味を持つように見えるが、これらは役割が異なる。

ポインタIDはタッチした指それぞれに割り当てられて、それは離されるまで不変のものだ。
例えば指Aには0、指Bには1、等と割り当てられる。
これはまさに私達が欲しい情報だ。

ではポインタインデックスはそれとどう違うのか。
これはMotionEvent内でポインタIDを制御するためのインデックスのようだ。

イメージとしてはこうだ。
ある配列があったとして、要素として保持されるのがポインタIDで、添字となるのがポインタインデックスとなる。

今ひとつピンと来ない感じではあるが、ここを理解するのはとても重要だ。
何故ならポインタインデックスとポインタIDは常に同じ組み合わせとなるとは限らないからだ。

二本指の操作をしている場合
ポインタインデックス:0、ポインタID:0
ポインタインデックス:1、ポインタID:1

となることもあれば
ポインタインデックス:0、ポインタID:1
ポインタインデックス:1、ポインタID:0

となることもあり得る。

アクションの種類を理解する
ACTION_DOWNは一本目の指のタッチがあった時に呼ばれる。
注意しなければならないのは、これが呼ばれるのは一本目の指の場合だけだということだ。
二本目以降の指がタッチした場合はACTION_POINTER_DOWNが呼ばれる。

なので、指Aをタッチした場合はACTION_DOWNが呼ばれ、指Bをタッチした場合はACTION_POINTER_DOWNが呼ばれるということになる。

指を離す場合も同じような流れだ。
最後の一本となる指が離されるときはACTION_UPが呼ばれ、そうでない指が離されるときはACTION_POINTER_UPが呼ばれる。

指B→指Aの順番に離した場合、指Bを離した時はACTION_POINTER_UPが呼ばれ、指Aを離した時はACTION_UPが呼ばれる。
では指A→指Bの順番に離した場合はどうだろうか。
当然、指Aを離した時はACTION_POINTER_UPが呼ばれ、指Bを離した時はACTION_UPが呼ばれる。

ACTION_CANCELは処理がキャンセルした時に呼ばれるらしいが、どういう時に起こるのかはよくわからない。
とりあえず、ACTION_UPと同じ処理をしていれば問題ないだろう。

ACTION_MOVEは指を動かしている場合に呼ばれる。

サンプルを作成する
ではサンプルの実装に入ろう。
まず、指の座標を表示するためにレイアウトにTextViewを追加しよう。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="" />

</LinearLayout>
一つ目のTextViewは指Aの座標を表示するためのもので、二つ目のTextViewは指Bの座標を表示するためのものだ。

次はActivityの実装だ。
ひな形はこうなる。
public class MainActivity extends Activity {
    private TextView mTextView1, mTextView2;
    private int mPointerID1, mPointerID2; // ポインタID記憶用

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mTextView1 = (TextView)findViewById(R.id.textView1);
        mTextView2 = (TextView)findViewById(R.id.textView2);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int eventAction = event.getActionMasked();
        int pointerIndex = event.getActionIndex();
        int pointerId = event.getPointerId(pointerIndex);
        
        switch (eventAction) {
        case MotionEvent.ACTION_DOWN:
            break;
            
        case MotionEvent.ACTION_POINTER_DOWN:
            break;
            
        case MotionEvent.ACTION_POINTER_UP:
            break;

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            break;
            
        case MotionEvent.ACTION_MOVE:
            break;
        }
        
        return true;
    }
}
3行目でポインタIDを記憶しておくための変数を定義してある。
これらを使うことで、例え何本の指でタッチしたとしても、指Aと指Bを判別できるようになる。

指一本で操作している場合の実装
まずはACTION_DOWNを実装しよう。
        case MotionEvent.ACTION_DOWN:
            mPointerID1 = pointerId;
            mPointerID2 = -1;
            break;
指AのポインタIDをmPointerID1に保存する。
指Bは当然まだタッチされていないのでmPointerID2には-1を入れておく。

次はACTION_MOVEの実装を行ってみよう。
        case MotionEvent.ACTION_MOVE:
            // 指の座標の更新
            float x1 = 0.0f;
            float y1 = 0.0f;
            if (mPointerID1 >= 0)
            {
                int ptrIndex = event.findPointerIndex(mPointerID1);
                x1 = event.getX(ptrIndex);
                y1 = event.getY(ptrIndex);
            }
            
            // ジェスチャー処理
            if (mPointerID1 >= 0 && mPointerID2 == -1)
            {
                // 1本目の指だけが動いてる時の処理
                mTextView1.setText(String.format("pointer1: %3.1f, %3.1f", x1, y1));
            }
            break;
7~9行が指Aの座標を取得している処理だ。
変数ptrIndexonTouchEvent()冒頭で定義したpointerIdと同値であるが、マルチタッチ時の処理を行う場合のことを考えて、わざわざ取得するようにしてある。

13~17行はTextViewの更新処理だ。
if文の条件をこのようにすることで、二本指でのタッチ時は処理を行わないように出来る。

最後にACTION_UPの実装だ。
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mPointerID1 = -1;
            mPointerID2 = -1;
            mTextView1.setText("");
            mTextView2.setText("");
            break;
ポインタIDとTextViewをリセットする。

では実行してみよう。
タッチしてみるとこのように表示される。
multitouch_ss1

指二本で操作している場合の実装
次はいよいよ指二本の場合だ。
ACTION_POINTER_DOWNをこのように実装しよう。
        case MotionEvent.ACTION_POINTER_DOWN:
            if (mPointerID2 == -1)
            {
                mPointerID2 = pointerId;
            }
            else if (mPointerID1 == -1)
            {
                mPointerID1 = pointerId;
            }
            break;
2~4行は指Bがタッチされた時にポインタIDを保持するための処理だ。
これは説明するまでもないだろう。

6~9行は指AのポインタIDを保持するための処理であるが、何故これが必要なのか?
指Aの処理はACTION_DOWNで定義したはずだ。

こういう場合を考えてみよう。
指Aと指Bの二本指での操作中に指Aを離したとしよう。
後述するが、この場合mPointerID1は-1となる。
次に再度指Aをタッチした場合mPointerID1にポインタIDを入れなくてはならないため、このようにしているというわけだ。

二本指操作中に指Cをタッチした場合、ACTION_POINTER_DOWNは当然呼ばれるが、mPointerID1mPointerID2は既にポインタIDが入っているので、何も行われない。

次はACTION_POINTER_UPを実装しよう。
        case MotionEvent.ACTION_POINTER_UP:
            if (mPointerID1 == pointerId)
            {
                mPointerID1 = -1;
                mTextView1.setText("");
            }
            else if (mPointerID2 == pointerId)
            {
                mPointerID2 = -1;
                mTextView2.setText("");
            }
            break;
ここは特に説明するまでもないだろう。
それぞれ離した指の後始末を行っているだけだ。

次にACTION_MOVEをこのように変更しよう。
        case MotionEvent.ACTION_MOVE:
            // 指の座標の更新
            float x1 = 0.0f;
            float y1 = 0.0f;
            float x2 = 0.0f;
            float y2 = 0.0f;
            if (mPointerID1 >= 0)
            {
                int ptrIndex = event.findPointerIndex(mPointerID1);
                x1 = event.getX(ptrIndex);
                y1 = event.getY(ptrIndex);
            }
            if (mPointerID2 >= 0)
            {
                int ptrIndex = event.findPointerIndex(mPointerID2);
                x2 = event.getX(ptrIndex);
                y2 = event.getY(ptrIndex);
            }
            
            // ジェスチャー処理
            if (mPointerID1 >= 0 && mPointerID2 == -1)
            {
                // 1本目の指だけが動いてる時の処理
                mTextView1.setText(String.format("pointer1: %3.1f, %3.1f", x1, y1));
            }
            else if (mPointerID1 == -1 && mPointerID2 >= 0)
            {
                // 2本目の指だけが動いてる時の処理
                mTextView2.setText(String.format("pointer2: %3.1f, %3.1f", x2, y2));
            }
            else if (mPointerID1 >= 0 && mPointerID2 >= 0)
            {
                // 1本目と2本目の指が動いてる時の処理
                mTextView1.setText(String.format("pointer1/2: %3.1f, %3.1f", x1, y1));
                mTextView2.setText(String.format("pointer2/2: %3.1f, %3.1f", x2, y2));
            }
            break;
7~18行で指Aと指Bの座標を取得している。
先程も出てきたが、このようにすることでそれぞれの座標を正常に取得することが出来る。

では実行してみよう。
指二本で操作しているときはこのような画面になる。
multitouch_ss2

以上でマルチタッチについては一通り理解できた。
これらを応用することで、アプリに様々なジェスチャーを導入できるだろう。

今回のソース一式はGithubに上げておいた。
AndroidSample/MultiTouchSample