BottomSheetBehaviorを使う

Support Library 23.2から、Design Support LibraryにBottomSheetが追加されました！
http://android-developers.blogspot.jp/2016/02/android-support-library-232.html

今回追加されたBottomSheetではMaterial Designのガイドラインでいうところの[Persistent Bottom Sheets](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-persistent-bottom-sheets)と[Modal bottom sheets](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets)を使うことが出来ます。

この記事では主にPersistent Bottom Sheets（BottomSheetBehavior）について説明します。

# Persistent Bottom Sheets（BottomSheetBehavior）
Play Musicやマップアプリで使われているようなBottom Sheetを実現することが出来ます。

## BottomSheetの表示方法
![bullheadMMB29Qnapplecomputer02252016193853-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/52645/25e128ff-2aaf-d681-6890-c7611260f957.gif)


CoordinatorLayoutの直下のViewのBehaviorに、BottomSheetBehaviorを指定すると、そのViewをBottomSheetとして表示することが出来ます。
BottomSheetというViewが用意されているわけではありません。
具体的なレイアウトリソースは次のとおりです。

```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:minHeight="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Normal contents"
        android:textSize="30sp"
        />
  </LinearLayout>
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      app:layout_behavior="@string/bottom_sheet_behavior"
      app:behavior_peekHeight="200dp"
      android:background="@android:color/white"
      >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="BottomSheet"
        android:textSize="30sp"
        />
  </LinearLayout>
</android.support.design.widget.CoordinatorLayout>
```

今回の例ではBottomSheetにLinearLayoutを使用していますが、BottomSheetを引っ張って最も大きく表示した時に、BottomSheet内のコンテンツをスクロールさせたい場合は、スクロールをネスト出来るView（RecyclerViewやNestedScrollView）を使うようにしましょう。

## BottomSheetの表示位置を指定する
`app:behavior_peekHeight`属性をセットすることで、BottomSheetの表示位置を指定することが出来ます。

```xml
<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/bottom_sheet_behavior"
    app:behavior_peekHeight="300dp"
    >
```

setPeekHeightメソッドを使ってコードから指定することも出来ます。

```java
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(300);
```

表示位置を指定しない場合、何もしない状態ではBottomSheetが表示されません。が、画面の下にいるのでがんばれが引っ張ってこれます。
また、後述するBottomSheetを操作する方法で画面に表示することも出来ます。

## BottomSheetを隠すことが出来るようにする
![bullheadMMB29Qnapplecomputer02252016195956-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/52645/4618bde5-6046-1c75-c1c0-d5ea11bbd4f4.gif)

`app:behavior_hideable`属性でtrueを指定すると、BottomSheetを下にスワイプした時に、BottomSheetを画面外に隠すことが出来ます。

```xml
<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/bottom_sheet_behavior"
    app:behavior_hideable="true"
    app:behavior_peekHeight="300dp"
    >
```

setHideableメソッドを使ってコードから指定することも出来ます。

```java
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setHideable(true);
```

動画を見ていただくと、3つの場所でBottomSheetを留めることが出来るとわかると思います。
これは次の3つの場所です

- 最大
- peek height
- hide

## BottomSheetの状態を取得する
Behaviorに`BottomSheetBehavior.BottomSheetCallback`をセットすることで、BottomSheetの状態を取得することが出来ます。
コードは次のようになります。

```java
View bottomSheet = findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {

  @Override public void onStateChanged(View bottomSheet, int newState) {
    switch (newState) {
      case BottomSheetBehavior.STATE_DRAGGING:
        break;
      case BottomSheetBehavior.STATE_SETTLING:
        break;
      case BottomSheetBehavior.STATE_EXPANDED:
        break;
      case BottomSheetBehavior.STATE_COLLAPSED:
        break;
      case BottomSheetBehavior.STATE_HIDDEN:
        break;
    }
  }

  @Override public void onSlide(View bottomSheet, float slideOffset) {
  }
});
```

### onStateChangedメソッド
onStateChangedメソッドにはボトムシートの状態がnewState変数として渡されます。

| 値 | 状態 |
|:--:|:--:|
|STATE_DRAGGING|BottomSheetをドラッグしている|
|STATE_SETTLING|STATE_EXPANDED、STATE_COLLAPSEDに移り変わる直前|
|STATE_EXPANDED|BottomSheetが最大まで大きくなった時|
|STATE_COLLAPSED|BottomSheetがpeek heightで指定した場所にいる時|
|STATE_HIDDEN|BottomSheetが隠れた時|

いくつか注意点が有ります

- STATE_EXPANDED、STATE_COLLAPSED、STATE_HIDDENは実際に呼ばれるまでに少し時間がかかる
- ドラッグし続けてBottomSheetを動かすと`STATE_SETTLING`が呼ばれないことがある
- BottomSheetを指で弾いた時には、`STATE_DRAGGING`が呼ばれないことが有る

