上下のフリックモーションに対応する(ScrollView上のY軸モーションが無効になる)


拙作のカレンダービュー(CalendarView)は元々左右どちらかのフリック・モーションに反応して現在の月を変更する機能を実装している。

当初は左右のモーションだけで処理をしていたが、人間の指というものは横にしろ縦にしろ機械のように真っ直ぐに動かすものではなく必ずある程度のぶれが発生するため、右に指を弾いているはずが実際には斜め上に弾いている場合があったりと、多分に感覚的なものである。であれば左右の他に上下の動きも捕捉してやることで、より感覚的に操作できてミスも防ぐことができるのではないだろうか。


左右が実装できているのであれば上下は簡単だろうと早速改造に着手したのだが、テストした所CalendarView単体では問題ないものの、通常配置するであろう、ScrollView下に配置した場合に、Y軸のモーションイベント(MotionEvent)に全く反応しないことが判明した。

いろいろ調べて判ったのだがこの現象、ScrollView上に配置したViewでは避けられない問題のようだ。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    :
    :
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL); //ACTION_CANCELを送信
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }

    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                break;
            }
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            final float y = ev.getY(pointerIndex);
            final int yDiff = (int) Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop) {
                <span style="color:#FF0000;">mIsBeingDragged = true;</span>
                mLastMotionY = y;
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
        :
        :
    }
    return mIsBeingDragged;
}

ScrollView(ViewGroup)#onInterceptTouchEventは自身の子供のビューのタッチイベントを監視しており、ScrollViewクラスの実装では子供のViewのモーション中、Y軸側に一定量移動した条件で"mIsBeingDragged"を真に設定する、つまり上下へのモーションをスクロール開始の契機としているため、その子供のViewに対しては"ACTION_CANCEL"でMotionEventが送信されて、結果Y軸側のモーションをCalendarViewで捕捉できないのである。

ScrollViewが無ければいい訳だが、CalendarViewは比較的大きな画面を覆うタイプのビューであるため、機器によっては画面をランドスケープモードにした場合にカレンダの下端が切れてしまうため、ScrollViewを使わないと5週目が表示できないのだ。

例) ランドスケープにして下端が切れているCalendarView

この状態を避ける方法も無いことはない。ViewGroupを代表とするViewParentの実装クラスは子供のビューのタッチイベントを一切監視しないように指示することもできる。

public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)

requestDisallowInterceptTouchEventメソッドのパラメタにtrue(真)をセットすることにより、内部変数であるdisallowInterceptが真になる

public boolean dispatchTouchEvent(MotionEvent ev) {
    :
    :
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {

この操作により、子供のViewにイベントを送信するか否かをonInterceptTouchEventの結果に依らないようにできる訳だ。

しかし、実際に試してみるとこの方法には問題があることが判るだろう。ScrollView上に配置したViewは確かにY軸方向のモーションイベントを捕捉するようになるが、せっかくScrollView上に配置したにも関わらず、今度はスクロール操作に反応しなくなるのである。(当たり前)

ということで、一番の解決策はスクロールが必要な場合はモーションを無視し、必要無い時にはモーションを有効にすることだろうが、それを実現するためには、やはりサブクラスを書く必要があるだろう。

長くなったので続きは明日にでも。