Android中BottomSheetDialog的折叠、半展开、底部固定按钮等方案实现

文章目录


前言

在 Android 应用开发中,BottomSheetDialog 是 Material Design 体系下高频使用的底部弹窗组件,凭借滑动折叠、半屏展示、沉浸式交互等特性,广泛应用于筛选面板、操作菜单、详情弹窗等场景。但实际开发中,开发者常面临三类核心问题:

  1. 折叠状态与高度失控:默认的 BottomSheetDialog 折叠高度(peekHeight)适配性差,不同屏幕(如刘海屏、折叠屏)下高度计算错误,无法精准实现「半屏展开」「自定义折叠高度」;
  2. 多状态切换不灵活:需要同时支持「折叠(最小高度)」「半展开(中间高度)」「全屏(最大高度)」三种状态,但官方 API 未提供直接的多状态配置方案;
  3. 底部按钮跟随滑动:弹窗底部的确认 / 取消按钮会随面板一起拖动,破坏交互逻辑,需要实现「面板滑动时按钮固定在底部」的效果。

针对以上痛点,本文将从实际开发场景出发,结合 Android 全版本屏幕尺寸适配方案,详细讲解 BottomSheetDialog 的核心定制技巧:包括基于屏幕可用高度精准设置折叠 / 半展开高度、通过 BottomSheetBehavior 实现多状态切换、以及底部固定按钮的布局与交互方案。


一、基础使用方法

kotlin 复制代码
activity_main.xml页面:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/dialog_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />
    <Button
        android:id="@+id/dialog_btn"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:text="底部固定按钮"
        />
</LinearLayout>



activity页面:
class MainActivity : BaseActivity() {
    private lateinit var textBtn: Button
    val data = mutableListOf<String>("这是一条数据","这是一条数据","这是一条数据","这是一条数据","这是一条数据")
    override fun setViewId(): Int = R.layout.activity_main
    override fun initView() {
        textBtn = findViewById(R.id.text_btn)
    }
    override fun initListener() {
        textBtn.setOnClickListener {
            setBottomSheetDialog()
        }
    }

    /**
     *  @describe: 设置BottomSheetDialog的相关内容
     *  @params:
     *  @return:
     */
    private fun setBottomSheetDialog(){
        //创建BottomSheetDialog
        //获取dialog布局的view
        val view = LayoutInflater.from(this@MainActivity).inflate(R.layout.dialog_text,null)
        //初始化dialog对象
        val bottomSheetDialog = BottomSheetDialog(this@MainActivity)
        //设置dialog布局
        bottomSheetDialog.setContentView(view)
        //展示
        bottomSheetDialog.show()

        //设置bottomSheetDialog中recyclerView的相关内容
        setRecyclerView(view)
    }

    /**
     *  @describe: bottomSheetDialog中展示数据的RecyclerView的相关设置
     *  @params:
     *  @return:
     */
    private fun setRecyclerView(view: View) {
        val recyclerView = view.findViewById<RecyclerView>(R.id.dialog_recyclerview)
        recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
        recyclerView.adapter = RecyclerViewAdapter1(data){
            Toast.makeText(this@MainActivity,"当前点击了条目$it", Toast.LENGTH_SHORT).show()
        }
    }
}


adapter页面:
class RecyclerViewAdapter1(private val data:List<String>,private val onClick:(Int) -> Unit): RecyclerView.Adapter<RecyclerViewAdapter1.RecyclerViewHolder>() {
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): RecyclerViewHolder {
       val view = LayoutInflater.from(parent.context).inflate(R.layout.item_recyclerview1,parent,false)
        return RecyclerViewHolder(view)
    }

    override fun onBindViewHolder(
        holder: RecyclerViewHolder,
        position: Int
    ) {
       holder.bind(position,onClick)
    }

    override fun getItemCount(): Int = data.size

   inner class RecyclerViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
      private val itemView = itemView.findViewById<TextView>(R.id.item_text)

        fun bind(position: Int, onClick: (Int) -> Unit){
            itemView.text = data[position]
            itemView.setOnClickListener {
                 onClick(position) //执行作为函数类型的函数体
            }
        }
    }
}

二、不同场景下的设置

下面所用的获取全屏高度工具类如下:
Android中获取当前设备的宽高与屏幕密度等数据的工具类

点击后展开为全屏状态

kotlin 复制代码
其他代码与上面相同,只改动bottomSheetDialog 属性:
val view = LayoutInflater.from(this@MainActivity).inflate(R.layout.dialog_text,null)
        //初始化dialog对象
        val bottomSheetDialog = BottomSheetDialog(this@MainActivity)
        //设置dialog布局
        bottomSheetDialog.setContentView(view)
        //获取底部可活动面板的布局FrameLayout
        val frameLayout = bottomSheetDialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)!!
        //⭐关键:设置可活动面板高度为全屏
        frameLayout.layoutParams.height = ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度
        //获取FrameLayout的behavior
        val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)
        //通过behavior设置具体属性
        behavior.apply {
            //设置展示时的初始状态为展开
            state = BottomSheetBehavior.STATE_EXPANDED //不设置此状态则展开为折叠高度,拖动最高为全屏
            //设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
        }

        //展示
        bottomSheetDialog.show()

