JUnitについての書籍が遂に発売します!
Java開発時のユニットテストを加速する「JUnit速効レシピ」
Java開発時のユニットテストを加速する「JUnit速効レシピ」
フリックされた方向を判定する為の汎用的なクラスを作成致しましたので、
是非とも活用して下さい。
そもそも、Androidアプリではタッチイベントがその時の状態に応じた値として
イベントが起動されるだけですので、どのイベントの時に何の処理をする。
どのイベントだとどうだとかいった判定は自分でしないといけません。
特に、フリック処理では、ただのタッチとは違ってドラッグした距離や
ドラッグされた方向を考慮して、フリックされたのか?
どの方向へフリックされたのか?を自分で判定ロジックを実装しなければいけません。
スマホではフリック操作を良く使用しますので、
一度フリックの方向検知だけでも汎用的なクラスとして
様々なシーンで使いまわせるように、一旦纏めたものを
今回の記事では公開し、あわせて解説を行いたいと思います。
- タッチイベントの通知先クラスをviewへ設定しよう
- フリックされた方向を判定する独自クラスの実装
- MotionEventから取得出来る判断基準あれこれ
- 実際にフリック方向判定処理をActivityへ組み込んでみよう
タッチイベントの通知先クラスをviewへ設定しよう
まずはタッチを検知する為のviewを作成し、
タッチのイベント通知先としてオリジナルのクラスのインスタンスを指定します。
// タッチジェスチャー検出用のイベントを付加したviewを設置する View touch_view = new View( this ); touch_view.setOnTouchListener( new FlickTouchListener() ); layout.addView( touch_view, layout_params );
FlickTouchListenerというのが、今回作成するフリック方向判定クラスです。
今回は、引数として渡す時にnewしていますが、
例えばフリック方向の判定以外にも、独自メソッドを追加したりして
何かの受け渡しをしたい時は、事前にnewしてインスタンス化した後に
セッタ等、様々な事をしてから最終的にviewへsetOnTouchListener()するといった
使い方でも問題ありません。
タッチイベントを専用のクラスへ通知するには、
タッチを受け付けるviewに対して
setOnTouchListener()で専用のクラスのインスタンスを渡してあげるという事だけ
シンプルに把握しておいてください。
フリックされた方向を判定する独自クラスの実装
それでは早速FlickTouchListenerの実装を見てみましょう。
//-------------------------------------------------------------------------- // フリックされた方向を算出する //-------------------------------------------------------------------------- private class FlickTouchListener implements View.OnTouchListener { // 最後にタッチされた座標 private float startTouchX; private float startTouchY; // 現在タッチ中の座標 private float nowTouchedX; private float nowTouchedY; // フリックの遊び部分(最低限移動しないといけない距離) private float adjust = 120; @Override public boolean onTouch( View v_, MotionEvent event_ ) { // タッチされている指の本数 Log.v( "motionEvent", "--touch_count = "+event_.getPointerCount() ); // タッチされている座標 Log.v( "Y", ""+event_.getY() ); Log.v( "X", ""+event_.getX() ); switch( event_.getAction() ){ // タッチ case MotionEvent.ACTION_DOWN: Log.v( "motionEvent", "--ACTION_DOWN" ); startTouchX = event_.getX(); startTouchY = event_.getY(); break; // タッチ中に追加でタッチした場合 case MotionEvent.ACTION_POINTER_DOWN: Log.v( "motionEvent", "--ACTION_POINTER_DOWN" ); break; // スライド case MotionEvent.ACTION_MOVE: Log.v( "motionEvent", "--ACTION_MOVE" ); break; // タッチが離れた case MotionEvent.ACTION_UP: Log.v( "motionEvent", "--ACTION_UP" ); nowTouchedX = event_.getX(); nowTouchedY = event_.getY(); if( startTouchY > nowTouchedY ){ if( startTouchX > nowTouchedX ){ if( ( startTouchY - nowTouchedY ) > ( startTouchX - nowTouchedX ) ){ if( startTouchY > nowTouchedY + adjust ){ Log.v( "Flick", "--上" ); // 上フリック時の処理を記述する } } else if( ( startTouchY - nowTouchedY ) < ( startTouchX - nowTouchedX ) ){ if( startTouchX > nowTouchedX + adjust ){ Log.v( "Flick", "--左" ); // 左フリック時の処理を記述する } } } else if( startTouchX < nowTouchedX ){ if( ( startTouchY - nowTouchedY ) > ( nowTouchedX - startTouchX ) ){ if( startTouchY > nowTouchedY + adjust ){ Log.v( "Flick", "--上" ); // 上フリック時の処理を記述する } } else if( ( startTouchY - nowTouchedY ) < ( nowTouchedX - startTouchX ) ){ if( startTouchX < nowTouchedX + adjust ){ Log.v( "Flick", "--右" ); // 右フリック時の処理を記述する } } } } else if( startTouchY < nowTouchedY ){ if( startTouchX > nowTouchedX ){ if( ( nowTouchedY - startTouchY ) > ( startTouchX - nowTouchedX ) ){ if( startTouchY < nowTouchedY + adjust ){ Log.v( "Flick", "--下" ); // 下フリック時の処理を記述する } } else if( ( nowTouchedY - startTouchY ) < ( startTouchX - nowTouchedX ) ){ if( startTouchX > nowTouchedX + adjust ){ Log.v( "Flick", "--左" ); // 左フリック時の処理を記述する } } } else if( startTouchX < nowTouchedX ){ if( ( nowTouchedY - startTouchY ) > ( nowTouchedX - startTouchX ) ){ if( startTouchY < nowTouchedY + adjust ){ Log.v( "Flick", "--下" ); // 下フリック時の処理を記述する } } else if( ( nowTouchedY - startTouchY ) < ( nowTouchedX - startTouchX ) ){ if( startTouchX < nowTouchedX + adjust ){ Log.v( "Flick", "--右" ); // 右フリック時の処理を記述する } } } } break; // アップ後にほかの指がタッチ中の場合 case MotionEvent.ACTION_POINTER_UP: Log.v( "motionEvent", "--ACTION_POINTER_UP" ); break; // UP+DOWNの同時発生(タッチのキャンセル) case MotionEvent.ACTION_CANCEL: Log.v( "motionEvent", "--ACTION_CANCEL" ); // ターゲットとするUIの範囲外を押下 case MotionEvent.ACTION_OUTSIDE: Log.v( "motionEvent", "--ACTION_OUTSIDE" ); break; } return( true ); } }
まず、第一にクラスのメンバとしてタッチが開始された時の座標をXとYでそれぞれ保持し、
タッチが離れた時の座標と比較出来る様になっています。
現在の実装では、タッチが離れた時の座標をメンバのnowTouchedX,nowTouchedYへ保持させていますが、
これはメンバ変数ではなく、ローカル変数でも問題ありません。
ドラッグ中の軌跡に対して何かしたい時とか、
スライド中の座標を追いかけたい等の追加要望が来た時に簡単に拡張出来る様に
念の為にメンバ変数としていますが、不要でしたらローカル変数へ変更してお使い下さい。
前述のsetOnTouchListener()でタッチの通知先を当クラスに指定しましたが、
タッチが行われると該当クラスのonTouch()メソッドが自動的に呼ばれます。
(必ず、View.OnTouchListenerを当クラスにimplementsしておく必要があります)
イベントで渡されてくる引数には、
第一引数に対象となるviewと、第二引数にタッチ状態を保持したMotionEventがあります。
今回のサンプルでは、MotionEventのみ使用しますが、
フリックされた対象のviewを判別したい時には、第一引数で渡されてくる
viewにて判定を行ってください。
MotionEventから取得出来る判断基準あれこれ
さて、MotionEventには様々な状態取得メソッドが用意されています。
今回の様に、Androidアプリにてタッチ操作に関する状態取得メソッドの種類と機能を
下記に纏めましたので、参照してください。
■タッチ関係の状態取得メソッド一覧
getAction() | タッチ操作によるアクション種別を取得 |
---|---|
getPointerCount() | タッチされている指の本数 |
getActionIndex() | アクションのインデックスを取得 |
getPointerId() | ポインタIDを取得 |
getX() | タッチイベントが発生しているX座標を取得 |
getY() | タッチイベントが発生しているY座標を取得 |
getDownTime() | タッチされていた時間(ms)を取得 |
getEventTime() | イベントの継続時間(ms)を取得 |
getEdgeFlags() | スクリーンの端かどうかの判定 |
getSize() | タッチされている範囲、サイズを取得 |
getPressure() | タッチの圧力・強さを取得 |
まず、今回のフリック操作による方向を判定するのに、
現在のタッチ状態が、どんな状態の種別にあるのかを取得する必要がありますので、
getAction()メソッドにて現在のアクション種別を取得し、switchにて処理を切り分けています。
フリック操作というものを、日本語で動作を分割すると
『タッチされ、タッチされた状態で移動し、タッチが離される』
という流れになるかと思います。
これを、getAction()で取得出来るアクション種別に置き換えると
『ACTION_DOWN⇒ACTION_MOVE⇒ACTION_UP』
となります。
このアクション種別は、MotionEvent内に定数として用意されていますので、
定数名で比較するのが一般的でしょう。
タッチにて使用する可能性のあるアクションの定数については、下記を参照してください。
■getAction()メソッドで返却されるタッチ関係の定数一覧
ACTION_DOWN | タッチされた |
---|---|
ACTION_POINTER_DOWN | タッチ中に追加でタッチされた |
ACTION_MOVE | タッチされた状態で移動 |
ACTION_UP | タッチが離れた |
ACTION_POINTER_UP | アップ後にほかの指がタッチ中の場合 |
ACTION_CANCEL | UP+DOWNの同時発生(タッチのキャンセル) |
ACTION_OUTSIDE | ターゲットとするUIの範囲外を押下 |
タッチイベントを登録したviewへタッチ操作があった場合は
MotionEventと共に指定クラスへ通知され、
getAction()メソッドでイベントのアクションを判別し、
タッチが離れた時に、それがフリックだったのか?フリックされた方向は?
というものを判定しています。
判定処理はACTION_UPイベント発生時に行い、
ACTION_DOWN時に保持した座標と比較し、
フリックされた方向を判別します。
その動作がフリックなのか?を、ただのタッチと区別する為に
移動距離に「遊び」を設けています。
メンバで初期化しているadjustの値が、その遊びの値ですので、
動作確認をしながら適度に調整して頂ければ幸いです。
後は、LogCatへ出力している通り、
フリック方向が取得できますので、該当する方向のフリック動作にて
起動したい処理をLogCatの下辺りに記述してください。
別にメソッドとして定義し、外だしする方が綺麗かもしれません。
今回のサンプルは、あくまでフリック操作の方向を取得する所までですので、
方向を判定した後はお好きな処理を実装して下さい。
実際にフリック方向判定処理をActivityへ組み込んでみよう
ここまでの流れを、実際にActivityへ実装すると、下記の様になります。
package com.example.androidhelloworld; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; public class Test1Activity extends Activity { // レイアウトの定数を省略してメンバに保持 private final int MP = ViewGroup.LayoutParams.MATCH_PARENT; //-------------------------------------------------------------------------- // フリックされた方向を算出する //-------------------------------------------------------------------------- private class FlickTouchListener implements View.OnTouchListener { // 最後にタッチされた座標 private float startTouchX; private float startTouchY; // 現在タッチ中の座標 private float nowTouchedX; private float nowTouchedY; // フリックの遊び部分(最低限移動しないといけない距離) private float adjust = 120; @Override public boolean onTouch( View v_, MotionEvent event_ ) { // タッチされている指の本数 Log.v( "motionEvent", "--touch_count = "+event_.getPointerCount() ); // タッチされている座標 Log.v( "Y", ""+event_.getY() ); Log.v( "X", ""+event_.getX() ); switch( event_.getAction() ){ // タッチ case MotionEvent.ACTION_DOWN: Log.v( "motionEvent", "--ACTION_DOWN" ); startTouchX = event_.getX(); startTouchY = event_.getY(); break; // タッチ中に追加でタッチした場合 case MotionEvent.ACTION_POINTER_DOWN: Log.v( "motionEvent", "--ACTION_POINTER_DOWN" ); break; // スライド case MotionEvent.ACTION_MOVE: Log.v( "motionEvent", "--ACTION_MOVE" ); break; // タッチが離れた case MotionEvent.ACTION_UP: Log.v( "motionEvent", "--ACTION_UP" ); nowTouchedX = event_.getX(); nowTouchedY = event_.getY(); if( startTouchY > nowTouchedY ){ if( startTouchX > nowTouchedX ){ if( ( startTouchY - nowTouchedY ) > ( startTouchX - nowTouchedX ) ){ if( startTouchY > nowTouchedY + adjust ){ Log.v( "Flick", "--上" ); // 上フリック時の処理を記述する } } else if( ( startTouchY - nowTouchedY ) < ( startTouchX - nowTouchedX ) ){ if( startTouchX > nowTouchedX + adjust ){ Log.v( "Flick", "--左" ); // 左フリック時の処理を記述する } } } else if( startTouchX < nowTouchedX ){ if( ( startTouchY - nowTouchedY ) > ( nowTouchedX - startTouchX ) ){ if( startTouchY > nowTouchedY + adjust ){ Log.v( "Flick", "--上" ); // 上フリック時の処理を記述する } } else if( ( startTouchY - nowTouchedY ) < ( nowTouchedX - startTouchX ) ){ if( startTouchX < nowTouchedX + adjust ){ Log.v( "Flick", "--右" ); // 右フリック時の処理を記述する } } } } else if( startTouchY < nowTouchedY ){ if( startTouchX > nowTouchedX ){ if( ( nowTouchedY - startTouchY ) > ( startTouchX - nowTouchedX ) ){ if( startTouchY < nowTouchedY + adjust ){ Log.v( "Flick", "--下" ); // 下フリック時の処理を記述する } } else if( ( nowTouchedY - startTouchY ) < ( startTouchX - nowTouchedX ) ){ if( startTouchX > nowTouchedX + adjust ){ Log.v( "Flick", "--左" ); // 左フリック時の処理を記述する } } } else if( startTouchX < nowTouchedX ){ if( ( nowTouchedY - startTouchY ) > ( nowTouchedX - startTouchX ) ){ if( startTouchY < nowTouchedY + adjust ){ Log.v( "Flick", "--下" ); // 下フリック時の処理を記述する } } else if( ( nowTouchedY - startTouchY ) < ( nowTouchedX - startTouchX ) ){ if( startTouchX < nowTouchedX + adjust ){ Log.v( "Flick", "--右" ); // 右フリック時の処理を記述する } } } } break; // アップ後にほかの指がタッチ中の場合 case MotionEvent.ACTION_POINTER_UP: Log.v( "motionEvent", "--ACTION_POINTER_UP" ); break; // UP+DOWNの同時発生(タッチのキャンセル) case MotionEvent.ACTION_CANCEL: Log.v( "motionEvent", "--ACTION_CANCEL" ); // ターゲットとするUIの範囲外を押下 case MotionEvent.ACTION_OUTSIDE: Log.v( "motionEvent", "--ACTION_OUTSIDE" ); break; } return( true ); } } //-------------------------------------------------------------------------- // ライフサイクル 1 : アクティビティ初期化(iOSの、viewDidLoad) //-------------------------------------------------------------------------- public void onCreate( Bundle saved_instance_state_ ) { super.onCreate( saved_instance_state_ ); // レイアウトを作成する LinearLayout linear_layout = new LinearLayout( this ); setContentView( linear_layout ); // タッチジェスチャー検出用のイベントを付加したviewを設置する View touch_view = new View( this ); touch_view.setOnTouchListener( new FlickTouchListener() ); linear_layout.addView( touch_view, new LinearLayout.LayoutParams( MP, MP ) ); } //-------------------------------------------------------------------------- // ライフサイクル 2 : アクティビティ開始(iOSの、viewWillAppear) //-------------------------------------------------------------------------- public void onStart() { super.onStart(); } //-------------------------------------------------------------------------- // ライフサイクル 3 : 破棄 //-------------------------------------------------------------------------- public void onDestroy() { // ...code... super.onDestroy(); } }
今後は、さらに判定出来る操作を増やしたり(マルチタップジェスチャーに対応したり)
コールバックの設定で、指定動作時に起動するメソッドを指定したりと、
どんどん汎用的に使えるように、タッチ関係のライブラリクラスとして
改造を続ける予定です。
今回はフリック操作の方向を判定する事しか出来ませんが、
機能を追加してクラスを強化致しましたら、随時ブログにて報告、公開をさせて頂ければと思います。
もし、このクラスについて、
『もっとこうした方が良い』
『そこが間違っている』
『こういう機能を追加してほしい』
等が御座いましたら、どしどしコメントを書き込んで頂ければ幸いです。
以上を持ちまして、Androidアプリでのフリック操作判定方法についての解説を終わります。
ありがとうございました。
JUnitについての書籍が遂に発売します!
Java開発時のユニットテストを加速する「JUnit速効レシピ」
Java開発時のユニットテストを加速する「JUnit速効レシピ」
私は研究でゲーム開発をしているのですが、プログラミング初心者であるため、非常に参考になりました。
返信削除おそらく小さな勘違いだと思うのですが、
75行目 if( startTouchX + adjust < nowTouchedX ){
85行目 if( startTouchY + adjust < nowTouchedY ){
99行目 if( startTouchY + adjust < nowTouchedY ){
105行目 if( startTouchX + adjust < nowTouchedX ){
(※行数はいずれも、Activityへの実装前(本記事の2つめのコード)のもの)
としないと、adjustが効かないと思います。
これだけ場合分けが多いと、頭がこんがらかってきますね……(^^;
あとは、
・「右斜め上」のように、縦・横以外の方向検知の方法
・ここではフリックの速度が考慮されていないため、ゆっくり画面を指でなぞってもフリックとみなされてしまう
辺りのお話にも触れて頂けるとありがたいです。