如何基于BottomSheetDialogFragment实现非阻塞式底部弹窗?
基于BottomSheetDialogFragment实现非阻塞底部弹窗需求
问题背景
需要实现一款满足以下要求的底部弹窗:
- 圆角样式
- 固定高度
- 不可拖拽
- 内容可滚动
- 点击非弹窗区域隐藏弹窗
- 按返回键隐藏弹窗
- 非阻塞式:点击非弹窗区域时,弹窗隐藏且被点击元素获得焦点
使用BottomSheetBehavior可实现所有需求,但会增加Activity布局复杂度且需手动处理触摸事件;BottomSheetDialogFragment代码更简洁,但无法实现第7点——点击外部EditText时弹窗隐藏,但EditText无法获焦。
解决方案
核心思路是拦截BottomSheetDialog的触摸事件,记录弹窗外部被点击的View,在弹窗隐藏后主动为该View请求焦点。以下是具体实现步骤:
1. 自定义BottomSheetDialogFragment
重写onCreateDialog方法,配置弹窗属性并拦截触摸事件:
class CustomBottomSheetDialogFragment : BottomSheetDialogFragment() { private var clickedOutsideView: View? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // 加载弹窗内容布局(包含ScrollView实现内容滚动) return inflater.inflate(R.layout.bottom_sheet_content, container, false) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog // 配置弹窗核心属性 dialog.behavior.apply { isDraggable = false // 禁用拖拽 peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_sheet_fixed_height) // 设置固定高度 } // 拦截触摸事件,记录外部点击的View dialog.window?.decorView?.setOnTouchListener { v, event -> if (event.action == MotionEvent.ACTION_UP) { val sheetView = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) sheetView?.let { sheet -> val location = IntArray(2) sheet.getLocationOnScreen(location) val sheetLeft = location[0] val sheetTop = location[1] val sheetRight = sheetLeft + sheet.width val sheetBottom = sheetTop + sheet.height // 判断点击位置是否在弹窗外部 if (event.rawX < sheetLeft || event.rawX > sheetRight || event.rawY < sheetTop || event.rawY > sheetBottom) { // 根据坐标找到被点击的外部View clickedOutsideView = activity?.window?.decorView?.findViewAtPosition( event.rawX.toInt(), event.rawY.toInt() ) } } } // 传递触摸事件,保证原有逻辑正常执行 v.onTouchEvent(event) } // 监听弹窗隐藏事件,为外部View请求焦点 dialog.setOnDismissListener { clickedOutsideView?.requestFocus() clickedOutsideView = null } return dialog } }
2. 添加坐标找View的工具函数
扩展View类,实现根据屏幕坐标查找对应View的功能:
fun View.findViewAtPosition(x: Int, y: Int): View? { val childCount = childCount for (i in 0 until childCount) { val child = getChildAt(i) val location = IntArray(2) child.getLocationOnScreen(location) val childLeft = location[0] val childTop = location[1] val childRight = childLeft + child.width val childBottom = childTop + child.height if (x in childLeft..childRight && y in childTop..childBottom) { // 递归查找子View,确保找到最底层的可点击View return child.findViewAtPosition(x - childLeft, y - childTop) ?: child } } return this }
3. 配置弹窗圆角样式
在弹窗内容布局的根View中添加圆角背景:
bottom_sheet_content.xml
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bottom_sheet_rounded_bg"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <!-- 可滚动的内容区域 --> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="示例内容1" android:textSize="18sp"/> <!-- 添加更多内容实现滚动效果 --> </LinearLayout> </ScrollView>
drawable/bottom_sheet_rounded_bg.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="@android:color/white"/> <corners android:topLeftRadius="16dp" android:topRightRadius="16dp"/> </shape>
原理说明
- 拦截触摸事件时,通过坐标判断点击位置是否在弹窗外部,找到对应的目标View并记录。
- 当弹窗关闭(
onDismiss触发),主动调用requestFocus()让记录的外部View获得焦点,实现非阻塞的交互需求。 - 保留
BottomSheetDialogFragment的简洁性,无需额外布局和复杂的触摸事件处理。
内容的提问来源于stack exchange,提问作者Cheok Yan Cheng
相关产品推荐
相关产品推荐