点击后展开为半屏状态

kotlin 复制代码
 private fun setBottomSheetDialog(){
        //创建BottomSheetDialog
        //获取dialog布局的view
        val view = LayoutInflater.from(this@MainActivity).inflate(R.layout.dialog_text,null)
        //初始化dialog对象
        val bottomSheetDialog = BottomSheetDialog(this@MainActivity)
        //设置dialog布局
        bottomSheetDialog.setContentView(view)
        //获取底部可活动面板的布局FrameLayout
        val frameLayout = bottomSheetDialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)!!
        
        //设置可活动面板高度为全屏
        frameLayout.layoutParams.height = ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度
        //获取FrameLayout的behavior
        val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)
        //通过behavior设置具体属性
        behavior.apply {
            //⭐关键:设置展示时的初始状态为半展开
            state = BottomSheetBehavior.STATE_HALF_EXPANDED
            halfExpandedRatio = 0.5f //配合状态调节半展开比例,0<取值<1,通常配置全屏高度设置
            //设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
        }

        //展示
        bottomSheetDialog.show()

        //设置bottomSheetDialog中recyclerView的相关内容
        setRecyclerView(view)
    }

注:

1.半折叠的状态必须配合 frameLayout.layoutParams.height为全屏高度才会有上面的效果,如果没有设置面板会默认为自适应高度,此时面板出现位置过高会如下图出现底部变成透明样子;
2.halfExpandedRatio 可以在0到1之间自由调节,但是不能为0或1,通过该数值可以自由调节展开状态的高度比例;

3.当前设置仅仅会在展开时出现目标高度,来回拖动时只会在全屏跟折叠高度之间停留,如果需要在目标高度进行停留时可参考下面的三态切换。

点击后展开为折叠状态

设置了面板高度为全屏高度时:

kotlin 复制代码
   //设置可活动面板高度为全屏
        frameLayout.layoutParams.height = ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度
        //获取FrameLayout的behavior
        val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)
        //通过behavior设置具体属性
        behavior.apply {
            //⭐关键:设置展示时的初始状态为折叠态,不设置全屏高度时最高只能拖动面板自适应高度
            state = BottomSheetBehavior.STATE_COLLAPSED
            //设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
        }

没有设置面板高度为全屏高度时:

kotlin 复制代码
val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)
        //通过behavior设置具体属性
        behavior.apply {
            //设置展示时的初始状态为折叠态,不设置全屏高度时最高只能拖动面板自适应高度
            state = BottomSheetBehavior.STATE_COLLAPSED
            //设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
        }

点击后展开为半屏状态且可以在全屏、半屏、折叠之间三态切换

kotlin 复制代码
  //⭐关键:设置可活动面板高度为全屏
        frameLayout.layoutParams.height =
            ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度
        //获取FrameLayout的behavior
        val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)
        //通过behavior设置具体属性
        behavior.apply {
            //⭐关键:设置展示时的初始状态为半展开
            state = BottomSheetBehavior.STATE_HALF_EXPANDED
            halfExpandedRatio = 0.5f //半展开比例
            isHideable = true          // 允许下拉隐藏
            skipCollapsed = false     // 不跳过折叠态
            //设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
            isFitToContents = false //⭐关键:高度不由内容决定
        }

全屏态、半屏态、折叠态之间两两组合实现两态切换

1.全屏态与半屏态切换:

用折叠态高度充当半屏态,其余设置与基础设置相同

kotlin 复制代码
//⭐关键:设置可活动面板高度为全屏
        frameLayout.layoutParams.height =
            ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度

        //通过behavior设置具体属性
        behavior.apply {
            isHideable = true          // 允许下拉隐藏,false时折叠态无法继续向下拖动
            skipCollapsed = false      //⭐关键:是否允许跳过折叠态
            //设置展示时的初始状态为折叠态
            state = BottomSheetBehavior.STATE_COLLAPSED

            //⭐关键:设置折叠态的高度为全屏的一半
            peekHeight = ScreenSizeUtil.getAppUsableSize(this@MainActivity).second / 2
            isFitToContents = false //高度不由内容决定

        }

2.全屏态与折叠态切换:

本质上与前面一样,只要折叠高度设置低一点

kotlin 复制代码
//⭐关键:设置可活动面板高度为全屏
        frameLayout.layoutParams.height =
            ScreenSizeUtil.getAppUsableSize(this@MainActivity).second //通过上面工具类获取全屏高度

        //通过behavior设置具体属性
        behavior.apply {
            isHideable = true          // 允许下拉隐藏,false时折叠态无法继续向下拖动
            skipCollapsed = false      //⭐关键:是否允许跳过折叠态
            //设置展示时的初始状态为折叠态
            state = BottomSheetBehavior.STATE_COLLAPSED

            //⭐关键:设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
            //isFitToContents = false //高度不由内容决定
        }

3.半屏态与折叠态切换:

通过设置面板高度为屏幕一半来限制半屏展示,且必须不设置isFitToContents = false才不会拖动面板到全屏

kotlin 复制代码
 //⭐关键:设置可活动面板高度为半屏
        frameLayout.layoutParams.height =
            ScreenSizeUtil.getAppUsableSize(this@MainActivity).second / 2//通过上面工具类获取全屏高度

        //通过behavior设置具体属性
        behavior.apply {
            isHideable = true          // 允许下拉隐藏,false时折叠态无法继续向下拖动
            skipCollapsed = false      //是否允许跳过折叠态
            //⭐关键:设置展示时的初始状态为半屏
            state = BottomSheetBehavior.STATE_HALF_EXPANDED//初始状态为折叠可设置STATE_COLLAPSED

            //⭐关键:设置折叠态的高度
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
            //isFitToContents = false //高度不由内容决定
        }

点击后展开为半屏状态且上下拖动时面板底部的按钮固定不动,当向下拖动高度小于折叠态+按钮高度时才跟随向下

关键点在于对面板高度的设置,通过计算让面板高度在全屏到折叠高度+按钮高度之间拖动时跟随变化为可见区域的高度,这样就可以通过权重来改变recyclerView的高度从而使按钮呈现固定的效果

kotlin 复制代码
创建该文件设置固定dp
res/values/demians.xml:
<resources>
    <dimen name="bottom_view_height">100dp</dimen>
</resources>


BottomSheetDialog设置的完整内容:
    /**
     *  @describe: 设置BottomSheetDialog的相关内容
     *  @params:
     *  @return:
     */
    private fun setBottomSheetDialog() {
        //创建BottomSheetDialog
        //获取dialog布局的view
        val view = LayoutInflater.from(this@MainActivity).inflate(R.layout.dialog_text, null)
        val dialogBtn = view.findViewById<Button>(R.id.dialog_btn)
        //初始化dialog对象
        val bottomSheetDialog = BottomSheetDialog(this@MainActivity)
        //设置dialog布局
        bottomSheetDialog.setContentView(view)
        //获取底部可活动面板的布局FrameLayout
        val frameLayout =
            bottomSheetDialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)!!
        //获取FrameLayout的behavior
        val behavior = BottomSheetBehavior.from<FrameLayout>(frameLayout)

        //⭐关键:设置可活动面板高度为半屏,让底部按钮一开始就显示出来
        frameLayout.layoutParams.height =
            ScreenSizeUtil.getAppUsableSize(this@MainActivity).second / 2 //通过上面工具类获取全屏高度

        //通过behavior设置具体属性
        behavior.apply {
            isHideable = true          // 允许下拉隐藏,false时折叠态无法继续向下拖动
            skipCollapsed = false      //是否允许跳过折叠态
            state = BottomSheetBehavior.STATE_HALF_EXPANDED//初始状态为折叠可设置STATE_COLLAPSED
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_view_height)
            isFitToContents = false //高度不由内容决定
        }
        //可移动的最高高度
        val maxHeight = ScreenSizeUtil.getAppUsableSize(this@MainActivity).second
        //可移动的最低高度
        val minHeight = behavior.peekHeight
        //通过面板的移动监听来动态改变面板的高度
        behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {}

            override fun onSlide(bottomSheet: View, slideOffset: Float) {
                //⭐关键:通过改变高度让按钮固定
                val targetHeight = ((maxHeight - minHeight) * slideOffset + minHeight).toInt()
                //如果希望底部按钮从头到尾都是固定的可以去掉该判断直接 frameLayout.layoutParams.height = targetHeight
                if (targetHeight <= minHeight + dialogBtn.height) //目的是向下拖动到折叠高度+固定按钮高度时可以让按钮此时跟随一起向下
                    frameLayout.layoutParams.height = minHeight + dialogBtn.height
                else
                    frameLayout.layoutParams.height = targetHeight
                bottomSheet.requestLayout() //刷新
            }
        })

        //展示
        bottomSheetDialog.show()
        //设置bottomSheetDialog中recyclerView的相关内容
        setRecyclerView(view)
    }
相关推荐
LeeeX!2 小时前
YOLOv13全面解析与安卓平台NCNN部署实战:超图视觉重塑实时目标检测的精度与效率边界
android·深度学习·yolo·目标检测·边缘计算
dongdeaiziji2 小时前
Android 图片预加载和懒加载策略
android
一起养小猫3 小时前
Flutter for OpenHarmony 实战:科学计算器完整开发指南
android·前端·flutter·游戏·harmonyos
帅得不敢出门3 小时前
Android定位RK编译的system.img比MTK大350M的原因
android·framework·策略模式
darkb1rd3 小时前
三、PHP字符串处理与编码安全
android·安全·php
STCNXPARM13 小时前
Android camera之硬件架构
android·硬件架构·camera
2501_9445255414 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
松☆16 小时前
Dart 核心语法精讲:从空安全到流程控制(3)
android·java·开发语言
_李小白17 小时前
【Android 美颜相机】第二十三天:GPUImageDarkenBlendFilter(变暗混合滤镜)
android·数码相机