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() }
问题截图

解决方案
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
相关产品推荐
相关产品推荐

