From ada29b2f7a3b4089a0f21715fc0b9400a35df4ce Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 12 Aug 2023 17:43:06 -0600 Subject: [PATCH] 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. --- .../ViewBindingBottomSheetDialogFragment.kt | 74 +++++++++++++++++-- .../java/org/oxycblt/auxio/util/LangUtil.kt | 6 +- app/src/main/res/values/styles_ui.xml | 8 -- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt index 3a5adce95..e3087d42f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingBottomSheetDialogFragment.kt @@ -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 : 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 : 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. - behavior.state = BottomSheetBehavior.STATE_EXPANDED + 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) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index d1b0125eb..8b8d8c6a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -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 + } } /** diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 5106e12b9..18e87da49 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -32,19 +32,11 @@