自分用の調査メモです。
どういうコードで問題が発生するか
何かのAPIのロードが終わってからボトムシートを表示したい
と思ってこんなコードを書いてた
擬似コード
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 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>
見よ!ボトムシートが上から降ってくるぞい!
対処1:ボトムシート対象のビューに 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">
ビュー全体が上から降ってくる、という現象は起きなくなった。
しかしLinearLayoutの内包してるコンテンツ分の高さ分が見えてから引っ込むような挙動になっている。
対処2: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
}
}
お????? なおった???
hiddenにできてしまう、という問題はありつつ、上から降ってこなくなった!
対処3: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にするタイミングをずらしてやると対処はできそう。
擬似コード(修正版)
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
実動作を見ると...
キタ━━━━(゚∀゚)━━━━!! キタ━━━━(゚∀゚)━━━━!! キタ━━━━(゚∀゚)━━━━!!
ってなります。
後始末:少しだけまともなコードにする
擬似コードだとHideableフラグをActivityに直接もたせてるけど、実際のプロダクションコードだと、ActivityとかFragmentにボトムシートのHideableかどうかフラグなんて持たせたくない。
ということで、
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
}
}
}
こんな感じでヘルパークラスを適当に作って
ちょっとプロダクション寄りな擬似コード
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
}
}
これで同じ内容の処理ができるはず。