DatePickerDialog更新selectableDays时重置回默认月份问题求解
问题描述
使用com.wdullaer.materialdatetimepicker.date包下的DatePickerDialog组件时,通过API接口填充selectableDays可选日期数据。由于对接的API单次仅支持查询单月的可选日期,因此需要在用户点击月份切换箭头、跳转至下一月时,拉取对应月份的可选日期补充到可选日期列表中。

当前实现方案是维护一个全局Calendar集合,将接口返回的新增可选日期追加到该集合中:
val selectableCalendars = HashSet<Calendar>() // 存储可预约日期(含时间)的集合
该方案基础运行逻辑正常,但当DatePickerDialog处于打开状态时,执行如下代码更新可选日期列表:
datePickerDialog.selectableDays = selectableCalendars.toTypedArray()
DatePickerDialog会直接跳转回日历列表中的第一个月份。
这种“跳转回最早可选日期所在月份”的非预期行为仅在同时满足以下两个条件时触发:
- 刚完成
datePickerDialog.selectableDays的新值赋值操作后立即触发;- 用户滑动进入的目标月份没有可选日期(若返回的该月存在可选日期则不会触发该bug)。
可行临时解决方案
该问题是组件内部固有逻辑导致的:setSelectableDays方法执行时会自动校验当前显示月份是否存在可选日期,若不存在则直接定位到可选日期数组中时间最早的月份,不会保留用户当前停留的月份状态。以下是3种不需要修改库源码即可落地的方案,适配4.2.3稳定版:
- 预占位方案(体验最优,无闪屏)
拉取目标月份接口数据前,先记录用户当前停留的年、月值,往selectableCalendars集合中临时添加一个对应当月1号的Calendar对象(注意将时分秒、毫秒值清零,和集合内其他Calendar的时间精度对齐),避免组件判定当前月无可用日期触发跳转。等接口返回真实数据后,移除占位对象、追加真实返回的日期,再重新给selectableDays赋值即可。
配套加点击拦截:给对话框设置setOnDateClickListener,如果用户点击的日期和占位日期年月日完全一致,直接返回false禁止选中即可,用户完全感知不到占位数据的存在。
核心实现代码:
如果接口返回当月确实没有可选日期,可以选择保留占位,在点击拦截里给用户弹个“当月无可预约日期”的提示即可,不会出现跳月问题。// 月份切换触发时,先获取当前停留的年月 val curYear = datePickerDialog.year val curMonth = datePickerDialog.month // 生成当月1号的占位日期 val placeholder = Calendar.getInstance().apply { set(curYear, curMonth, 1, 0, 0, 0) set(Calendar.MILLISECOND, 0) } // 先加入占位,更新可选列表 selectableCalendars.add(placeholder) datePickerDialog.selectableDays = selectableCalendars.toTypedArray() // 发起接口请求拉取当月真实可选日期 repo.fetchAvailableDates(curYear, curMonth) { realDates -> // 移除占位,追加真实数据 selectableCalendars.remove(placeholder) selectableCalendars.addAll(realDates) datePickerDialog.selectableDays = selectableCalendars.toTypedArray() } - 赋值后主动重置月份(实现最简单)
如果不想维护占位数据,可以在赋值新的可选日期列表前,先记录当前停留的年、月、日,赋值操作完成后,通过post方法把切回原月份的操作投递到主线程消息队列尾部,等组件内部跳首月的逻辑执行完后,再把显示状态切回用户之前停留的月份即可。
核心实现代码:
该方案的唯一缺陷是部分低性能机型上可能出现极短暂的首月闪屏,对体验要求不高的场景可以直接使用。// 记录当前停留位置 val targetYear = datePickerDialog.year val targetMonth = datePickerDialog.month val targetDay = datePickerDialog.dayOfMonth // 更新可选日期 datePickerDialog.selectableDays = selectableCalendars.toTypedArray() // 等组件内部逻辑执行完再切回原位置 datePickerDialog.post { datePickerDialog.updateDate(targetYear, targetMonth, targetDay.coerceAtLeast(1)) } - 反射拦截内部跳转逻辑(无侵入无副作用)
如果前两种方案都无法满足需求,可以通过反射拿到DatePickerDialog内部的mController实例,拦截其自动跳转至首个可选日期的逻辑。该方案体验和原生一致,缺点是不同版本的库内部字段名可能有变动,升级库版本时需要做兼容校验。
内容的提问来源于stack exchange,提问作者Alan Nelson
相关产品推荐
相关产品推荐

