Unmotivated

やる気はない

Android のタッチイベントを理解する(その1)

タッチイベントがうまく流れてこなくて困ったり、自力でイベントをルーティングしたりするときに困ったりと、ちょこちょことタッチイベントについて勉強したのでまとめておきます。
主にタッチイベントがどう流れてどう止まるかなどについて調べています。

イベントの流れを理解するには以下の資料がかなり参考になりました。

毎度のことながら、間違いがありましたらご指摘頂ければ幸いです。

タッチイベントを処理する主要なメソッド

実際の流れを理解する前に、主要なメソッドを三つ紹介しておきます。

メソッド名 概要
onTouchEvent() (View) タッチイベントに対して何かを処理するメソッド。setOnTouchListener() で登録した listener はこのタイミングで呼び出される。
onInterceptTouchEvent() (ViewGroup) タッチイベントが子供へと伝搬することを阻止できるメソッド。
dispatchTouchEvent() (Activity, View, ViewGroup) タッチイベントを受け取り、子に伝搬させるかどうか、自分が処理するかどうかなどを管理する。onInterceptTouchEvent() や onTouchEvent() を呼ぶ人。

 

基本的なタッチイベントの流れ

タッチイベントは Activity を経由して PhoneWindow 直下の DecorView から伝搬が始まり、親の View からその子 View へと dispatchTouchEvent() なるメソッドを通じて伝搬していきます。

dispatchTouchEvent() は 自身が ViewGroup だった場合、まず自身の onInterceptTouchEvent() を呼び出します。 onInterceptTouchEvent() では子にイベントを渡すかどうかなどを判断します(後で詳しく説明します)。

次に、子の dispatchTouchEvent() を呼び出してイベントを渡していきます。 このとき、親が子の dispatchTouchEvent() を呼び出す順番は、一番新しく追加した子から古い子へと逆順に渡していきます。

そして、最後に一般的にタッチイベントを処理するメソッドである onTouchEvent() が dispatchTouchEvent() によって呼ばれます。 この onTouchEvent() は、子から親へと逆順(ユーザから見ると手前に表示されている順)に流れていくようになっています。

文章だけだと分かりづらいので、たとえば以下のような構成の View の場合を考えてみます。

Sample View

1
2
3
4
FrameLayout1(白)
|-- FrameLayout2(赤)
`-- FrameLayout3(緑)
    `- Button

この構成の場合、いずれの View もタッチイベントを処理しない、最もシンプルなイベントの流れは以下のようになります。

TouchEvent Flow

(1) から (13) の順番で処理が流れ、 onInterceptTouchEvent() が呼び出される順番は (I) から (III) となり、肝心の onTouchEvent() が呼び出される順番は (i) から (iv) となります。

画面と照らし合わせてみると、ユーザから見て手前側の View から onTouchEvent() が呼び出されていることを確認できるかと思います。

タッチイベントを処理した場合の流れ

タッチイベントは以上の流れで伝搬していきますが、いずれかのメソッドで true が返された場合、そこで連鎖がとまり、以降の処理は実行されなくなります。

例として、以下のように Button で onTouchEvent() を処理して true を返してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TouchTestActivity implements OnTouchListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        findViewById(R.id.button).setOnTouchListener(this);
    }

    @Override
    protected void onTouch(View v, MotionEvent event) {
        int id = v.getId();
        switch(id) {
        case R.id.button:
            Log.d(TAG, "Touched!");
            return true;
        }
        return false;
    }

}

この場合、 Button の onTouchEvent() でイベントの伝搬が停止するため、 FrameLayout3 の onTouchEvent() はコールされず、 FrameLayout2 は onDispatchTouchEvent() すら呼ばれません。 つまり、ユーザから見て「触ってイベントが発生した View より後ろにある View」には onTouchEvent() が一切発生しないことになります。

TouchEvent Flow

OnTouchListener を登録したり onTouchEvent() を直接処理したりする以外にも、良くある例だと View#setOnClickListener() で OnClickListener を登録した場合なども、 onTouchEvent() で true が返るようになり、それ以降の View の onTouchEvent() はキャンセルされることになります。

子の OnClickListener などに影響されずにタッチイベントを処理したい場合は、子を呼びだすより前に呼ばれる onInterceptTouchEvent() で必要な処理を行うという方法があります。 例えば ScrollView は子に影響されずにスクロール可能でなければならないため、 onInterceptTouchEvent() でスクロールの開始フラグを立てたりしています。

onInterceptTouchEvent() で伝搬を止めた場合

ViewGroup に実装されている onInterceptTouchEvent() で true を返すと、子供の View にイベントを伝搬しないようになります。

たとえば、以下のように onInterceptTouchEvent() で true を返すようにした CustomFrameLayout で FrameLayout3 を置き換えてみます。

1
2
3
4
5
6
7
8
public class CustomFrameLayout extends FrameLayout {
    // ...
    @Override
    public boolean onInterceptTouchEvent() {
        return true;
    }
    // ...
}

この場合、 FrameLayout3 が子供(Button)にイベントを伝搬しなくなるため、以下のようなイベントの発生順序になります。

TouchEvent Flow

また ScrollView を例にあげると、スクロール中は子供のタッチイベントを発生させないようにする必要があるため、 onInterceptTouchEvent() が活用されている様子を見る事ができます。(イベントのアクションが MotionEvent.ACTION_MOVE かつドラッグ中フラグが立っていたら子へ伝搬させないなど)

また、この onInterceptTouchEvent() を使って親側から子へのイベントの伝搬が止められることがある場合、子供側から止めないようにお願いする事ができます。 イベントの伝搬を親側から止めないようにする場合は、親の requestDisallowInterceptTouchEvent() を呼びます。

1
2
3
4
5
6
7
8
9
public class TouchTestActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        findViewById(R.id.button).getParent().requestDisqllowInterceptTouchEvent(true);
    }

}

TouchEvent Flow

requestDisallowInterceptTouchEvent(true) で onInterceptTouchEvent() を抑制した場合、親の onInterceptTouchEvent() が呼ばれないことになるので、子にイベントを流すようにするだけでなく、副産物的に onInterceptTouchEvent() で行っている処理を無視させることができます。

たとえば、 requestDisallowInterceptTouchEvent() と加えて、子の onTouchEvent() で true を返すなどしてイベントの伝搬を子で止めてしまえば ScrollView のスクロールを無効化できたりします。 このあたりを組み合わせると、入れ子状の ScrollView を実現できたりします。

次回予告?

自力でイベントをルーティングする場合の話や、 Action の話なども書こうと思いましたが、思ったより長くなってしまったので、気が向いたらまた次回書きます。

Comments