タッチイベントについて
端末の画面をタッチした情報はMotionEventとしてActivityやViewに通知されます。
MotionEventはさまざまな情報を持っています。
MotionEvent – Android Developers
- アクション(触れたのか、動かしたのか、離したのか)
- ポインタの数(何本の指で触っているのか)
- タッチした座標
これらは全てポインタごと別々に識別されていて、全てポインタのインデックスでアクセスすることが出来ます。(ポインタのIDではありません)
その辺りをごっちゃにしてハマった結果、Stackoverflowに投稿した質問がこちらです。Androidでマルチタッチ時のポインターIDを検出する方法(ちなみに投稿後に勘違いが原因であることに気づいた)
ポインタのインデックスは必ず0から始まり、getPointerCount() - 1
まで割り振られます。
例えば2本の指でタッチしている場合、getPointerCount()
は2を返します。1本目の指がポインタインデックス0で、2本目がインデックス1となります。
さらにこの状態で1本目の指を離すと、2本目の指のインデックスが0に変わります。
指を離す順番によってインデックスはころころ変わるため、特定のポインタを識別するのには使えません。
例えば人差し指、中指、薬指を使ったタップを考えましょう。途中で人差し指、薬指は離したり触れたりしているとします。しかし常に中指はつけたままにして、これをトラッキングしたいとします。この場合にはポインタインデックスを使うことは出来ません。
特定のポインタを識別するにはポインタIDを利用します。
一度タッチするとポインタにはIDが割り当てられ、そのIDは指を離すまで変わりません。
上記の例で言うと、中指を画面から離さないかぎり中指を示すポインタのIDは常に同じです。
一方で注意しなければいけないのは、座標を取得したりするメソッドの引数はポインタインデックスであるということです。
ポインタはIDで識別するけど、そのポインタの情報を取得するために必要なのはポインタインデックスです。
そのため、特定のポインタIDの座標を取得したりするには、findPointerIndex()
メソッドを使って、IDからポインタインデックスを引き出す必要があります。
ポインタインデックスは常に0から始まり、他のポインタが増減する度に再割当てされます。一方でポインタを識別するIDは、指が触れたときに割り振られ画面に触れている限りその値は変わりません。
例えばこんな感じになります。
インデックス0 ID0 人差し指 インデックス1 ID1 中指 インデックス2 ID2 薬指 ↓この状態で人差し指を離す インデックス0 ID1 中指 インデックス1 ID2 薬指 ↓人差し指でタッチする インデックス0 ID0 人差し指 インデックス1 ID1 中指 インデックス2 ID2 薬指
ポインタIDとポインタインデックスの値は、指を押した順番と反対に離す分には一致したままですが、押した順番とは異なる離し方をすると値がズレます。
タッチイベントはリアルタイムに配信されるわけではありません。
開発者向けオプションでポインタの位置を表示するようにすると、ポインタの軌跡がそのまま表示されますが、onTouchEvent()
にMotionEventが配信される間隔はマチマチです。例えばgetX()
で取得できる座標は飛び飛びになってしまいます。
手書きの文字を描画しようと思うと、getX()
メソッドだけを使っていると、描画処理の分MotionEventが配信される間隔が空いてしまい、描画できる線がカクカクしてしまうことでしょう。
しかしちゃんとMotionEventには、前回onTouchEvent
に配信されてから今回配信されるまでの間に記録している情報が格納されて配信されています。
getHisorySize()
を使うことで、以前のonTouchEvent
が呼ばれてから今回のイベントが呼ばれるまでに、いくつのイベントを保持しているかが分かります。
ヒストリー情報を使ってポインタの情報を取得するには、getHistoricalX()
といったメソッドを利用することになります。
タッチイベントの種類(触れたのか、離したのか、動かしたのか)はgetAction()
で取得できます。
しかしgetAction()
で取得できる情報は、ポインタのインデックスとポインタごとのアクションがごちゃまぜになった情報になります。例えば2本指同時押しだとgetAction()
では261という数字が返ります。ちなみに1本でタッチすれば0です。
これはgetAction()
がアクションの発生したポインタインデックスと、ポインタインデックスごとのアクションを全てまとめた値を取得するメソッドだからです。
getActionIndex()
でアクションが発生したポインタのインデックスが分かります。
getAcitonMasked()
は動作を表す純粋なアクションだけを返します。
つまりタップ(MotionEvent.ACTION_DOWN)を検出したい場合、マルチタッチを考慮するとgetActionMasked()
を使う必要があるということです。getAciton()
では二本指での同時押しを検出できない可能性があります。
座標はgetX()
でX座標、getY()
でY座標を取得できます。
引数にポインタインデックスを渡すことで、指定したポインタインデックスの示す座標を取得できます。引数を省略した場合には、インデックス0の座標が取得できます。
getRawX()
やgetRawY()
と、getX()
やgetY()
の違いは、どこを基準とした座標数値が取得できるかです。
getRawX()
などは座標の補正を行わない、端末のスクリーン上の座標を示します。スクリーンの左上をX=0,Y=0とした座標になります。Raw座標はポインタインデックス0のものしか取得できないみたいです。
対してgetX()
はMotionEventを受け取るViewの左上をX=0,Y=0とした座標に変換されます。
getSize()
でサイズが取得できます。
このサイズは何かというと、多分タッチパネルが認識しているタッチの範囲とでも言いましょうか、指のサイズみたいなイメージです。
指の触れる範囲を増やしていくとサイズも大きくなります。
getPressure()
で圧力を取得できます。
感圧式のタッチパネルならそのまま圧力(どれくらいの強さで押しているのか)が分かるのだと思います。
静電気を検出する静電容量方式タッチパネルでも値は変動しますが、純粋な意味での圧力を示しているわけではありません。指の触れている範囲が大きくなれば圧力も大きくなるみたいです。
指で触れているのか、スタイラスなのかというのが、getToolType()
を使うことで検出できます。
しかしこの情報でスタイラスを識別するには、当然ながらスタイラスが端末に「自分はスタイラスである」と情報を送信している必要があります。
スタイラスを識別する万能メソッドではないことは注意が必要でしょう。少なくとも端末とペアリングするタイプのスタイラスでないと、ダメだと思います。
試していませんが、Bluetoothのマウスを端末にペアリングして使うと、これでマウスのポインタが識別できるのかもしれません。
ActivityであればonTouchEvent
をオーバーライドすればタッチイベントを受け取ることが出来ます。例えばこんなコードを利用することでタッチイベントを確認することが出来ます。
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
int actionMask = event.getActionMasked();
int actionIndex = event.getActionIndex();
int count = event.getPointerCount();
String text = "ACTION:" + action + " INDEX:" + actionIndex + "(id:" + event.getPointerId(actionIndex)
+ ") MASK:" + actionMask + "\n"
+ " pointer count:" + count + " x:" + event.getX() + " y:" + event.getY() + "\n";
for (int i = 0; i < count; i++) {
int id = event.getPointerId(i);
text += " pointer index:" + i + " pointer id:" + id
+ " x:" + event.getX(i)
+ " y:" + event.getY(i)
+ "\n";
}
textView.setText(text);
return true;
}
}