为了date-time-picker顺滑,造了个小轮子

使用效果

使用微信的 picker 造出来的 date-time-picker 特别顺滑,轻轻一滑溜,完全不卡顿,也不会跳动,整列就畅快的滚,就像冰面上打滑,根本停不下来。

自从使用了自己写的小轮子,都觉得整个小程序顺畅起来,浑身舒服哈哈哈。

S20324-22300536.png

为什么造小轮子

需要在微信小程序中使用日期时间选择器,而微信官方提供的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年,下层的所有都不需要变动,但是也都进行了各项计算。

更好的方法是,在计算前就先判断是否需要计算,不需要就不动了。

等有时间了再改吧哈哈哈。

组件源码在这儿,需要可以瞅一眼

猜你喜欢

转载自juejin.im/post/7078678083452010526