Unmotivated

やる気はない

GestureDetector でリスナに渡ってくる MotionEvent が Null になる

タッチジェスチャを検出する場合は、 GestureDetector に次のようなリスナを渡して判定するのが常套手段ですが、onScroll()onFling() の第一引数の e1 にはジェスチャの開始地点の MotionEvent が入ります。
ところが、時々 e1 に null が渡ってくることがあって困りました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private class MyGestureListener extends SimpleOnGestureListener {

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2,
            float distanceX, float distanceY) {
        // なぜか e1 が null !!
        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2,
            float velocityX, float velocityY) {
        // なぜか e1 が null !!
        return true;
    }

}

ググるといくつか stackoverflow が見つかったけど、どれも根本解決には至っていない様子。

というわけで、原因を調べてみました。

結論を先に言うと、 ACTION_DOWN のタッチイベント(MotionEvent)を GestureDetector#onTouchEvent() に渡せていないのが原因でした。

原因特定

GestureDetector のコードを見てみる。

GestureDetector.java(2.2_r1)
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
public boolean onTouchEvent(MotionEvent ev) {

    // ...

    if (mIsDoubleTapping) {
        // Give the move events of the double-tap
        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    } else if (mAlwaysInTapRegion) {
        final int deltaX = (int) (x - mCurrentDownEvent.getX());
        final int deltaY = (int) (y - mCurrentDownEvent.getY());
        int distance = (deltaX * deltaX) + (deltaY * deltaY);
        if (distance > mTouchSlopSquare) {
            // onScroll が呼ばれる
            handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
            mLastMotionX = x;
            mLastMotionY = y;
            mAlwaysInTapRegion = false;
            mHandler.removeMessages(TAP);
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
        }
        if (distance > mBiggerTouchSlopSquare) {
            mAlwaysInBiggerTapRegion = false;
        }
    } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
        // onScroll が呼ばれる
        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
        mLastMotionX = x;
        mLastMotionY = y;
    }

    // ...

    if (mIsDoubleTapping) {
        // Finally, give the up event of the double-tap
        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    } else if (mInLongPress) {
        mHandler.removeMessages(TAP);
        mInLongPress = false;
    } else if (mAlwaysInTapRegion) {
        handled = mListener.onSingleTapUp(ev);
    } else {

        // A fling must travel the minimum tap distance
        final VelocityTracker velocityTracker = mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
        final float velocityY = velocityTracker.getYVelocity();
        final float velocityX = velocityTracker.getXVelocity();

        if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                || (Math.abs(velocityX) > mMinimumFlingVelocity)){
            // onFling が呼ばれる
            handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
        }
    }

    // ...

}

どちらも mCurrentDownEvent がそのまま渡されるだけなので、 mCurrentDownEvent を確認する。

GestureDetector.java(2.2_r1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean onTouchEvent(MotionEvent ev) {

    // ...

    case MotionEvent.ACTION_DOWN:
        // ...
        if (mCurrentDownEvent != null) {
            mCurrentDownEvent.recycle();
        }
        mCurrentDownEvent = MotionEvent.obtain(ev);

    // ...

}

というわけで ACTION_DOWN のイベントを GestureDetector#onTouchEvent() に渡せていないのが原因でした。

ジェスチャを判定したい View において dispatchTouchEvent() や onInterceptTouchEvent() なんかでイベントの分配をしている状況だと、 View の onTouchEvent() に ACTION_DOWN の MotionEvent が渡らなかったりするので、そう言うときに発生していたみたい。

今回は別のところでタッチ時の情報を保存していたので、その情報を使って e1 の代わりとさせました。
そもそも GestureDetector が ACTION_DOWN 時に色々初期化しているので、これを渡せないと onScroll() とか onFling() は判定されたものの、そもそも全く判定されないジェスチャが盛りだくさんかと思われます。
ちゃんと全てのイベントを渡せるようにした方がよさそう。

ちなみに、どんな時でもジェスチャを判定させたいなら View#dispatchTouchEvent() で GestureDetector#onTouchEvent() を呼び出したりすれば、ややこしいことは一切抜きで子供の View に邪魔されずにジェスチャ判定できたりします。(その View にタッチイベントが回って来てさえいれば:参考)

余談

GestureDetector のジェスチャ判定のためのパラメータを変更できないのかな?と前々から疑問に思っていたのでついでに読んでみたけど、どうも定数がぶち込まれるようになっているのでやっぱりできないっぽいです。
onFling() で拾った後に velocity でさらに絞る…とかいつもやっているので、 GestureDetector 自体にパラメータを渡せた方が楽そうなんだけど…。

Comments