抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

BottomSheetDialog自定义指北

最近在做控件,一个底部的弹出菜单,因为是dialog,所以弹出时虚拟按键和状态栏显得很不理想,产品要求隐藏掉系统的虚拟按键和状态栏,踩了些坑,记录下。

底部弹出时进入全屏隐藏状态栏、虚拟按键

这个在dialog创建后更改window的flag标志位,同时将最外层的view的fitsSystemWindows置为false即可,也就是在onShow之后,或者fragment的onActivityCreated之后,同时这时候还可以设置window的透明度,或者背景颜色以及window的进入动效。

dialog!!.window?.apply {
    //make window transparent
    setDimAmount(0f)
    //setBackgroundDrawableResource(android.R.color.transparent)
    //动画可自行处理
    //setWindowAnimations(R.style.Animation_Dialog_FadeInOut)
    makeFullScreenBottomSheet()
}
val frameLayout = dialog!!.findViewById<FrameLayout>(R.id.design_bottom_sheet)
(frameLayout.parent.parent as View).fitsSystemWindows = false

进入全屏的扩展函数:

fun Window.makeFullScreenBottomSheet() {
    val uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION.or(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
            .or(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
            .or(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
    decorView.systemUiVisibility = uiOptions
    addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}

锁屏后再开启屏幕虚拟按键会自己弹出来

这个问题是生命周期问题,锁屏后也就是关屏后实际上dialog已经重新走了一遍流程。解决办法也简单,就是在fragment的onResume中再处理一次。只是这次使用的是setOnSystemUiVisibilityChangeListener方法。

override fun onResume() {
super.onResume()
view?.setOnSystemUiVisibilityChangeListener {
dialog?.window?.apply {
makeFullScreenBottomSheet()
}
}
}

拦截点击外部自动消失事件

先上源码:

//wrapInBottomSheet method
coordinator.findViewById(id.touch_outside).setOnClickListener(new OnClickListener() {
    public void onClick(View view) {
        if (BottomSheetDialog.this.cancelable && BottomSheetDialog.this.isShowing() && BottomSheetDialog.this.shouldWindowCloseOnTouchOutside()) {
            BottomSheetDialog.this.cancel();
        }

    }
});

看源码会发现,实际上这个点击外部消失和以往的那种实现并不同,新版的实现选择使用一个view的点击事件来替代,id为:R.id.touch_outside,在kotlin中直接用即可,java中可能需要加上很长的一段包名。

首先关闭dialog本身的点击外侧消失功能:

dialog.setCanceledOnTouchOutside(false)

然后自己在dialog创建后设置监听,可以拿到点击的位置坐标,逻辑自行实现:

val outside = dialog!!.findViewById<View>(R.id.touch_outside)
outside.setOnTouchListener { v, event ->
    if (shouldDismiss(event.rawX.toInt(), event.rawY.toInt())) {
        Log.d(TAG, "dismiss")
        dismiss()
        true
    } else {
        Log.d(TAG, "no dismiss")
        false
    }
}

监听BottomSheetDialog控件在上拉下拉时的进度变化

因为涉及不同屏幕尺寸,相关的计算还请自行修正,我这里提供的只是一个在全屏状态下的计算,如果有状态栏或者虚拟按键而又没屏蔽,可能会有问题。

也是在中进行处理:

/**
 * 计算实际偏移量比例
 */var heightBiasOffset: Float = 1f

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    //----
    val frameLayout = dialog!!.findViewById<FrameLayout>(R.id.design_bottom_sheet)
    val screenHeight = getScreenHeight(activity ?: return)
    Log.d(TAG, "click outside  screenHeight:${screenHeight}")
    frameLayout.viewTreeObserver.addOnPreDrawListener {
        val alpha =
            (screenHeight - frameLayout.top) * 1.0 / (frameLayout.height * heightBiasOffset)
        val float = BigDecimal(alpha).setScale(2, BigDecimal.ROUND_HALF_UP).toFloat()
        if (heightBiasOffset == 1f) {
            heightBiasOffset = float
        }
        Log.d(
            TAG,
            "alpha:${float}  ${heightBiasOffset} frameLayout.height:${frameLayout.height * heightBiasOffset} ${(screenHeight - frameLayout.top)} "
        )
        true
    }
    //----float是当前弹窗位置的比例,完全弹出时为0.99近1,往下拉会逐渐减少到0,可用于透明度处理等需求
}

/**
 * 获取屏幕高度(px)
 *
 * @param context
 * @return
 */fun getScreenHeight(context: Context): Int {
    // 获取屏幕分辨率, 计算方格大小
    val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
    val displaySize = Point()
    wm.defaultDisplay.getRealSize(displaySize)
    return displaySize.y
}

需要注意的一点是,这里不要使用addOnDrawListener,因为在部分安卓sdk上如果设置得太早不会触发该方法的调用,原因看源码,相关文章推荐:https://kymjs.com/note/2018/09/20/01/

dialog和fragment所在activity的背景冲突导致看不到圆角问题

这个主要还是需要通过style解决,在DialogFragment加上初始化代码:

init {
setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Theme_Dialog_NoFrame)
}

style:

<style name="Theme.Dialog.NoFrame" parent="Theme.AppCompat.Light.Dialog">
    <item name="windowNoTitle">true</item>
    <item name="windowActionModeOverlay">false</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
    <item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
    <item name="android:windowActionBarOverlay">false</item>
    <item name="android:windowActionModeOverlay">false</item>
</style>

然后在BottomSheetDialog创建时使用带theme的构造方法

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val dialog = BottomSheetDialog(context!!, theme)
    dialog.setOnShowListener {
        onShowDialog(it as BottomSheetDialog)
    }
    return dialog
}

