【Android】GoogleマップアプリのようなBottomSheet（画面下部のメニュー）を作る

# 概要
Androidアプリで、Google Mapのアプリのような[BottomSheet](https://material.io/develop/android/components/bottom-sheet-behavior/)を作ってみました。

![google_map_collapsed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/d3831363-9474-f51d-9c31-7b5785132e4e.png)　![google_map_expanded.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/5feb298f-24f6-7511-b5ba-56d9a1f1e343.png)
Googleの「マップ」アプリのBottomSheet

![bottom_sheet_collapsed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/1f1e1ee6-1f5c-537c-bdbc-a1e381bd8c36.png)　![bottom_sheet_expanded.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/00ac1e6c-2114-1cba-3610-79d50ad518dd.png)
作成したBottomSheet


主に以下のような特徴を実現しました。

- BottomSheet領域をスワイプ、またはBottomSheet上部のバー領域をタップすることで展開・収納できる
- FAB（Floating Action Button）がBottomSheetに追従して移動する

Google MapのBottomSheetは、収納時下にスワイプすると隠れる・展開時上にスワイプすると全画面になる、などの挙動もありますが今回はやっていません。

## BottomSheetとは
[マテリアルデザインのガイドライン](https://material.io/components/sheets-bottom/)では、BottomSheetについて以下のような説明がされています。
> Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen.

# BottomSheetを作成する
BottomSheetを作成します。

## 画面レイアウト
```fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#BED6FF"
    tools:context=".HomeFragment">

    <include
        layout="@layout/bottom_sheet"
        android:id="@+id/bottomSheet" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
```
[`CoordinatorLayout`](https://developer.android.com/reference/kotlin/androidx/coordinatorlayout/widget/CoordinatorLayout?hl=ja)を使ってBottomSheetの挙動を実現します。

## BottomSheet
```bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:orientation="vertical"
    android:background="@drawable/bottom_sheet_background"
    android:elevation="30dp"
    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
    app:behavior_peekHeight="60dp">

    <LinearLayout
        android:id="@+id/bottomSheetBarArea"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
 
       <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:gravity="center"
            android:background="#00000000">

            <View
                android:id="@+id/bottomSheetBar"
                android:layout_width="40dp"
                android:layout_height="6dp"
                android:background="@drawable/bottom_sheet_bar" />
 
       </LinearLayout>

    </LinearLayout>

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="BottomSheet"
        android:textColor="@android:color/black"
        android:textSize="18sp" />

</LinearLayout>
```

<details><summary>drawable</summary><div>

```bottom_sheet_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="0dp"
        android:topLeftRadius="6dp"
        android:topRightRadius="6dp" />
    <solid android:color="@android:color/white"/>
</shape>
```

```bottom_sheet_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#808080"/>
    <corners android:radius="5dp"/>
</shape>
```

</div></details>

BottomSheetのレイアウトは`LinearLayout`で作成しています。中身は好きなように要素を配置してください。

- `layout_height`  
今回は展開時のBottomSheetの高さを指定しています。
- `background`  
上部のみ角丸の背景（`bottom_sheet_background`）を設定しています。
- `layout_behavior`  
`CoordinatorLayout`の子要素に[`BottomSheetBehavior`](https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetBehavior)を適用することで、その要素をBottomSheetのように動かせます。これだけで、スワイプで展開・収納できます。
- `behavior_peekHeight`  
収納時の高さを指定できます。`setPeekHeight(peekHeight)`でスクリプトでも設定できます。

今回は設定していませんが、`app:behavior_hideable="true"`でBottomSheetを隠せるようにできます。
![bottom_sheet_hideable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/af244106-ce06-fade-6928-043bf5f104aa.png)



### 上部のバー領域
タップすると展開・収納できる領域を作成します。バー部分だけではタップできる領域が狭すぎるため、バー以外の上部をタップした時にもイベントが発生するように、透明なバー領域を作成しています。

![bottom_sheet_bar_area.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/b6d5d2bd-10a3-a121-d6f0-4b907a8a69c3.png)
黒い領域がタップ領域です。

## スクリプト
今回は、バー領域をタップした時にもBottomSheetを展開・収納できるように`ClickListener`を追加しました。

```HomeFragment.kt
class HomeFragment : Fragment() {
  private lateinit var bottomSheet: View
  private lateinit var bottomSheetBarArea: View
  private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
  override fun onCreateView(inflater: LayoutInflater,
                          container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)

    val view = inflater.inflate(R.layout.fragment_home, container, false)
    bottomSheet = view.findViewById(R.id.bottomSheet)
    bottomSheetBarArea = view.findViewById(R.id.bottomSheetBarArea)
    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
    setup()
    return view
  }

  private fun setup() {
    bottomSheetBarArea.setOnClickListener {
      if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
      } else if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED)
      }
    }
  }
}
```

`BottomSheetBehavior.from(view: V)`でBottomSheetBehaviorを取得します。
そして、bottomSheetBarAreaをタップした時の処理を、BottomSheetBehaviorの状態によって分岐しています。

- 収納されている時（`STATE_COLLAPSED`）には展開（`STATE_EXPANDED`）する
- 展開されている時には収納する

これで、スワイプだけでなくタップでも展開・収納できます。

# 別の要素を追従させる
BottomSheetの動きに合わせて[FAB（Floating Action Button）](https://material.io/components/buttons-floating-action-button/)を上下させます。今回は2つのFABを`LinearLayout`で囲って一緒に動かしています。
![bottom_sheet_fab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/084aa162-c352-2969-b7b2-596932922a41.png)


```fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#BED6FF"
    tools:context=".HomeFragment">

    <include
        layout="@layout/bottom_sheet"
        android:id="@+id/bottomSheet" />

    <LinearLayout
        android:id="@+id/fabContainer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_gravity="top"
        app:layout_anchor="@id/bottomSheet"
        app:layout_anchorGravity="top|end">

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
            app:backgroundTint="#FF0000" />

    </LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

- `layout_gravity`  
`top`を指定してBottomSheetと被らないようにしています。
- `layout_anchor`  
[アンカー](https://developer.android.com/reference/kotlin/androidx/coordinatorlayout/widget/CoordinatorLayout.LayoutParams#setAnchorId(kotlin.Int))を指定します。今回はBottomSheetに追従させたいのでBottomSheet要素を指定しています。
- `layout_anchorGravity`  
アンカーに対するgravityを設定します。今回はBottomSheetの右上に配置したいので`top|end`を指定しています。

# まとめ
- `CoordinatorLayout`と`BottomSheetBehavior`でBottomSheetを作成しました。
- アンカーを指定して別の要素をBottomSheetに追従するようにしました。

# 参考
https://qiita.com/napplecomputer/items/5b3d1225533a59488ac3