◆PROCESSING 逆引きリファレンス
カテゴリー:スマホ(AndroidMode)
スワイプを検知するには(AndroidMode編)
【概要】
PROCESSINGにAndroidMode を導入する事で、PROCESSINGで開発したプログラムをAndroid端末上で動かす事ができるようになります。
AndroidModeの導入については「PROCESSINGをAndroid端末で動かすには(4.0版)」記事を参照してください。
スワイプとは画面をタップした状態で、そのまま指を離さずに、一定方向へゆっくりと滑らせて移動する操作です。画面の切り替え操作などに利用しますね。
応用例として、タッチアンドホールド(長押し)した状態から、指を離さずに滑らせる操作を「ドラッグ」と呼ぶ事があります。
(画像URL:illust-AC 様:かみたまさん、amiyaoさん)
AndroidModeで画面のスワイプを検知するには、以下の2種類の方法があります。
- タッチ系イベント関数を利用する
- surfaceTouchEvent 関数を利用する
タッチ系イベント関数で指の移動を検知するのは、touchMoved 関数になります。
タッチ系イベント関数には、これ以外にもタップの開始を検知する touchStarted や終了を検知するtouchEnded があります。詳しくは「タップを検知するには(AndroidMode編)」記事を参照して下さい。
surfaceTouchEvent 関数はタッチ系イベント関数のように、操作毎に処理が分かれておらず、指の移動を含めた全てのイベントがこの関数に通知されてきます。
surfaceTouchEvent は、タッチ系イベント関数に比べて詳細な情報を得ることができます。
またダブルタップやフリックなど、より高度な操作を検知したい場合も、surfaceTouchEvent 関数を利用する事になります。
マルチタップなどを考慮するなら、タッチ系イベント関数よりも surfaceTouchEvent 関数を利用する事をオススメします。
【詳細】
タッチ系イベントで移動を知る
移動イベントvoid touchMoved ( TouchEvent touchEvent ) ;
touchEvent : イベント内容
画面上で指を動かした際に呼び出されます。シングルタップ、マルチタップのどちらでも、同じイベント関数が呼ばれます。
渡されてくる TouchEvent クラスのインスタンス変数を利用する事で、移動した指の数や座標を知ることができます。
float x = touchEvent . getPointer( int index ).x ;
float y = touchEvent . getPointer( int index ).y ;
tapNum :移動中の指の情報数
index : 何番目の情報かを示す指標(0以上 tapNum-1 まで)
x : 移動した指の横座標
y : 移動した指の縦座標
TouchEvent には様々なメソッドやプロパティがありますが、よく使うのは上記3つではないかと思います。
getNumPointers が1ならシングルタップ、2以上ならマルチタップされたという事です。
getNumPointers でタップされている指の数(渡されてくる Pointer 情報の数)を取得し、該当Pointerからタップされている座標を拾う形になります。
touchMoved イベントは指が動いた時に発生するイベントでしかないので、それがスワイプなのかフリックなのかは判別できません。
またタップしたつもりが、微妙に指が動いていてしまった場合も呼び出されてくるため、touchMoved で正確なジェスチャーを拾う事は困難です。
ただ指が動いたという事を検知したいだけなら十分ですが、詳細なジェスチャーを知りたい場合は、surfaceTouchEvent を利用しましょう。
surfaceTouchEventで移動を知る
イベントboolean surfaceTouchEvent( MotionEvent motionEvent ) ;
motionEvent : イベント内容
渡されてくる情報はTouchEvent ではなく、MotionEvent クラスのインスタンス変数です。
MotionEvent は、Android 標準のonTouchEventに渡されてくるものと同じものになります。
int pIndex = motionEvent . getActionIndex() ;
int eAction= motionEvent . getActionMasked() ;
float x = motionEvent . getX( int pIndex ) ;
float y = motionEvent . getY( int pIndex ) ;
pcount: Pointer 配列数(タップされている指の数)
pIndex : イベントが発生したPointer配列の位置(0 ~ pcount – 1 まで)
eAction: イベントの種類
x : タップされた横座標
y : タップされた縦座標
getPointerCount で渡される数は、タップされている指の数(Pointer情報の数)です。マルチタップが行われると、getPointerCount は2以上の数を戻します。
surfaceTouchEvent は指を動かした時だけでなく、タップしたり指を離した時にも呼び出されます。
ですのでどんな操作が行われたかを知るには、 getActionMasked 命令を使って自分で判定する必要があります。
指を動かした時に発生する Action は MotionEvent.ACTION_MOVE (2) になります。
float hx = motionEvent.getHistoricalX( int pos, int hpos );
float hy = motionEvent.getHistoricalY( int pos, int hpos );
histNum : 履歴の数
pos : イベントが発生したPointer配列の位置(0 ~ pcount – 1 まで)
hpos : 履歴の位置(0 ~ histNum – 1 まで)
hx : 履歴のタップされた横座標
hy : 履歴のタップされた縦座標
ACTION_MOVE イベントで理解しておいて欲しい事は、指が動かされた時に、surfaceTouchEvent がタイムリーに呼び出されるわけではないという事です。
指を素早く動かすと、各移動情報がOS内部に蓄積され、OSの都合が良いタイミングで surfaceTouchEvent が呼び出されます。
(画像URL:illust-AC 様:かみたまさん、casa4さん)
例えば上図では、 B と C の情報は指が移動した時にタイムリーには通知されず、Dの情報と一緒に通知されてきます。
この蓄積された情報数を知るのが getHistorySize() 命令です。また蓄積された情報から座標を取得するには、getHistoricalX()、getHistoricalY() 命令を利用します。
蓄積情報は添字の若い方(0)が最も古い(早く蓄積された)情報になります。また蓄積情報よりも Pointer情報の方が新しい情報になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int tapNum = motionEvent.getPointerCount(); int historySize = motionEvent.getHistorySize(); if( motionEvent.getActionMasked() == MotionEvent.ACTION_MOVE){ //全ての指の情報を取得する for( int pos = 0; pos < tapNum; pos++ ){ //まずは蓄積情報から取得する for( int hpos = 0; hpos < historySize; hpos++ ){ float hx = motionEvent.getHistoricalX( pos, hpos ); float hy = motionEvent.getHistoricalY( pos, hpos ); //なにかの処理 } //次に、Pointer情報を得る float px = motionEvent.getX( pos ); float py = motionEvent.getY( pos ); //なにかの処理 } } |
たとえば上記のような感じです。
surfaceTouchEventでスワイプを知る
さて前置きが長くなりましたが、実はここまでなら surfaceTouchEvent とtouchMoved は大差がありません。
つまり指が動いたことはわかっても、それがスワイプなのかフリックなのかを判別する事は困難です。
本関数の使い所は、GestureDetector クラスの onTouchEvent 関数を呼び出し、GestureDetector クラスに様々なジェスチャーを判定させて、OnGestureListener がもつ対応イベント関数を呼び出すことにあります。
GestureDetector クラスを利用すると、スワイプ以外にも様々なジェスチャーを検知できるようになります。
GestureDetectorを生成するGestureDetector ges = new GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler);
ges : GestureDetector クラスのインスタンス変数
listener : OnGestureListener インタフェースを実装したクラスのインスタンス
context : Application Context
handler : UIスレッドのLooperと連携したHandlerインスタンス
GestureDetector クラスを利用するには、まずインスタンスを作成する必要があります。
インスタンス作成時には、GestureDetector クラスのコンストラクタにOnGestureListener インタフェースを実装したリスナークラスのインスタンスを与える必要があります。
OnGestureListenerインスタンスを実装したクラスではなく、GestureDetector.SimpleOnGestureListener クラスを継承したクラスを用意して、そのクラスのインスタンスを与えてもOKです。
下記サンプルプログラムでは、標準エディタからコーディングする事と、いろいろなジェスチャーをキャッチする事を想定して、SimpleOnGestureListener クラスを継承した専用のクラスを用意しています。
で、少し話は難しくなるのですが・・・GestureDetector クラスをsetup()関数から作成しようとすると
java.lang.RuntimeException: Can’t create handler inside thread that has not called Looper.prepare()
例外が発生します。
これはGestureDetector クラスが、タップに関連するイベントを処理するための Handler を、内部的に生成するためです。
Handlerには、Handlerが処理した結果を受け取る「対」となる存在(Looper)が必要です。ところが、(特に何も指定しなければ)LooperはUIスレッドにしか存在しません。
setup() 関数は、バックグラウンドスレッドで動作しています。そしてバックグラウンドスレッドには、Looperは存在しないのです。
解決策は2通りあります。
- UIスレッドでGestureDetector クラスを作成する
- UIスレッドのLooperへリクエストを送信するHandlerを生成して、GestureDetector のコンストラクタに与える
ここでは2番めの方法を採用します。
以下のような感じです。
(画像URL:illust-AC 様:かみたまさん、casa4さん)
理屈は複雑ですが、コーディングはシンプルです(笑)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//ジェスチャー検知クラス //SimpleOnGestureListener クラスを継承した専用のクラスを用意する class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //スクロールを検知するイベント関数をオーバライドする return super.onScroll(e1, e2, distanceX, distanceY); } } Activity act; //Activity Context con; //Context public void setup() { //ActivityとContextを取得 act = getActivity(); con = act.getApplicationContext(); //UIスレッドのLooperと連携したHandlerを作成して、GestureDetectorに与える Handler mHandler = new Handler(Looper.getMainLooper()); gestureDetector = new GestureDetector(con, new MyGestureListener(), mHandler); } @Override public boolean surfaceTouchEvent(MotionEvent motionEvent) { //GestureDetectorにイベントを渡す gestureDetector.onTouchEvent(motionEvent); return super.surfaceTouchEvent(motionEvent); } |
ポイントになる処理だけを抜き出すと、こんな感じになります。
Looperや、Handler、UIスレッドについて興味がある方は、以下のサイト様などを参照してみてください。
- realm 様:Androidのバックグラウンドを使いこなす Thread, Looper, Handler
- Qiita 様:絶対UIスレッドで動作させたい処理の実装方法:@yamacraftさん
- moxt 様:AndroidのHandlerって何?
タップイベントを通知するboolean gestureDetector.onTouchEvent( MotionEvent motionEvent );
gestureDetector : GestureDetector インスタンス
motionEvent : surfaceTouchEventに渡されたイベント
スワイプを検知するには、作成したGestureDetector インスタンスに、発生したタップイベントを通知する必要があります。
GestureDetector インスタンスにタップイベントを通知するには、surfaceTouchEvent関数から、GestureDetectorクラスが持つ onTouchEvent を呼び出して下さい。
スワイプイベントboolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) ;
e1 : イベントが最初に発生した位置情報(起点)
e2 : 現在のイベント位置
distanceX : 横方向の移動距離
distanceY : 縦方向の移動距離
さてここまで準備ができたら、SimpleOnGestureListenerを継承したクラスで、スワイプを検知するために 、onScroll 関数をオーバライドします。
distanceX、distanceY に渡されてくる移動距離は、起点(e1)から現在のイベント位置(e2)までの距離ではありません。そうではなく、前回 onScroll 関数が呼び出された場所(前回のe2)から「どれだけ動いたか」を示す情報になります。
(画像URL:illust-AC 様:かみたまさん、casa4さん)
distanceX、distanceY は移動方向によって正負が変わります。
(画像URL:illust-AC 様:かみたまさん)
distanceX、distanceY の正負をみれば、指を滑らせた方向がわかります。またdistanceX、distanceY を使って三平方の定理を計算する事で、前回イベントからの移動距離が求まります。
【関連記事】
- タップを検知するには(AndroidMode編)
- ダブルタップを検知するには(AndroidMode編)
- 長押しを検知するには(AndroidMode編)
- フリック検知するには(AndroidMode編)
- ピンチを検知するには(AndroidMode編)
- ローテートを検知するには(AndroidMode編)
- 様々なタッチ操作を検知するには(Ke:tai.lib 編)
サンプルプログラム
タッチイベントを利用する例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/** * Android Mode スワイプ Sample * @author MSLABO * @version 1.0 */ final int FONT_SIZE = 16; //フォントサイズ PFont font; //フォント ArrayList<PVector> tapP; //座標 public void setup() { fullScreen(); //フォントを読み込む font = createFont("ipag.ttf", FONT_SIZE ); textSize( FONT_SIZE ); textFont( font ); textAlign( LEFT, TOP ); tapP = new ArrayList<PVector>(); } public void draw(){ background( 200 ); //スワイプ中の位置に青丸を描く for( PVector point: tapP ){ //青丸を描く fill( color( 100,100,255 ) ); ellipse( point.x, point.y, 50, 50 ); } } public void touchMoved(TouchEvent touchEvent) { int tapNum = touchEvent.getNumPointers(); //全ての指の座標を覚える for( int i = 0; i < tapNum; i++ ){ TouchEvent.Pointer p = touchEvent.getPointer( i ); tapP.add( new PVector( p.x, p.y )); } } public void touchEnded(TouchEvent touchEvent) { int tapNum = touchEvent.getNumPointers(); //全ての指が離れたら、座標情報を初期化する if( tapNum == 0 ){ tapP.clear(); } } |
touchMovedイベントで指が動いた座標を全て拾い、画面に青丸を描いています。
スワイプを検知する例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
import android.app.Activity; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; /** * Android Mode スワイプ Sample * @author MSLABO * @version 1.1 */ //移動方向 final int L2R = 0x01; //左から右 → final int R2L= 0x02; //右から左 ← final int T2B = 0x10; //上から下 ↓ final int B2T = 0x20; //下から上 ↑ final int L2RT = 0x21; //右斜め上 ↗ final int L2RB = 0x11; //右斜め下 ↘ final int R2LT = 0x22; //左斜め上 ↖ final int R2LB = 0x12; //左斜め下 ↙ final int FIRE_DISTANCE = 70; //イベント発火距離 Activity act; //Activity Context con; //Context GestureDetector gestureDetector; //ジェスチャーOBJ PImage Arrow; //画像 PVector nowPoint; //現在のスワイプ位置 PVector startPoint; //スワイプ起点 PVector distance; //移動量 int degree; //回転角度 //ジェスチャー検知クラス //SimpleOnGestureListener クラスを継承した専用のクラスを用意する class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //起点座標を記録 int index = e1.getActionIndex(); startPoint = new PVector( e1.getX(index), e1.getY(index)); //現在のスワイプ位置を記録 index = e2.getActionIndex(); nowPoint = new PVector( e2.getX(index), e2.getY(index)); //移動量を記録 distance = new PVector(distanceX, distanceY); return super.onScroll(e1, e2, distanceX, distanceY); } } public void setup() { fullScreen(); //タップ情報初期化 nowPoint = new PVector(0,0); startPoint = new PVector(0,0); distance = new PVector(0,0); //ActivityとContextを取得 act = getActivity(); con = act.getApplicationContext(); Arrow = loadImage( "arrow.png"); //UIスレッドのLooperと連携したHandlerを作成して //GestureDetectorに与える Handler mHandler = new Handler(Looper.getMainLooper()); gestureDetector = new GestureDetector(con, new MyGestureListener(), mHandler); //回転角度を初期化 degree = 0; } /** * 回転処理 * @param x 表示座標X * @param y 表示座標Y * @param img 画像 * @param deg 回転角度 */ void rotateImage( int x, int y, PImage img, float deg ){ pushMatrix(); //画像中央を回転の中心にする translate( x + img.width/2, y + img.height/2 ); //回転する rotate(radians( deg )); //回転の中心が画像中央なので、画像描画原点も画像中央にする //こうすると、(0,0)に配置すれば期待した位置に画像が置ける imageMode(CENTER); //画像を描画 image( img, 0, 0 ); //画像描画原点を元(画像の左上隅)に戻す imageMode(CORNER); popMatrix(); } public void draw() { background( 200 ); //画像表示位置を画面中央にする int dispX = (width - Arrow.width)/2; int dispY = (height - Arrow.height)/2; //移動方向を回転角度に変換する switch(getDirection()){ case L2R: degree = 90; //左から右 break; case R2L: degree = 270; //右から左 break; case T2B: degree = 180; //上から下 break; case B2T: degree = 0; //下から上 break; case L2RT: degree = 45; //右斜め上 break; case L2RB: degree = 135; //右斜め下 break; case R2LT: degree = 315; //左斜め上 break; case R2LB: degree = 225; //左斜め下 break; } //画像を回転する rotateImage(dispX, dispY, Arrow, degree ); } /** * 移動方向計算処理 * @return 移動方向 */ int getDirection(){ int dire = 0; //起点からの直線移動距離を求める float distanceLine = sqrt( sq(startPoint.x - nowPoint.x) + sq(startPoint.y - nowPoint.y) ); if( distanceLine < FIRE_DISTANCE ){ //イベント発火距離を満たしていなければ、なにもしない return( dire ); } //X方向を検知 if( abs(distance.x) > FIRE_DISTANCE ){ //X方向に一定以上動いた場合 if( distance.x < 0 ){ //左から右 dire = L2R; } else { //右から左 dire = R2L; } } //Y方向はX方向の結果に加算する事で、斜め移動を検知する if( abs(distance.y) > FIRE_DISTANCE ) { //Y方向に一定以上動いた場合 if( distance.y < 0){ //上から下 dire = dire + T2B; } else { //下から上 dire = dire + B2T; } } return( dire ); } @Override public boolean surfaceTouchEvent(MotionEvent motionEvent) { //GestureDetectorにイベントを渡す gestureDetector.onTouchEvent(motionEvent); return super.surfaceTouchEvent(motionEvent); } |
onScroll イベントに渡されるdistanceX、distanceY を利用して、上下左右斜めの8方向のうち、どちらにスワイプされたかを判定し、矢印画像を回転させています。
FIRE_DISTANCE定数を変更する事で、スワイプの感度を調整できます。
なお画像の回転方法については「画像を回転するには」記事を参照してください。
<出力サンプル>
(画像URL:illust-AC 様:anny さん)
本ページで利用しているアイコン画像は、下記サイト様より拝借しております。各画像の著作権は、それぞれのサイト様および作者にあります。