仙石浩明の日記

2010年7月26日

Nexus One で Android 2.2 froyo のマルチタッチを試してみる tweets

Android は 2.1-update1 以降でマルチタッチ (Multi-touch) をサポートしている。 ところがマルチタッチといっても、 ピンチイン/ピンチアウトなどのジェスチャをサポートしているだけのアプリが大半で、 複数のタッチを独立に扱えるアプリはいまだほとんどなく、 iPhone と比べるとその差が際立っている。

どうして Android にはマルチタッチを活用したアプリケーションが無いのだろう? と思ったので、 マルチタッチを試すテストアプリ MultiTouch.java (apk) を書いてみた:

MultiTouch

タッチした位置にタッチの強さに応じた大きさの円を表示するだけの単純なアプリ。 指を移動すれば円も追随する。 Android ではタッチID が順に割り振られるので、 ID が 0 のタッチを赤色の円で、 ID が 1 のタッチを緑色の円で描いている。

プログラム上は ID が 2 のタッチを青色の円で描くことになっているが、 残念ながら現行の Android で同時に扱えるタッチは 2箇所のみ (追記: Samsung Galaxy S は 5箇所のマルチタッチが可能らしい) なので、 3箇所にタッチしても三つ目の円が描かれることはない。 だから例えば iPhone のアプリにあるような鍵盤楽器アプリを作ろうと思っても、 三つ以上の音を同時に鳴らすことはできない。

とはいえ、 2箇所のタッチを独立に扱えれば、 いろいろ応用が効くだろうにと思いつつ、 このテストアプリをいじっていると...

ありゃ?

上の写真の状態から、 左指を上方へ、右指を下方へ動かしただけなのだが、 両方の指の X 座標 (画面では上下方向) が交わった時点で両方の円の X 座標が入れ替わってしまって、 緑円は右指の動きに合わせて下へ動き、 赤円は左指の動きに合わせて上に動いた。 その結果、 指の位置と円の位置がずれてしまった:

MultiTouch Error

つまり複数のタッチ (xi, yi), (xj, yj) を個別に扱えるのではなく、 X 座標の集合 {xi, xj} と Y 座標の集合 {yi, yj} として検知している模様。 例えて言うならダイオード無しのスイッチ・マトリックスみたいな感じ。 だから (xi, yi), (xj, yj) の 2点をタッチしているのか、 (xi, yj), (xj, yi) の 2点をタッチしているのか、 の区別ができない。

Android のマルチタッチを扱うプログラムの書き方について解説している Web ページをほとんど見かけないので、 MultiTouch.java についても一応解説:

タッチイベントは、 MotionEvent インスタンスとして Android OS からアプリへ伝えられる。 MotionEvent#getAction() メソッドでイベント種別 (ACTION_DOWN ならタッチ開始、 ACTION_MOVE ならドラッグ、 ACTION_UP ならタッチ終了といった具合) が取得できる。

マルチタッチの場合、 すなわちすでにタッチしている状況において追加で別の場所にタッチした場合、 ACTION_DOWN の代りに ACTION_POINTER_DOWN イベントが伝えられる。 パネルから指が離れてタッチが終わる場合、 まだ他の場所でタッチが継続しているのなら、 ACTION_UP の代りに ACTION_POINTER_UP イベントが伝えられる。

今回のテストプログラムの場合、 タッチの座標が分かればよいので、 ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_MOVE は区別せずに、 put_points メソッド (後述) でイベントの座標を記録している。

        public boolean onTouchEvent(MotionEvent ev) {
            int action = ev.getAction();
            switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_MOVE:
                put_points(ev);
                break;
            case MotionEvent.ACTION_UP:
                points.remove(ev.getPointerId(0));
                break;
            case MotionEvent.ACTION_POINTER_UP:
                put_points(ev);
                int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                points.remove(ev.getPointerId(index));
                break;
            }
            Canvas canvas = getHolder().lockCanvas();
            if (canvas != null) {
                onDraw(canvas);
                getHolder().unlockCanvasAndPost(canvas);
            }
            return true;
        }

