2012年9月7日金曜日

GestureDetectorでフリック処理を実装した時に困った事について

GridView(ListViewも含む)でアイテムに対してOnItemClickListenerと
OnItemLongClickListenerのみ設定するのは容易だが、更にGridView上で
GestureDetectorによるフリック処理も実装するとなると、タッチイベントの
処理に注意が必要となる。

と言うのも、自作アプリにViewFlipper(子ViewはGridViewまたはListView)と
GestureDetectorを追加してフリック処理を実装する際、下記2つの問題点に
出くわしてかなり困った。

以下に問題点と解決方法を併せて記載する。

【問題点】
(1)GridView上でフリックして画面遷移させた後、元の画面に戻るとタッチした箇所のアイテムにフォーカスが当たったままとなってしまう。

(2)GridView上でアイテムを長押ししてダイアログを表示させる直前にフリックすると、ダイアログが表示された裏で画面が遷移してしまう。
(ロングクリック処理後のフリック処理はキャンセルされるものだと勝手に想定していたが違った)

【回避方法】
・問題点1について
GridViewのOnTouchListenerで、GestureDetector#onTouchEventをチェックした後、イベント発行元(ここではGridView)に対してMotionEvent.ACTION_CANCELを発行する。

・問題点2について
ロングクリックとフリックイベントは以下の順序で実行される。
OnGestureListener#onDown→OnItemLongClickListener#onItemLongClick→OnGestureListener#onFling
そこでonItemLongClickでフラグを立てて、フラグが立っていたらonFlingをキャンセルさせる。

具体的には下記のようなコードとなる。
// レイアウトXMLのViewFlipperにはGridViewが3つ定義済みとする

private ViewFlipper viewFlipper = null;
private GestureDetector gestureDetector = null;
private boolean isItemLongClickEnable = false;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    OnGestureListenerImpl gestureListener = new OnGestureListenerImpl();
    GestureDetector gestureDetector = new GestureDetector(this, gestureListener);

    GridView gridView01 = (GridView)findViewById(R.id.gridview01);
    GridView gridView02 = (GridView)findViewById(R.id.gridview02);
    GridView gridView03 = (GridView)findViewById(R.id.gridview03);
    
    OnTouchListenerImpl touchListener = new OnTouchListenerImpl();
    // タッチリスナーを設定
    gridView01.setOnTouchListener(touchListener);
    gridView02.setOnTouchListener(touchListener);
    gridView03.setOnTouchListener(touchListener);

    OnItemClickListenerImpl onClickListener = new OnItemClickListenerImpl();
    // クリックリスナーを設定
    gridView01.setOnItemClickListener(onClickListener);
    gridView02.setOnItemClickListener(onClickListener);
    gridView03.setOnItemClickListener(onClickListener);
    // ロングクリックリスナーを設定
    gridView01.setOnItemLongClickListener(onClickListener);
    gridView02.setOnItemLongClickListener(onClickListener);
    gridView03.setOnItemLongClickListener(onClickListener);

    viewFlipper = (ViewFlipper)findViewById(R.id.viewflipper);
    viewFlipper.setDisplayedChild(0);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    // GridView以外の領域でスワイプした場合
    if(gestureDetector.onTouchEvent(event)){
        return true;
    }

    return false;
}

private class OnItemClickListenerImpl implements OnItemClickListener, OnItemLongClickListener{

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // 任意の処理
    }

    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        // ロングクリックのフラグを立てる
        isItemLongClickEnable = true;

        // 任意の処理
    }
}

private class OnTouchListenerImpl implements OnTouchListener {

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        if(gestureDetector.onTouchEvent(event)){
            // ACTION_CANCELイベントでViewのタッチイベントをキャンセル(問題点1の回避)
            MotionEvent cancelEvent = MotionEvent.obtain(event);
            cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
            view.onTouchEvent(cancelEvent);

            return true;
        }

        return false;
    }
}

private class OnGestureListenerImpl implements OnGestureListener {

    @Override
    public boolean onDown(MotionEvent event) {
        // 毎回ここでロングクリックのフラグを初期化
        isItemLongClickEnable = false;

        return false;
    }

    @Override
    public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
        // ロングクリックのフラグが立っていたらフリック処理をキャンセル(問題点2の回避)
        if(isItemLongClickEnable){
            isItemLongClickEnable = false;

            return true;
        }

        float dx = Math.abs(velocityX);
        float dy = Math.abs(velocityY);

        if(dx > dy && dx > 200) {
            if(event2.getX() - event1.getX() < 150){
                // 前のViewへ遷移
                viewFlipper.showPrevious();
            } else if(event1.getX() - event2.getX() < 150){
                // 次のViewへ遷移
                viewFlipper.showNext();
            }

            return true;
        }

        return false;
    }

    @Override
        public void onLongPress(MotionEvent event) {
    }

    @Override
    public boolean onScroll(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
        return false;
    }

    @Override
        public void onShowPress(MotionEvent event) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent event) {
        return false;
    }
}
イベント処理をフラグ管理するとハマるのでなるべく使用したくないのだが、
onItemLongClickとonFlingを両立させるには仕方ないと思う。

この問題点について下記リンク等に関連した内容があるので参考にして欲しい。

■ Android ListView with OnItemClickListener AND GestureDetector
http://stackoverflow.com/questions/4817770/android-listview-with-onitemclicklistener-and-gesturedetector

0 件のコメント:

コメントを投稿