### onSlideメソッド
onSlideメソッドには、BottomSheetがどの位置にいるのかslideOffset変数として渡されます。
slideOffsetの値は-1から1までのfloat型で、BottomSheetが動くたびに断続的に値が送られてきます。
値の意味は次の表の通りです。

| 値 | 状態 |
|:--:|:--:|
|-1|STATE_HIDDENと同じ|
|0|STATE_COLLAPSEDと同じ|
|1|STATE_EXPANDEDと同じ|

onStateChangedメソッドよりも細かく値を取得したい場合に使えそうです。

## コードからBottomSheetを操作する
![bottom_button-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/52645/1f88a2c7-65f5-4434-180c-8ad3f9b56ef3.gif)

`setState`メソッドを使うと、BottomSheetの状態を指定することが出来ます。
BottomSheetの状態をHideに指定するコードは次のとおりです。

```java
View bottomSheet = findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
```

次の3つの状態を指定できます。

- Expande
- Hide
- Collapse

それぞれの状態の意味は既に説明済みですので省略します。

## BottomSheetにFloatingActionBottonを連動させる
Mapアプリなどでよく見る、BottomSheetとFloatingActionButton（以下FAB）を組み合わせた画面を作ってみましょう。
今回はExpandeとHide状態ではFABが消えるようにします。

### anchorを指定する
レイアウトリソース上で、FABにanchorとしてBottomSheetを指定します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:minHeight="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Normal contents"
        android:layout_margin="30dp"
        android:textSize="30sp"
        />
  </LinearLayout>
  <LinearLayout
      android:id="@+id/bottom_sheet"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      android:background="@android:color/white"
      app:layout_behavior="@string/bottom_sheet_behavior"
      app:behavior_peekHeight="200dp"
      app:behavior_hideable="true"
      >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="30dp"
        android:text="BottomSheet"
        android:textSize="30sp"
        />
  </LinearLayout>
  <android.support.design.widget.FloatingActionButton
      android:id="@+id/fab"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@mipmap/ic_launcher"
      android:layout_margin="16dp"
      app:layout_anchor="@id/bottom_sheet"
      app:layout_anchorGravity="right|top"
      />
</android.support.design.widget.CoordinatorLayout>
```

このまま動かしてみると、動きは追従するものの、FABが画面の中に留まります。
![bullheadMMB29Qnapplecomputer02252016202526-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/52645/ddd5ab6c-c0b8-b0c1-ed20-f5d739b74d45.gif)
anchorを指定すると、画面の外には移動しない特性があるためこのままではFABが消えません。

### BottomSheetの状態を取得してFABを制御する
当初FABのBehaviorにBottomSheet用の制御が増えたのかと思っていたのですが、[どうやらそうではないようです](http://togetter.com/li/942659)。
独自Behaviorでも出来ると思いますので誰か試してみたください。
今回はアドバイス頂いたとおり、BottomSheetの状態を取得してFABを制御してみます。

ちょっと雑ですが、onSlideメソッドに渡されるslideOffsetの値を使ってFABを制御します。

```java
private FloatingActionButton mFab;
private float mTmpSlideOffset;

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_fab);
  Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
  setSupportActionBar(toolbar);

  mFab = (FloatingActionButton) findViewById(R.id.fab);
  View bottomSheet = findViewById(R.id.bottom_sheet);
  BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
  behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
    @Override public void onStateChanged(View bottomSheet, int newState) {
    }

    @Override public void onSlide(View bottomSheet, float slideOffset) {
      if (slideOffset > 0.8) {
        if (mTmpSlideOffset > slideOffset) {
          mFab.show();
        } else {
          mFab.hide();
        }
      } else if (slideOffset < -0.7) {
        // 今回はpeek heightとhideの幅が短く、値の重みが異なるため値を変える
        if (mTmpSlideOffset < slideOffset) {
          mFab.show();
        } else {
          mFab.hide();
        }
      }
      mTmpSlideOffset = slideOffset;
    }
  });
}
```

なんとか形になったと思います。
![bullheadMMB29Qnapplecomputer02252016205740-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/52645/da7f2225-6778-af7f-6060-400487e5c56a.gif)
気になる人はもう少し調整してみてください。

# Modal bottom sheets（BottomSheetDialog, BottomSheetDialogFragment）
ガイドラインでいうところの、[Modal bottom sheets](https://www.google.com/design/spec/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets)がこれに当たります。
DirectShareのようなBottom Sheetです。

BottomSheetDialogFragmentあるいはBottomSheetDialogを用いてDialogとして実装します。
この説明はまた次回に・・・