实例

以上遇到的坑差不多说完了。上完整代码,避免以后我自己也看不清,base类:

open class BaseBottomSheetDialog : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = BottomSheetDialog(context!!, theme)
dialog.setOnShowListener {
onShowDialog(it as BottomSheetDialog)
}
//关闭点击外侧消失
dialog.setCanceledOnTouchOutside(false)
return dialog
}

protected open fun onShowDialog(dialog: BottomSheetDialog) {

}

override fun onResume() {
super.onResume()
//解锁屏后虚拟按键屏蔽失效问题
view?.setOnSystemUiVisibilityChangeListener {
dialog?.window?.apply {
makeFullScreenBottomSheet()
}
}
}
}

然后写一个扩展类继承下:

class BottomSheetDialog : BaseBottomSheetDialog() {

    init {
        setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Theme_Dialog_NoFrame)
    }

    companion object {
        const val TAG = "BottomSheetDialog"

        fun create(context: Context): BottomSheetDialog {
            return instantiate(context, BottomSheetDialog::class.java.name) as BottomSheetDialog
        }

    /**
    * 获取屏幕高度(px)
    *
    * @param context
    * @return
    */    fun getScreenHeight(context: Context): Int {
    // 获取屏幕分辨率, 计算方格大小
    val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
    val displaySize = Point()
    wm.defaultDisplay.getRealSize(displaySize)
    return displaySize.y
    }

    /**
     * 计算实际偏差比例尺寸
     */    var heightBiasOffset: Float = 1f

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        dialog!!.window?.apply {
            //make window transparent
            setDimAmount(0f)
            //setBackgroundDrawableResource(android.R.color.transparent)
            //动画可自行处理
            //setWindowAnimations(R.style.Animation_Dialog_FadeInOut)
            makeFullScreenBottomSheet()
        }

        val frameLayout = dialog!!.findViewById<FrameLayout>(R.id.design_bottom_sheet)
        (frameLayout.parent.parent as View).fitsSystemWindows = false
        val outside = dialog!!.findViewById<View>(R.id.touch_outside)
        outside.setOnTouchListener { v, event ->
            if (shouldDismiss(event.rawX.toInt(), event.rawY.toInt())) {
                Log.d(TAG, "dismiss")
                dismiss()
                true
            } else {
                Log.d(TAG, "no dismiss")
                false
            }
        }

        val screenHeight = getScreenHeight(activity ?: return)
        Log.d(TAG, "click outside screenHeight:${screenHeight}")
        frameLayout.viewTreeObserver.addOnPreDrawListener {
            val alpha =
                (screenHeight - frameLayout.top) * 1.0 / (frameLayout.height * heightBiasOffset)
            val float = BigDecimal(alpha).setScale(2, BigDecimal.ROUND_HALF_UP).toFloat()
            if (heightBiasOffset == 1f) {
                heightBiasOffset = float
            }
            Log.d(
                TAG,
                "alpha:${float}  ${heightBiasOffset} frameLayout.height:${frameLayout.height * heightBiasOffset} ${(screenHeight - frameLayout.top)} "
            )
            true
        }

    }

    private fun shouldDismiss(x: Int, y: Int): Boolean {
        Log.d(TAG, "click outside  x:${x} y:${y}")
        return false
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //自行定义view
        return inflater.inflate(R.layout.bottom_sheet_fragment, container, false)
    }

}

用的时候:

BottomSheetDialog.create(this).show(supportFragmentManager,“tag”)

实际上就是一个fragment中填充了一个dialog,然后show实际上不是真正的show,只是一个全屏的fragment。相关的逻辑可在源码中找到。而DialogFragment本身的优点,这里就不再说明。


0条评论

发表评论