You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

BottomSheetDialogFragment嵌套RecyclerView的多问题排查需求

嵌套RecyclerView的BottomSheetDialogFragment问题解决

现存问题

  • BottomSheet仅能随RecyclerView上滑抬起,无法回落;
  • 打开BottomSheetDialogFragment后,数据加载期间RecyclerView区域为空,露出底层Fragment;
  • RecyclerView无法滚动到底部,底部Item仅显示一半。

布局代码

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@drawable/rounded_bottom_sheet_dialog" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageButton
            android:id="@+id/bt_back"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginStart="16dp"
            android:background="@color/transparent"
            android:rotation="-90"
            app:layout_constraintBottom_toBottomOf="@+id/tv_title"
            app:layout_constraintStart_toStartOf="parent"
            app:srcCompat="@drawable/slim_arrow"
            app:tint="@color/appBlack" />

        <View
            android:id="@+id/bottom_sheet_line"
            android:layout_width="65dp"
            android:layout_height="3dp"
            android:layout_gravity="center"
            android:layout_marginTop="16dp"
            android:background="@drawable/bt_sheet_line_radius"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Выбрать дату и время"
            android:textColor="@color/black"
            android:textSize="@dimen/_18ssp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_line" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end">

        <TextView
            android:id="@+id/tv_current_month"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:textColor="@color/black"
            android:textSize="@dimen/_14ssp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/tv_next_month"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="НОЯБРЬ" />

        <TextView
            android:id="@+id/tv_next_month"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="24dp"
            android:textColor="@color/grayLight"
            android:textSize="@dimen/_14ssp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="ДЕКАБРЬ" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.kizitonwose.calendar.view.WeekCalendarView
        android:id="@+id/calendar_view"
        android:nestedScrollingEnabled="false"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="15dp"
        app:cv_daySize="seventhWidth"
        app:cv_dayViewResource="@layout/calendar_weekend_res"
        app:cv_orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_current_month" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rc_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="24dp"
            android:background="@color/white"
            android:focusableInTouchMode="true"
            android:nestedScrollingEnabled="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/booking_times_item" />

    </LinearLayout>

    </LinearLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constrainedHeight="true">

        <ProgressBar
            android:id="@+id/progress_bar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/lProceedOrder"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@color/white"
        android:clickable="true"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:paddingBottom="20dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/shadow"
            android:layout_width="match_parent"
            android:layout_height="10dp"
            android:src="@drawable/shadow_gradient"
            android:visibility="visible"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_booking_description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:text="@string/text_insurance"
            android:textColor="@color/appBlack"
            android:textSize="16sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/shadow" />

        <Button
            android:id="@+id/btnBlock"
            style="@style/GreenDeclineButtonBlock"
            android:layout_marginStart="50dp"
            android:layout_marginTop="10dp"
            android:layout_marginEnd="50dp"
            android:visibility="gone"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_booking_description"
            tools:text="@string/hire_block"
            tools:visibility="visible" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

Fragment代码

