使用效果
使用微信的 picker 造出来的 date-time-picker 特别顺滑,轻轻一滑溜,完全不卡顿,也不会跳动,整列就畅快的滚,就像冰面上打滑,根本停不下来。
自从使用了自己写的小轮子,都觉得整个小程序顺畅起来,浑身舒服哈哈哈。
为什么造小轮子
需要在微信小程序中使用日期时间选择器,而微信官方提供的picker只有普通/多列/时间/日期/省市区五种类型选择器。
因为在小程序中引用了vant库,所以自然而然的从中找到了时间选择组件(DatetimePicker)。但在真机的使用过程中,发现在滚动列选项时常常会出现卡顿、跳选项的问题。
上网冲浪发现,很多人在实际使用中都遇到过这问题,也提出了各种各样的方法,企图困住DatetimePicker的自由灵魂。我也跟着操作了一番,但也许是我的三年前的安卓机不配吧,组件还是有自己的想法,跳来跳去。
就在我又一次小心翼翼的滚动选了35分,它又自己慢悠悠的跳动成43分时,我暴躁的直接关闭了这个小程序,并且决定用官方picker自己搞一个日期时间选择器。
解决方案
使用微信小程序原生picker的多列选择器类型封装为date-time-picker组件。
想象中的麻烦点: 1、特殊日问题 2、时间边界问题
实践过程:
一、基础
1、引用组件时,传入三个时间戳(minTime、maxTime、defaultTime),对时间戳关系进行校验。 需要满足 minTime<=defaultTime<=maxTime 的关系,才能正经的进行其他操作。
//校验已知条件
checkCondition() {
let minTime = this.data.minTime
let maxTime = this.data.maxTime
let currentTime = this.data.defaultTime
if (maxTime - minTime < 0) {
console.error('可选范围参数有误')
} else if (currentTime - minTime < 0 || currentTime - maxTime > 0) {
console.error('默认时间不在可选范围内')
} else {
this.getCondition()
}
}
复制代码
2、需要获取到基础时间戳的具体数据情况(y-m-d-h-i),并且在列表中展示时需要补零。
// 获取时间详情
getTimeInfo(stamp) {
let time = new Date(stamp)
let obj = {
y: time.getFullYear() + '',
m: this.setZero(time.getMonth() + 1),
d: this.setZero(time.getDate()),
h: this.setZero(time.getHours()),
i: this.setZero(time.getMinutes())
}
return obj
},
// 补零
setZero(value) {
return Number(value) < 10 ? '0' + value : String(value)
}
复制代码
3、自定义defaultInfo,把生活中的时间情况表示出来方便使用。 如范围情况(月1-12、时0-23、分0-59)、大月时间(1, 3, 5, 7, 8, 10, 12)、天数情况(分大小月和闰平年)
defaultInfo = {
// 默认极限值
limitInfo: {
minM: 1,
maxM: 12,
minD: 1,
maxD: 31,
minH: 0,
maxH: 23,
minI: 0,
maxI: 59
},
// 大月
bigDaysMonth: [1, 3, 5, 7, 8, 10, 12],
// 天数
daysInfo: {
isBigMonth: 31,
isLitMonth: 30,
isLeapYearFeb: 29,
isCommonYearFeb: 28
}
}
复制代码
4、日列表最特殊,关系到年和月的情况,所以把它单独提取出来计算。
// 处理天数
getDays(year, month) {
let y = Number(year)
let m = Number(month)
let bigDaysMonth = defaultInfo.bigDaysMonth
let daysInfo = defaultInfo.daysInfo
let days = 0
if (m === 2) {
// 2月 闰年29天 平年28天
days = (y % 4 == 0 && y % 100 !== 0 || y % 400 == 0) ? daysInfo.isLeapYearFeb : daysInfo.isCommonYearFeb
} else if (bigDaysMonth.includes(m)) {
// 大月31天
days = daysInfo.isBigMonth
} else {
// 小月30天
days = daysInfo.isLitMonth
}
return days
},
复制代码
二、设置多列选择器的各个列
在picker滚动过程中,两端的边界位置(minTime、maxTime)很关键,比如原来的月可选项为1-12,但如果当前在边界时间2022-3-14至2022-11-25,则2022年的月列表最小值应为3而非1,最大值应为11而非12。
依据上述情况,分析发现上层是否在边界、自己是否在边界变得尤为重要。(所谓的上层,指的是根据y-m-d-h-i,如天的上层就为年和月)
在实现过程中,我选择了 setYear->setMonth->setDay->setHour->setMin 的处理方法。各个方法有相同的思路:
1、判断当前项是否在limit
// 设置当前值与极限值
getInfo(key, val) {
currentInfo[key] = val
let value = Number(val)
let min = Number(minInfo[key])
let max = Number(maxInfo[key])
return {
min: min,
max: max,
isMin: value === min,
isMax: value === max
}
}
复制代码
2、获取当前项的所有上层是否为limit
3、根据以上两点确定当前列的取值范围,定scope
// 设定范围及当前值位置
let scopeMin = upperLimit.isMin ? info.min : limitInfo.minM
let scopeMax = upperLimit.isMax ? info.max : limitInfo.maxM
let scope = this.getScopeArr(scopeMin, scopeMax)
let index = scope.indexOf(value)
复制代码
4、计算当前项是否在scope内,如果不在,当前项变化,大则取大、小则取小。获得新index后,重新判断当前项是否在scope边界,并保存limit的状态。
// 范围变化导致当前值不在范围内,则小取最小,大取最大,并校验极限值
checkInScope(key, index, value, scope, min, max, isMin, isMax) {
let newIsMin = isMin
let newIsMax = isMax
let newIndex = index
if (index === -1) {
newIndex = Number(value) < Number(scope[0]) ? 0 : scope.length - 1
currentInfo[key] = scope[newIndex]
newIsMin = Number(scope[newIndex]) === min
newIsMax = Number(scope[newIndex]) === max
}
return {
index: newIndex,
isMin: newIsMin,
isMax: newIsMax
}
}
复制代码
通过以上操作,就可以定下五列的展示内容。
写在最后
这个写法其实不是最优写法,因为当一列滚动时,背后就要去计算下层,哪怕下层本来不需要改变。举例,如边界是2016-3-21 17:32 至 2025-12-14 07:12,当前是2022-3-14 17:32,我滚动首列到2023年,下层的所有都不需要变动,但是也都进行了各项计算。
更好的方法是,在计算前就先判断是否需要计算,不需要就不动了。
等有时间了再改吧哈哈哈。