如何用RecyclerView实现预订应用的连续日期选择日历?
实现日历连续日期选择功能
我正在开发一款预订应用的日历视图,需要向用户展示已被占用的日期,同时实现连续日期选择功能(日历日期选择器展示连续选中的日期,起始和结束日期带圆角,中间日期为矩形填充)。目前我基于java.util.Calendar作为数据源创建了RecyclerView,每个日期对应一个ViewHolder,现有代码仅支持单个日期选择,希望改成连续选择功能。我不想放弃RecyclerView改用自定义视图,确认RecyclerView可以实现该需求。
现有Adapter代码:
class CalendarAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var list = emptyArray<CalendarItem>() override fun getItemViewType(position: Int): Int { return list[position].viewType?.asInt ?: super.getItemViewType(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { ViewType.CURRENT_DAY.asInt -> { val binding = ViewCurrentDayBinding.inflate(inflater, parent, false) CurrentDayHolder(binding) } ViewType.DAY_OF_MONTH.asInt -> { val binding = ViewDayOfMonthBinding.inflate(inflater, parent, false) DayOfMonthHolder(binding) } ViewType.DAY_OF_WEEK.asInt -> { val binding = ViewDayOfWeekBinding.inflate(inflater, parent, false) DayOfWeekHolder(binding) } ViewType.SELECTED_DAY.asInt -> { val binding = ViewSelectedDayBinding.inflate(inflater, parent, false) SelectedDayHolder(binding) } ViewType.MOCK.asInt -> { val binding = ViewMockDayBinding.inflate(inflater, parent, false) MockDayHolder(binding) } else -> { val binding = ViewMockDayBinding.inflate(inflater, parent, false) MockDayHolder(binding) } } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (getItemViewType(position)) { ViewType.CURRENT_DAY.asInt -> { (holder as CurrentDayHolder).bind(list[position], position, callback) } ViewType.DAY_OF_MONTH.asInt -> { (holder as DayOfMonthHolder).bind(list[position], position, callback) } ViewType.DAY_OF_WEEK.asInt -> { (holder as DayOfWeekHolder).bind(list[position]) } ViewType.SELECTED_DAY.asInt -> { (holder as SelectedDayHolder).bind(list[position], position, callback) } ViewType.MOCK.asInt -> { (holder as MockDayHolder).bind(list[position]) } } } override fun getItemCount(): Int { return list.size } private val callback: (index: Int) -> Unit = { // 查找当前选中的索引并取消选择 val currentSelectedIndex = list.indices.find { el -> list[el].viewType == ViewType.SELECTED_DAY } if (currentSelectedIndex != null) { list[currentSelectedIndex].viewType = list[currentSelectedIndex].defaultViewType notifyItemChanged(currentSelectedIndex) } // 选中新的索引 list[it].viewType = ViewType.SELECTED_DAY notifyItemChanged(it) } }
ViewHolder示例:
class CurrentDayHolder(var binding: ViewCurrentDayBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( calendarItem: CalendarItem, index: Int, callback: (index: Int) -> Unit ) { binding.day.text = calendarItem.label binding.root.setOnClickListener { callback.invoke(index) } } }
解决方案
1. 调整数据模型(CalendarItem)
扩展CalendarItem,添加连续选择所需的状态标记,替换原有的单一选中状态:
data class CalendarItem( val label: String, val defaultViewType: ViewType, var isSelected: Boolean = false, var isRangeStart: Boolean = false, var isRangeEnd: Boolean = false, var isInRange: Boolean = false, val isOccupied: Boolean = false // 标记日期是否已被占用 )
2. 修改Adapter的选择逻辑
移除原有的SELECTED_DAY视图类型,改为通过状态标记控制UI样式,同时更新点击回调支持连续选择:
class CalendarAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var list = emptyArray<CalendarItem>() private var startSelectedIndex: Int? = null private var endSelectedIndex: Int? = null override fun getItemViewType(position: Int): Int { val item = list[position] return when { item.isOccupied -> ViewType.OCCUPIED_DAY.asInt // 新增已占用日期类型 item.isRangeStart || item.isRangeEnd || item.isInRange -> ViewType.SELECTED_RANGE.asInt item.defaultViewType == ViewType.CURRENT_DAY -> ViewType.CURRENT_DAY.asInt else -> item.defaultViewType.asInt } } // onCreateViewHolder 新增 OCCUPIED_DAY 和 SELECTED_RANGE 的视图创建逻辑,参考原有类型实现即可 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = list[position] when (getItemViewType(position)) { ViewType.CURRENT_DAY.asInt -> { (holder as CurrentDayHolder).bind(item, position, selectionCallback) } ViewType.DAY_OF_MONTH.asInt -> { (holder as DayOfMonthHolder).bind(item, position, selectionCallback) } ViewType.SELECTED_RANGE.asInt -> { (holder as SelectedRangeHolder).bind(item) } ViewType.OCCUPIED_DAY.asInt -> { (holder as OccupiedDayHolder).bind(item) } // 其他类型绑定逻辑保持不变 } } private val selectionCallback: (index: Int) -> Unit = { clickedIndex -> val clickedItem = list[clickedIndex] if (clickedItem.isOccupied) return@selectionCallback // 禁止选择已占用日期 when { // 第一次点击,设置起始点 startSelectedIndex == null -> { startSelectedIndex = clickedIndex updateRangeStates() } // 点击起始点,取消选择 startSelectedIndex == clickedIndex -> { clearSelection() } // 已有起始点,设置结束点(兼容起始点在结束点前后的情况) else -> { endSelectedIndex = clickedIndex updateRangeStates() } } } // 更新范围内所有日期的状态 private fun updateRangeStates() { val start = startSelectedIndex ?: return val end = endSelectedIndex ?: start val min = minOf(start, end) val max = maxOf(start, end) // 先清空所有选中状态 list.forEachIndexed { index, item -> if (item.isSelected || item.isInRange || item.isRangeStart || item.isRangeEnd) { item.isSelected = false item.isInRange = false item.isRangeStart = false item.isRangeEnd = false notifyItemChanged(index) } } // 设置范围内的状态 for (i in min..max) { val item = list[i] when (i) { min -> { item.isRangeStart = true item.isSelected = true } max -> { item.isRangeEnd = true item.isSelected = true } else -> { item.isInRange = true } } notifyItemChanged(i) } } // 清空所有选择状态 private fun clearSelection() { startSelectedIndex?.let { start -> val end = endSelectedIndex ?: start val min = minOf(start, end) val max = maxOf(start, end) for (i in min..max) { val item = list[i] item.isSelected = false item.isInRange = false item.isRangeStart = false item.isRangeEnd = false notifyItemChanged(i) } } startSelectedIndex = null endSelectedIndex = null } // 其他方法保持不变 }
3. 适配ViewHolder的UI样式
创建SelectedRangeHolder,根据状态设置不同的背景样式:
class SelectedRangeHolder(var binding: ViewSelectedRangeBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(calendarItem: CalendarItem) { binding.day.text = calendarItem.label when { calendarItem.isRangeStart && calendarItem.isRangeEnd -> { // 单个选中日期,全圆角 binding.root.background = ContextCompat.getDrawable(binding.root.context, R.drawable.bg_selected_full_round) } calendarItem.isRangeStart -> { // 起始日期,左圆角 binding.root.background = ContextCompat.getDrawable(binding.root.context, R.drawable.bg_selected_left_round) } calendarItem.isRangeEnd -> { // 结束日期,右圆角 binding.root.background = ContextCompat.getDrawable(binding.root.context, R.drawable.bg_selected_right_round) } calendarItem.isInRange -> { // 中间日期,无圆角 binding.root.background = ContextCompat.getDrawable(binding.root.context, R.drawable.bg_selected_middle) } } } }
4. 处理已占用日期的交互
创建OccupiedDayHolder,展示禁用样式并禁止点击:
class OccupiedDayHolder(var binding: ViewOccupiedDayBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(calendarItem: CalendarItem) { binding.day.text = calendarItem.label binding.root.isClickable = false // 禁止点击 // 设置已占用样式,比如灰色背景加斜线 } }
内容的提问来源于stack exchange,提问作者stefanodp91
相关产品推荐
相关产品推荐

