TouchEventをGestureDetectorで置き換える

拙作のカレンダビューに関して、以前にフリック・モーションで月を変えることに言及した。
ViewFlipperによるビューの切替えとアニメーション

実装としてはこれでOKだと思っていたのだが、いざ実機でテストしてみると腑に落ちない振る舞いをする。

  • 現象

左右のフリック・モーションが認識されないことがある。(不定期)

実装は以前にエントリに書いたように、ViewFlipperクラスのonTouchEvent中のアクションの切替え時に、タッチされてからの移動変量を閾値として次月又は前月に移動することで実装している。

    • ViewFlipper#onTouch抜粋
protected float lastTouchX;
@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: 
            lastTouchX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            float currentX = event.getX();
            if (lastTouchX < currentX) {
                viewflipper.showNext();
            }
            if (lastTouchX > currentX) {
                viewflipper.showPrevious();
            }
            break;
    }
    return true;
}

不具合に関しては昔ながらの手法で各アクションの発生時にログを出力しながら調べてみたのだが、MotionEvent.ACTION_DOWNは確実に発生しているのに対して、MotionEvent.ACTION_UPは発生したりしなかったりしている。これが直接の原因のようだが、しかしその理由が分からない。

  • GestureDetector

タッチイベントに関しては様々なジェスチャを検知するための専用のリスナがあることが解っていたが、プリミティブなタッチイベントでなんとかなると思って気にしていなかった。しかし、今回のような不具合が起きると標準的な実装に頼りたくなるが人情というものだ。

GestureDetectorにViewのonTouchEventを委譲することにより、より抽象度の高い、以下のジェスチャを捕捉することができるようになる。

    • onDown タッチ
    • onShowPress プレス開始?(タッチ後、指が動き出す前を捕捉したい場合に)
    • onSingleTapUp シングルタップ(タッチ後すぐに指を離した。マウスのクリックに相当)
    • onScroll スクロール(タッチしたまま指を滑らせた)
    • onLongPress ロングプレス(長押し)
    • onFling フライング(タッチしたまま一定の距離を移動した)

それぞれが独立したイベントとして捕捉可能なため、上記のコードのようにアクションをまたいで値を保持する必要が無く、プログラミングが簡素になるメリットがある。

    • ViewFlipper#onTouchをGestureDetectorに委譲
    final GestureDetector detector = 
        new GestureDetector(context, new OnGestureListener(){
            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2,
                    float velocityX, float velocityY) {
                
                if (e1.getX() < e2.getX()) {
                    viewflipper.showPrevious();
                } else {
                    viewflipper.showNext();
                }
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {}

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2,
                    float distanceX, float distanceY) {
                return true;
            }

            @Override
            public void onShowPress(MotionEvent e) {}

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                MonthlyCalendarView currentView = 
                    (MonthlyCalendarView)viewFlipper.getCurrentView();
                
                performSelectionCalendar(currentView);
                return result;
            }});
    viewFlipper.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return  detector.onTouchEvent(event);
        }
    
    });

onFlingの発生閾値は、タッチ後に移動距離がViewConfiguration.getMinimumFlingVelocity()で取得した値を超えたか否かを判定している。
確かにシンプルになるのだが、処理する必要の無いイベントの記述が邪魔だな。適切なアダプタを作ったほうが良いのかもしれない。

と、これで治ると思ったら大間違いだった。ソースコードを見てみたが、OnGestureListenerの各メソッドも結局はonTouch内で同様に判定をしているに過ぎないからだ。相変わらずACTION_UPが処理されないケースがある。

まだ何かが間違っている。それを書こうと思ったが今日はここまで。

※ UI操作の名称の基準が分かり難いので、今後はApplieが提唱する呼称で統一する。(しっくりこなかったら止める)

    • タップ……指で軽く叩く操作。マウスのクリックに相当
    • ダブルタップ……2回叩く操作。ダブルクリックに相当
    • ドラッグ……写真を移動する時に指をずらす操作
    • フリック……リストをスクロールする時に指で軽くはらう操作
    • ピンチ……2本指でのつまむ操作の総称
    • ピンチアウト/ピンチオープン……2本指の間を広げて拡大する時の操作
    • ピンチイン/ピンチクローズ……2本指の間を縮めて縮小する時の操作