MotionEvent インスタンスには 複数 (現状 2個までだが、 API 仕様的には 256個まで可能) のタッチの座標および強さ (pressure) が格納できて、 getX(i), getY(i), getPressure(i) の各メソッドを呼び出すことにより、 i番目 (i=0, 1,...) のタッチの X座標、Y座標、強さがそれぞれ取得できる。 タッチの個数は getPointerCount() メソッドで取得できる。 マルチタッチをサポートしない Android 2.0 eclair だと getPointerCount() は常に 1 を返す (ちなみに 1.5 cupcake や 1.6 donut だと getPointerCount() は存在しない)。

各タッチは、 指がパネルに触れてから離れるまで固有の ID が OS によって割り振られるので、 アプリは各タッチの動きをそれぞれ追うことができる。 getPointerId(i) メソッドを使えば i番目のタッチの ID が取得できる。

put_points(MotionEvent ev) メソッドでは、 id をキーとし、 TouchPoint (X座標、Y座標、強さの組) を値とするハッシュテーブル points に、 各タッチの現在値を登録している:

    class TouchPoint {
        public float x;
        public float y;
        public float p;
    }
    Hashtable<Integer, TouchPoint> points;
        ...
        points = new Hashtable<Integer, TouchPoint>();
        ...

        void put_points(MotionEvent ev) {
            int count = ev.getPointerCount();
            for (int i=0; i < count; i++) {
                int id = ev.getPointerId(i);
                TouchPoint p = new TouchPoint();
                p.x = ev.getX(i);
                p.y = ev.getY(i);
                p.p = ev.getPressure(i);
                points.put(id, p);
            }
        }

現状 ID は 0 または 1 の値しかとらないようなので、 ハッシュテーブル (java.util.Hashtable) は牛刀な感を否めないが、 API 仕様上 ID は int 型としか規定していない (つまり最大値が 1ではなく int 型の上限となる可能性がある ^^;) ので、 配列ではなくハッシュテーブルを使った次第。

あとは SurfaceView#onDraw(Canvas canvas) にて、 points ハッシュテーブルに登録された座標、強さのデータ通りに円を描くだけ。

追記:
twitter で Galaxy S のマルチタッチはマトモと教えていただきました (_O_)
以下、頂いたツイートを (時間順に) 引用:

仙石浩明 gcd_org Androidのマルチタッチについて書いてるページが見当たらなかったのでブログを書いてみました https://www.gcd.org/blog/2010/07/613/ NexusOneだと座標を正しく取得できないケースがあるのですが他の機種だとどうですか? #androidjp
Northeye northeye @gcd_org Galaxy Sはわりとまとものようです http://www.youtube.com/watch?v=hVlsRCMltDg
仙石浩明 gcd_org @northeye すごい>Galaxy Sのマルチタッチ。これで3点以上のマルチタッチがサポートされたら言うことなしですね。 RT Galaxy Sはわりとまとものようです http://www.youtube.com/watch?v=hVlsRCMltDg
Northeye northeye @gcd_org 5点までいけるそうです http://www.youtube.com/watch?v=KRCDRXYJBCY
なかみちと nakamichito Galaxy Sではこんな感じです RT @gcd_org: Androidのマルチタッチについて書いてるページが見当たらなかったのでブログを書いてみました https://www.gcd.org/blog/2010/07/613/ #androidjp
なかみちと nakamichito URL忘れてた! http://bit.ly/bFBicn RT @nakamichito: Galaxy Sではこんな感じです RT @gcd_org: Androidのマルチタッチについて書いてるページが見当たらなかったのでブログを書いてみました #androidjp

すでに同種のテストアプリが Android Market で公開されていたのですね。
車輪の再発明をしてしまった (後悔はしていない ;-)。

Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 08:57

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.