Androidのイベントリスナ

onTouchとonTouchEventの使い分けがモヤモヤしていたのでまとめてみました。

 

1. イベントリスナ

イベントリスナの一覧はこちら

例えばonClickはクリック時(正確には指が離れる瞬間)に、onLongClickはロ ングクリック時(指が触れてから一定時間経過後)に動作するイベントハンド ラのインターフェースを提供します。

onTouchは全てのタッチイベントを補足するイベントリスナです。onClickと onLongClickはタッチイベントに関連するイベントリスナで、最初にまず onTouchが評価された後、onClickとonLongClickが呼ばれます。

以下のコードではLinearLayout上にButtonを配置し、ButtonにonClickと onLongClick、さらにonTouchを登録しています。

public class MainActivity extends Activity {
  
  public static final String Tag = "EventListener";
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    Button button = new Button(this);
    
    button.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        Log.d(Tag, "onClick");
      }
    });
    
    button.setOnLongClickListener(new OnLongClickListener() {
      @Override
      public boolean onLongClick(View v) {
        Log.d(Tag, "onLongClick");
        return false;
      }
    });
    
    button.setOnTouchListener(new OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        Log.d(Tag, "onTouch");
        return false;
      }
    });
    
    LinearLayout layout = new LinearLayout(this);
    layout.addView(button);
    setContentView(layout);
  }
}

logcatのログは以下の通りになります。

05-04 04:00:37.333: D/EventListener(2109): onTouch     # 画面タッチ
05-04 04:00:37.843: D/EventListener(2109): onLongClick # 一定時間経過
05-04 04:00:38.933: D/EventListener(2109): onTouch     # 画面タッチをやめた
05-04 04:00:38.933: D/EventListener(2109): onClick

画面タッチした際、最初にonTouchが呼ばれ、一定時間経過後にonLongClickが 呼ばれることが確認できます。 画面タッチをやめた際、最初にonTouchが呼ばれ、その直後にonClickが呼ばれ ることが確認できます。

2. 戻り値がboolean型のイベントリスナ

イベントリスナには大まかに分けて2種類あります。 一つは戻り値がvoid型のイベントリスナで、もう一つは戻り値がboolean型の イベントリスナです。

特にboolean型のイベントリスナは、それ以降で実行される予定のイベントリ スナを実行するかしないかを決定します。

先ほどのコードを変更し、onTouchでtrueを常に返すようにします。

--- MainActivity.java.org       2014-05-04 17:43:22.000000000 +0900
+++ MainActivity.java   2014-05-04 17:42:46.000000000 +0900
@@ -40,7 +40,7 @@ public class MainActivity extends Activi
       @Override
       public boolean onTouch(View v, MotionEvent event) {
         Log.d(Tag, "onTouch");
-        return false;
+        return true;
       }
     });

logcatのログは以下の通りになります。

05-04 04:43:52.572: D/EventListener(2172): onTouch # 画面タッチ、一定時間そのまま待つ
05-04 04:43:53.042: D/EventListener(2172): onTouch # 画面タッチをやめた

先ほどとは異なり、onTouch以降に呼ばれていたonLongClickとonClickは呼ば れません。
これはonTouchでtrueを戻り値として返している為です。

さらに、falseを返していた場合と異なり、Buttonの色は変化しません。

Android DevelopersのInput Eventsのページには以下の記載がされています。 

  * onTouch() - This returns a boolean to indicate whether your listener
    consumes this event. The important thing is that this event can have
    multiple actions that follow each other. So, if you return false when the
    down action event is received, you indicate that you have not consumed the
    event and are also not interested in subsequent actions from this event.
    Thus, you will not be called for any other actions within the event, such
    as a finger gesture, or the eventual up action event.

onTouchでイベントを処理する場合はtrueを返し、その他のイベントリスナに 任せる場合はfalseを返せば良いことになります。

3. onTouchの使いどころ

ここでonTouchでonClickの動作をトレースしてみます。 onTouchの引数に渡されるMotionEventでは、getAction()メソッドでタッチイ ベントの種別を、getX()メソッドとgetY()メソッドでタッチした場所の座標を 取得できます。

先ほどのonTouchを以下の用に書き換えます。getAction()で取得したイベント がACTION_UPの場合にonClickを擬似的に動作させるようにします。

    button.setOnTouchListener(new OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        Log.d(Tag, "onTouch");
        switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
          Log.d(Tag, "Pseudo onClick");
          Log.d(Tag, "x = " + event.getX() + ", y = " + event.getY());
          /** Do something. */
          return true;
        default:
          return false;
        }
      }
    });

logcatのログは以下の通りになります。

05-04 06:10:07.010: D/EventListener(2698): onTouch     # 画面タッチ
05-04 06:10:07.530: D/EventListener(2698): onLongClick # 一定時間経過
05-04 06:10:08.300: D/EventListener(2698): onTouch     # 画面タッチやめた
05-04 06:10:08.300: D/EventListener(2698): Pseudo onClick
05-04 06:10:08.300: D/EventListener(2698): x = 60.0, y = 54.0

