ui: improve bottom sheet edge-to-edge support
Don't disable bottom sheet inset calculations and use the expanded state hack to mitigate for the peek height calculation, instead, just clobber the window inset routine to fix the peek height while not applying the padding. The expanded hack still remains, but is now relegated to the cases where the 16:9 keyline breaks down.
This commit is contained in:
parent
4ade27e66d
commit
ada29b2f7a
3 changed files with 71 additions and 17 deletions
|
@ -20,16 +20,25 @@ package org.oxycblt.auxio.ui
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
|
@ -91,7 +100,10 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = onCreateBinding(inflater).also { _binding = it }.root
|
||||
): View {
|
||||
val root = onCreateBinding(inflater).also { _binding = it }.root
|
||||
return EdgeToEdgeFixWrapperLayout(root.context).apply { addView(root) }
|
||||
}
|
||||
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
@ -110,18 +122,66 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
|
|||
private inner class TweakedBottomSheetDialog
|
||||
@JvmOverloads
|
||||
constructor(context: Context, @StyleRes theme: Int = 0) : BottomSheetDialog(context, theme) {
|
||||
private var avoidUnusableCollapsedState = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Collapsed state is bugged in phone landscape mode and shows only 10% of the dialog.
|
||||
// Just disable it and go directly from expanded -> hidden.
|
||||
behavior.skipCollapsed = true
|
||||
// Automatic peek height calculations are bugged in phone landscape mode and show only
|
||||
// 10% of the dialog. Just disable it in that case and go directly from expanded ->
|
||||
// hidden.
|
||||
val metrics = context.resources.displayMetrics
|
||||
avoidUnusableCollapsedState =
|
||||
metrics.heightPixels - metrics.widthPixels <
|
||||
context.getDimenPixels(
|
||||
com.google.android.material.R.dimen.design_bottom_sheet_peek_height_min)
|
||||
behavior.skipCollapsed = avoidUnusableCollapsedState
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// Manually trigger an expanded transition to make window insets actually apply to
|
||||
// the dialog on the first layout pass. I don't know why this works.
|
||||
if (avoidUnusableCollapsedState) {
|
||||
// skipCollapsed isn't enough, also need to immediately snap to expanded state.
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EdgeToEdgeFixWrapperLayout
|
||||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
// BottomSheetBehavior's normal window inset behavior is awful. It doesn't
|
||||
// follow true edge-to-edge and instead just blindly pads the bottom part of
|
||||
// the view, causing visual clipping. We can turn it off, but that throws
|
||||
// of the peek height calculation and results in a collapsed state that only
|
||||
// expands a few pixels (specifically the size of the bottom inset) into an
|
||||
// expanded state. So, ideally we would just vendor and eliminate the padding
|
||||
// changes entirely, but due to layout dependencies that requires vendoring
|
||||
// both BottomSheetDialog and BottomSheetDialogFragment, which I generally
|
||||
// don't want to do. Instead, we deliberately clobber the window insets listener
|
||||
// of our bottom sheet and only re-implement the update of the cached inset
|
||||
// variables and the peek height update. This way, the peek height calculation
|
||||
// remains consistent and the top inset animation continues to work correctly
|
||||
// without the other absurd edge-to-edge behaviors.
|
||||
// TODO: Do a fix for this upstream
|
||||
(parent as View).setOnApplyWindowInsetsListener { v, insets ->
|
||||
val bsb = v.coordinatorLayoutBehavior as BottomSheetBehavior
|
||||
BSB_INSET_TOP_FIELD.set(bsb, insets.systemBarInsetsCompat.top)
|
||||
BSB_INSET_BOTTOM_FIELD.set(bsb, insets.systemBarInsetsCompat.bottom)
|
||||
BSB_UPDATE_PEEK_HEIGHT_METHOD.invoke(bsb, false)
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val BSB_INSET_TOP_FIELD: Field by
|
||||
lazyReflectedField(BottomSheetBehavior::class, "insetTop")
|
||||
val BSB_INSET_BOTTOM_FIELD: Field by
|
||||
lazyReflectedField(BottomSheetBehavior::class, "insetBottom")
|
||||
val BSB_UPDATE_PEEK_HEIGHT_METHOD: Method by
|
||||
lazyReflectedMethod(BottomSheetBehavior::class, "updatePeekHeight", Boolean::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,8 +82,10 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
|||
* @param clazz The [KClass] to reflect into.
|
||||
* @param method The name of the method to obtain.
|
||||
*/
|
||||
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
||||
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true }
|
||||
fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = lazy {
|
||||
clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also {
|
||||
it.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,19 +32,11 @@
|
|||
</style>
|
||||
|
||||
<style name="Widget.Auxio.BottomSheet" parent="Widget.Material3.BottomSheet">
|
||||
<item name="marginLeftSystemWindowInsets">false</item>
|
||||
<item name="marginRightSystemWindowInsets">false</item>
|
||||
<item name="paddingBottomSystemWindowInsets">false</item>
|
||||
<item name="paddingTopSystemWindowInsets">false</item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.None</item>
|
||||
<item name="shouldRemoveExpandedCorners">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Auxio.BottomSheet.Modal" parent="Widget.Material3.BottomSheet.Modal">
|
||||
<item name="marginLeftSystemWindowInsets">false</item>
|
||||
<item name="marginRightSystemWindowInsets">false</item>
|
||||
<item name="paddingBottomSystemWindowInsets">false</item>
|
||||
<item name="paddingTopSystemWindowInsets">false</item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.None</item>
|
||||
<item name="shouldRemoveExpandedCorners">true</item>
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue