Navigation drawerを左端以外をスワイプしても反応させる方法あれこれ

2016/06/27

画面の左端をスワイプすると表示される形式のメニューをMaterial DesignではNavigation drawerと呼びます。

Navigation drawer - Patterns - Google design guidelines

これをAndroidで簡単に実装するための仕組みとしてSupport LibraryでDrawerLayoutが提供されています。DrawerLayoutは実装が簡単でいい感じなんですが、端っこしか反応しないんですよね。

今回は画面のどこをスワイプしてもメニューが表示されるようにしたかったのです。ところがDrawerLayoutにはそんなオプションありません。 どうやら「端っこだけが反応する」のがNavigation drawerのポリシーのようです。

ポリシーからは外れちゃいますが、やりたいものは仕方ない。

ということで実現方法を調べたらいくつかやり方が見つかったので整理しておきます。いずれも一長一短なので要件や用途に合わせてお好きなものをどうぞ。

  • SlidingMenuを使う
  • SlidingPaneLayoutを使う
  • reflectionでDrawerLayoutを無理やり書き換える
  • DrawerLayoutとGestureDetectorを組み合わせる

SlidingMenuを使う

純正ライブラリにこだわらず、OSSを探そう、というアプローチ。NavigationDrawerを実現するOSSのライブラリの中ではjfeinstein10/SlidingMenuが一番有名っぽいです。実績は十分ですが、開発が2014年で止まってるのがマイナスポイント。

SlidingPaneLayoutを使う

SlidingPaneLayoutはマルチペインレイアウトを簡単に実現するためのLayout。2つのViewが画面に収まるかどうかを自動で判断し、並べて表示するか、片方をスライドメニューっぽい挙動にするか制御してくれます。

もうちょっと詳しく、という方はマルチペインレイアウトを簡単に実装する方法 - Qiitaあたりをご参照くださいませ。日本語情報あんまりないですね。使ってる人少ないのかな?

これを使って常にスライドメニューっぽい挙動にすればいい、というアプローチ。軽く実装してみたところ、確かにそれっぽくなります。ただ、細かいところがちょっと違うんだよなー。そもそもの用途が別なんで違って当たり前ですが。

reflectionでDrawerLayoutを無理やり書き換える

DrawerLayoutの中にあるViewDragHelperが「端からどれくらいの幅で反応するか」を持っています。mEdgeSizeというメンバ変数がそれ。mEdgeSizeを変更するようなメソッドは用意されていないので、reflectionで書き換えちゃおう、というアプローチ。

Set drag margin for Android Navigation Drawer - Stack Overflow

この通り書き換えれば確かに動くんですが、こういう感じでreflection使うのちょっと嫌だなあ、と。

DrawerLayoutとGestureDetectorを組み合わせる

GestureDetectorでスワイプを検知して、DrawerLayoutを明示的にopenするアプローチ。

NavigationDrawerとフリックでの表示 - Qiita

今回はこれを採用しました。上記サイトを参考にcloseもできるように拡張してみました。GestureDetectorのonFlingをこんな感じで実装します。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

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

  if (dx > dy && dx > SWIPE_VELOCITY_THRESHOLD) {

    if (e1.getX() - e2.getX() < SWIPE_THRESHOLD) {
      drawerContainer.openDrawer(GravityCompat.START);
      return true;

    } else if (e2.getX() - e1.getX() < SWIPE_THRESHOLD) {
      drawerContainer.closeDrawers();
      return true;
    }
  }

  return false;
}

SWIPE_VELOCITY_THRESHOLDとSWIPE_THRESHOLDで感度を調整します。100~300の間くらいですかね。

あとはActivityでGestureDetectorを普通に有効にし、dispatchTouchEventをOverrideすればOK。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    return gd.onTouchEvent(ev) || super.dispatchTouchEvent(ev);
}

画面大きめの端末を片手で操作してると左端からスワイプしたり左上のハンバーガーメニューをタップしたりはやりにくいですよね。 Navigation DrawerもどこをスワイプしてもOK、という風にならないかなあ。




関連(するかもしれない)記事


おススメ