BottomSheetが上から降ってくる？と思ったときにチェックすること

自分用の調査メモです。

## どういうコードで問題が発生するか

何かのAPIのロードが終わってからボトムシートを表示したい

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/7c6159d0-5a2c-62f4-4c5b-e4b0f90de47c.png)


と思ってこんなコードを書いてた

```kotlin:擬似コード
    private fun showOrHideBottomSheet(show: Boolean) {
        if (show) {
            bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
            bottomSheet.isHideable = false

        } else {
            bottomSheet.isHideable = true
            bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
        }
    }
```

```xml:擬似コンテンツ
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <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">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <LinearLayout
            android:id="@+id/bottom_sheet"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:minHeight="48dp"
            android:background="#0cf"
            app:layout_behavior="@string/bottom_sheet_behavior">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:text="BottomSheetだ"/>
            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="240dp"
                android:layout_margin="8dp"
                android:background="#fc0">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="でっかいコンテンツ"/>
            </FrameLayout>
        </LinearLayout>
    </android.support.design.widget.CoordinatorLayout>
</layout>
```

### 見よ！ボトムシートが上から降ってくるぞい！

![test.gif](https://qiita-image-store.s3.amazonaws.com/0/74571/29f459b6-51f5-096c-bb83-4f3dc7a050e0.gif)

## 対処１：ボトムシート対象のビューに layout_gravity="bottom" 指定してみる

```
        <LinearLayout
            android:id="@+id/bottom_sheet"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"   ★★★ ← 追加 ★★★
            android:orientation="vertical"
            android:minHeight="48dp"
            android:background="#0cf"
            app:layout_behavior="@string/bottom_sheet_behavior">
```

![test.gif](https://qiita-image-store.s3.amazonaws.com/0/74571/c9aa9270-f702-f660-0984-69fabdd3116a.gif)

ビュー全体が上から降ってくる、という現象は起きなくなった。
しかしLinearLayoutの内包してるコンテンツ分の高さ分が見えてから引っ込むような挙動になっている。

## 対処２：isHideable=falseを一旦取ってみる

```
    private fun showOrHideBottomSheet(show: Boolean) {
        if (show) {
            bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
            //bottomSheet.isHideable = false  ★★★ ← コメントアウト ★★★

        } else {
            bottomSheet.isHideable = true
            bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
        }
    }
```


### お？？？？？ なおった？？？

![test.gif](https://qiita-image-store.s3.amazonaws.com/0/74571/2318c78b-9841-ddb0-b22e-559c16727fed.gif)

hiddenにできてしまう、という問題はありつつ、上から降ってこなくなった！

## 対処３：hideable=falseを指定するタイミングを、ボトムシートがHIDDENじゃなくなってからにする

```
   bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
   Log.d("MainActivity", "showOrHideBottomSheet: ここでhideableにしたいけどできなさそう")
   bottomSheet.isHideable = false
```

ってログを仕掛けてみるとわかるのだけど

```
02-20 00:37:02.657 D/MainActivity(10416): showOrHideBottomSheet: ここでhideableにしたいけどできなさそう
02-20 00:37:02.680 D/MainActivity(10416): BottomSheetCallback#onStateChanged: newState=2
02-20 00:37:03.090 D/MainActivity(10416): BottomSheetCallback#onStateChanged: newState=4
```

実は **STATE_COLLAPSEDをセットしてから実際に状態が変わるまではちょっと時間がかかる**。
なので、hiddenにするタイミングをずらしてやると対処はできそう。

```kotlin:擬似コード（修正版）
    private fun setup() {
        //...

        bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(view: View, newState: Int) {
                Log.d(LOG_TAG, "BottomSheetCallback#onStateChanged: newState=${newState}")

                // hideable=falseに指定するのは、実際にHIDDENじゃなくなってから
                if (newState != BottomSheetBehavior.STATE_HIDDEN && !bottomSheetHideable && bottomSheet.isHideable) {
                    Log.d(LOG_TAG, "showOrHideBottomSheet: ここでhideableにする")
                    bottomSheet.isHideable = false
                }
            }

            override fun onSlide(bottomSheet: View, slideOffset: Float) {

            }
        })
    }

    private fun showOrHideBottomSheet(show: Boolean) {
        if (show) {
            bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
            Log.d(LOG_TAG, "showOrHideBottomSheet: ここでhideableにしたいけどできなさそう")
            bottomSheetHideable = false

        } else {
            bottomSheetHideable = true
            bottomSheet.isHideable = true
            bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
        }
    }

```

ログ的にはこんな感じになって、

```
02-20 01:14:34.048 D/MainActivity(11664): showOrHideBottomSheet: ここでhideableにしたいけどできなさそう
02-20 01:14:34.061 D/MainActivity(11664): BottomSheetCallback#onStateChanged: newState=2
02-20 01:14:34.061 D/MainActivity(11664): showOrHideBottomSheet: ここでhideableにする
02-20 01:14:34.474 D/MainActivity(11664): BottomSheetCallback#onStateChanged: newState=4
```

実動作を見ると...

![test.gif](https://qiita-image-store.s3.amazonaws.com/0/74571/91b4e410-e194-f3e9-7552-df9f8757ae36.gif)

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!　ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!　ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!

ってなります。

　


## 後始末：少しだけまともなコードにする

擬似コードだとHideableフラグをActivityに直接もたせてるけど、実際のプロダクションコードだと、ActivityとかFragmentにボトムシートのHideableかどうかフラグなんて持たせたくない。

ということで、

```kotlin:BottomSheetHideableHelper.kt
internal class BottomSheetHideableHelper(private val bottomSheet: BottomSheetBehavior<*>) {
    private var hideable: Boolean = false

    fun setHideable(hideable: Boolean) {
        this.hideable = hideable

        if (!hideable) {
            // stateがhiddenじゃなければ、即座にhideable=falseにする
            if (bottomSheet.state != BottomSheetBehavior.STATE_HIDDEN && bottomSheet.isHideable) {
                bottomSheet.isHideable = false
            }
        } else {
            // hideable=trueはいつでもOK
            bottomSheet.isHideable = true
        }
    }

    fun onStateChanged(newState: Int) {
        // state がhiddenじゃなくなったのを待ち合わせてからhideable=falseにする
        if (newState != BottomSheetBehavior.STATE_HIDDEN && !hideable && bottomSheet.isHideable) {
            bottomSheet.isHideable = false
        }
    }
}
```

こんな感じでヘルパークラスを適当に作って

```kotlin:ちょっとプロダクション寄りな擬似コード
    private fun setup() {
        //...

        bottomSheetHideableHelper = BottomSheetHideableHelper(bottomSheet)
        bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(view: View, newState: Int) {
                Log.d(LOG_TAG, "BottomSheetCallback#onStateChanged: newState=${newState}")
                bottomSheetHideableHelper.onStateChanged(newState)
            }

            override fun onSlide(bottomSheet: View, slideOffset: Float) {

            }
        })
    }

    private fun showOrHideBottomSheet(show: Boolean) {
        if (show) {
            bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
            bottomSheetHideableHelper.setHideable(false)
        } else {
            bottomSheetHideableHelper.setHideable(true)
            bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
        }
    }
```

これで同じ内容の処理ができるはず。