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:
Alexander Capehart 2023-08-12 17:43:06 -06:00
parent 4ade27e66d
commit ada29b2f7a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 71 additions and 17 deletions

View file

@ -20,16 +20,25 @@ package org.oxycblt.auxio.ui
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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.logD
import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -91,7 +100,10 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? 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?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -110,18 +122,66 @@ abstract class ViewBindingBottomSheetDialogFragment<VB : ViewBinding> :
private inner class TweakedBottomSheetDialog private inner class TweakedBottomSheetDialog
@JvmOverloads @JvmOverloads
constructor(context: Context, @StyleRes theme: Int = 0) : BottomSheetDialog(context, theme) { constructor(context: Context, @StyleRes theme: Int = 0) : BottomSheetDialog(context, theme) {
private var avoidUnusableCollapsedState = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Collapsed state is bugged in phone landscape mode and shows only 10% of the dialog. // Automatic peek height calculations are bugged in phone landscape mode and show only
// Just disable it and go directly from expanded -> hidden. // 10% of the dialog. Just disable it in that case and go directly from expanded ->
behavior.skipCollapsed = true // 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() { override fun onStart() {
super.onStart() super.onStart()
// Manually trigger an expanded transition to make window insets actually apply to if (avoidUnusableCollapsedState) {
// the dialog on the first layout pass. I don't know why this works. // skipCollapsed isn't enough, also need to immediately snap to expanded state.
behavior.state = BottomSheetBehavior.STATE_EXPANDED 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)
} }
} }
} }

View file

@ -82,8 +82,10 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
* @param clazz The [KClass] to reflect into. * @param clazz The [KClass] to reflect into.
* @param method The name of the method to obtain. * @param method The name of the method to obtain.
*/ */
fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { fun lazyReflectedMethod(clazz: KClass<*>, method: String, vararg params: KClass<*>) = lazy {
clazz.java.getDeclaredMethod(method).also { it.isAccessible = true } clazz.java.getDeclaredMethod(method, *params.map { it.java }.toTypedArray()).also {
it.isAccessible = true
}
} }
/** /**

View file

@ -32,19 +32,11 @@
</style> </style>
<style name="Widget.Auxio.BottomSheet" parent="Widget.Material3.BottomSheet"> <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="shapeAppearance">@style/ShapeAppearance.Material3.Corner.None</item>
<item name="shouldRemoveExpandedCorners">true</item> <item name="shouldRemoveExpandedCorners">true</item>
</style> </style>
<style name="Widget.Auxio.BottomSheet.Modal" parent="Widget.Material3.BottomSheet.Modal"> <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="shapeAppearance">@style/ShapeAppearance.Material3.Corner.None</item>
<item name="shouldRemoveExpandedCorners">true</item> <item name="shouldRemoveExpandedCorners">true</item>
</style> </style>