private val binding by lazy {(FragmentBookingBinding.inflate(layoutInflater))}
private val dateFormatter = DateTimeFormatter.ofPattern("dd")
private var presenter: BookingContracts.Presenter? = null
private var recyclerAdapter: BookingMainAdapter? = null
private var selectedDate = LocalDate.now()
private var vehicleId: Int? = null
private var selectedTimeslots = arrayListOf<Time>()
private var timeslotsList = listOf<TimeSlots>()


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View = binding.root

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    (activity as RentActivity).injectFragment(this, FRAGMENT_TAG)
    vehicleId = arguments?.getInt(VEHICLE_ID_KEY)
    injectFragment()
    initPresenter()
    recyclerAdapter = BookingMainAdapter(object : BookingMainAdapter.Listener {
        override fun timeslotSelect(time: Time, timeslots: List<TimeSlots>, position: Int) {
            selectedTimeslots.add(time)
            timeslots.forEachIndexed { index, vehicleTimeslots ->
                if (index != position) vehicleTimeslots.times.forEach { it.isValid = false }
                else {
                    vehicleTimeslots.times.filter { (it.position != position && it.position != position - 1 && it.position != position + 1) }
                        .forEach {
                            it.isValid = false
                        }
                    if (vehicleTimeslots.times[time.position-1].checked && vehicleTimeslots.times[time.position+1].checked) time.isValid = false
                    if (selectedTimeslots.size == 1) {
                        binding.tvBookingDescription.text = resources.getString(
                            R.string.booking_time,
                            selectedTimeslots[0].time,
                            vehicleTimeslots.times[time.position + 1].time
                        )
                    }
                    if (selectedTimeslots.isNotEmpty() && selectedTimeslots.size > 1) {
                        val starTime = selectedTimeslots.minBy { it.position }.time
                        val endTime = selectedTimeslots.maxBy { it.position }.time
                        binding.tvBookingDescription.text =
                            resources.getString(R.string.booking_time, starTime, endTime)
                    }
                }
            }
        }

        override fun timeslotRemove(time: Time, timeslots: List<TimeSlots>, position: Int) {
            selectedTimeslots.remove(time)
            if (selectedTimeslots.isEmpty()) timeslots.forEach { timeslot -> timeslot.times.forEach { time -> time.isValid = true } }
            else {
                timeslots.filter { it.position == position }.forEach { validTimeslot ->
                    validTimeslot.times.forEach {
                        if (it.position == time.position+1) if (it.isValid) it.isValid = false
                        if (it.position == time.position-1) if (it.isValid) it.isValid = false
                    }
                }
            }
        }

    },
        requireContext())
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.rcView.apply {
        this.layoutManager = LinearLayoutManager(requireContext())
        this.adapter = recyclerAdapter
    }
    vehicleId?.let { presenter?.getTimeSlots(it, selectedDate) }

    dialog?.setOnShowListener { dialog ->
        val d = dialog as BottomSheetDialog
        val bottomSheet = d.findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
        val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
        bottomSheetBehavior.apply {
            state = BottomSheetBehavior.STATE_EXPANDED
            isGestureInsetBottomIgnored = true
            isFitToContents = false
            skipCollapsed = true
            expandedOffset = 250
        }
    }

    class DayViewContainer(view: View) : ViewContainer(view) {
        val bind = CalendarWeekendResBinding.bind(view)
        lateinit var day: WeekDay

        fun bind(day: WeekDay) = with(bind) {
            this@DayViewContainer.day = day
            exSevenDateText.text = dateFormatter.format(day.date)
            exSevenDayText.text = day.date.dayOfWeek.displayText()
            dayBackground.showIf { day.date == selectedDate }
            dayContainer.setOnClickListener {
                dayBackground.showIf { day.date == selectedDate }
                if (selectedDate != day.date) {
                    val oldDate = selectedDate
                    selectedDate = day.date
                    binding.calendarView.notifyDateChanged(day.date)
                    oldDate?.let { binding.calendarView.notifyDateChanged(it) }
                    if (day.date != selectedDate) {
                        selectedDate = day.date
                        vehicleId?.let { vehicleId -> presenter?.getTimeSlots(vehicleId, selectedDate) }
                    }
                }
            }
        }
    }

    binding.calendarView.dayBinder = object : WeekDayBinder<DayViewContainer> {
        override fun create(view: View) = DayViewContainer(view)
        override fun bind(container: DayViewContainer, data: WeekDay) = container.bind(data)
    }

    binding.calendarView.weekScrollListener = { weekDays ->
        binding.tvCurrentMonth.text = getWeekPageTitle(weekDays)
    }

    val currentMonth = YearMonth.now()
    binding.calendarView.setup(
        currentMonth.minusMonths(5).atStartOfMonth(),
        currentMonth.plusMonths(5).atEndOfMonth(),
        firstDayOfWeekFromLocale(),
    )
    binding.calendarView.scrollToDate(LocalDate.now())

    binding.lProceedOrder.showIf { selectedTimeslots.isNotEmpty()
    }
}

private fun injectFragment() {
    activity.let { (it as BaseActivity).injectFragment(this, SubscriptionMainFragment.FRAGMENT_TAG) }
}

private fun initPresenter() {
    presenter = BookingPresenter(vehiclesInteractor).apply {
        attachView(this@BookingFragment)
        onRestoreInstance(null)
    }
}

companion object {

    val FRAGMENT_TAG = BookingFragment::javaClass.name
    const val VEHICLE_ID_KEY = "vehicle_ID"

    fun newInstance(bundle: Bundle): BookingFragment {
        val fragment = BookingFragment()
        fragment.arguments = bundle
        return fragment
    }
}

override fun showTimeslots(timeslots: List<TimeSlots>) {
    recyclerAdapter?.submitList(timeslots)
    timeslotsList = timeslots
    binding.progressBar.hide()
}

问题截图

RecyclerView项收起时的显示状态


解决方案

1. 修复BottomSheet无法回落问题

  • 移除skipCollapsed = true,允许BottomSheet进入收起状态;
  • 给RecyclerView添加滚动监听,联动BottomSheetBehavior:
// 在onViewCreated中添加
binding.rcView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
        val bottomSheet = (dialog as BottomSheetDialog).findViewById<FrameLayout>(R.id.design_bottom_sheet) ?: return
        val behavior = BottomSheetBehavior.from(bottomSheet)

        // 滚动到顶部且向下滑时,收起BottomSheet
        if (firstVisibleItem == 0 && dy < 0) {
            behavior.state = BottomSheetBehavior.STATE_COLLAPSED
        }
        // 滚动时如果BottomSheet是收起状态,展开它
        else if (firstVisibleItem != 0 && behavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
            behavior.state = BottomSheetBehavior.STATE_EXPANDED
        }
    }
})

// 调整BottomSheetBehavior初始化
bottomSheetBehavior.apply {
    state = BottomSheetBehavior.STATE_EXPANDED
    isGestureInsetBottomIgnored = true
    isFitToContents = true // 让BottomSheet适配内容高度
    skipCollapsed = false // 允许收起
    expandedOffset = 0 // 根据需求调整,确保足够显示空间
}

2. 修复加载期间露出底层Fragment问题

  • 初始隐藏RecyclerView,显示ProgressBar;数据加载完成后切换状态:
// onViewCreated中添加
binding.rcView.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE

// 修改showTimeslots方法
override fun showTimeslots(timeslots: List<TimeSlots>) {
    recyclerAdapter?.submitList(timeslots)
    timeslotsList = timeslots
    binding.progressBar.visibility = View.GONE
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 23:55:19