なんだ簡単じゃないか!

とは残念ながらいきません。 このコードでは、ボタンを押した際の色が変わる動作がそのままとなってしま います。
MotionEvent.ACTION_DOWNで実行された処理と逆の処理を実行する必要があります。例えば、v.setPressed(false)の呼び出し等。

 

以下はButtonをタッチする前の状態です。

Buttonをタッチする前

以下はButtonをタッチし終わった後の状態です。Buttonの色が青いままです。

Buttonをタッチし終わった後

 

ではMotionEvent.ACTION_DOWNの処理はどこで実行されているのでしょう。 onTouchが呼ばれる前と後のブレークポイントを使いながら追っていけば見つかるかもしれません。
しかし、問題なのはViewクラスの内部実装を深く知る必要があることです。 なによりViewクラスの内部実装はAndroidのバージョン変更で変わる可能性が高いことが問題です。

せっかくイベントリスナというインターフェースがあるのに、インターフェー スの中まで知らなくてはいけない。これはインターフェースを提供する側にとっても使う側にとってもメリットがありません。 インターフェースを使う目的は内部と外部を明確に区切ることなので。

よって、結論として、onTouchでtrueを返す場合はfalseを返すケースがないよ うにした方が無難でしょう。

よってonClickを擬似的に処理するコードは以下のようになるかと思います。 この際、onTouch以外のイベントリスナは処理しません。onTouch以外のイベントリスナを処理した場合はfalseを常に返すようにします。

    button.setOnTouchListener(new OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        Log.d(Tag, "onTouch");
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
          v.setPressed(true);
          break;
        case MotionEvent.ACTION_UP:
          Log.d(Tag, "Pseudo onClick");
          Log.d(Tag, "x = " + event.getX() + ", y = " + event.getY());
          v.setPressed(false);
          /** Do something. */
          break;
        default:
          break;
        }
        return true;
      }
    });

logcatのログは以下の通りになります。trueを返すのでonLongClickListener 等は登録していても呼ばれません。

05-04 06:57:56.163: D/EventListener(2892): onTouch     # 画面タッチ
05-04 06:57:57.013: D/EventListener(2892): onTouch     # 画面タッチやめた
05-04 06:57:57.013: D/EventListener(2892): Pseudo onClick
05-04 06:57:57.013: D/EventListener(2892): x = 96.0, y = 52.0

4. onTouchEventとonTouchの違い

onTouchと似たものでonTouchEventというものがあります。 こちらはViewクラスで定義されたメソッドで、Viewを継承したクラスでオーバ ライドすることができます。

public boolean onTouchEvent(MotionEvent event) {
<snip>
}

ややこしいのですが、Activityクラスでも同様のメソッドを定義しています。 これはActivityでもタッチイベントを補足できるようにするものです。 Button等のViewクラスが存在しない箇所をタッチした際はActivityクラスの onTouchEventが呼ばれます。
画面をスワイプする等の操作はこちらで実装することになります。

4.1. 呼ばれる順序の違い

話は戻ってViewクラスのonTouchEventについてですが、こちらはView上のタッ チイベントを補足するものです。 onTouchイベントリスナ、onTouchEventメソッド、onTouch以外のイベントハ ンドラは以下の順序で呼ばれます。 尚、各箇所でfalseを返していることが前提です

(1) onTouchイベントリスナが呼ばれる (2) onTouchEventメソッドが呼ばれる (3) onTouch以外のイベントリスナが呼ばれる

先にonTouchが呼ばれることが重要です。Viewクラスを継承したクラスにて onTouchEventの処理をオーバライドしていても、onTouchのイベントリスナを登録してtrueを返すようにすると、onTouchEventは呼ばれません。

4.2. 引数の違い

onTouchにはViewクラスが引数として渡される一方、onTouchEventは渡されま せん。これはonTouchEventが各クラスと一対一の関係にあるからです(各クラ ス単位に定義されているので当たり前ですが)。

onTouchはViewクラスと一対多の関係を持つことが可能です。Viewクラスのイ ンスタンス毎に挙動を変えることができます。見栄えの違いがある複数種類の Buttonクラスがあった場合にイベントリスナを共通化することも可能でしょ う。

4.3. どちらを使うべきか

ケースバイケースでしょう。 Viewクラスあるいはその派生クラスを拡張した場合はonTouchEventを拡張すれ ば良いと思います。
SurfaceView等で実装したViewは使う側でタッチイベントを変更することが稀 だと思います。
ただし、Buttonの見栄えを変えるだけという拡張の場合はonTouchEventを拡張 する必要はないと思います。

そもそもViewクラスあるいはその派生クラスを拡張しない場合はonTouch等の イベントリスナを用いることになります。