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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 23:05:31