以前写了一篇Calendar -『为移动端而生』的自定义日历,一直有童鞋对这个插件的手势处理存在一些问题,因此想写篇文章,来讲说它的成长史~css
在阅读本文以前,确保你有稍微看过 calendar 的效果 喔~git
点击查看github, 查看calendar源码 github
也能够在 NPM上搜索 mob-calendar 找到它。ajax
想作一个日历最主要的缘由,固然仍是由于在开发过程当中频繁的遇到。并且对日历的需求又是奇葩到不行,市面上的插件都知足不了咱们产品的需求。因此,我不得不动手本身造。算法
这段话,好像在造 上一个插件 - 级联选择器 的时候也说过
你们就当无事发生过(⁎⁍̴̛ᴗ⁍̴̛⁎)npm
首要问题依然是处理需求:segmentfault
用户不肯定本身要选择的时间点或时间范围,须要一些基本的时间参照单位,好比“下星期一”、“下个周末”。api
用户须要查看某个时间区间,以后再有选择性的选取时间点或时间范围,好比“尽量避开周末的20天翘班请假计划”。数组
用户须要查看某个时间区间的行为记录,好比“查看过去几周的打卡状况”浏览器
当出现以上问题的时候,日历的时间定位优点就显示出来了。
日历存在着点击事件,点击事件是 跳转事件 仍是 高亮事件 没法预知。
日历存在着选取操做,选取的结果是 时间点 仍是 时间范围 没法预知。
日历有多种展示形式,是直接 文档流显示 仍是 弹层显示 没法预知。
针对这些不稳定因素,接下来,会带你一步步解决。
肯定了日历的需求,就来设计一下构造函数的参数吧~
从如今市面上的常见的app上看,咱们会发现,日历常见的展示形式有两种:
普通文档流形式
弹层形式
在参数的设置中,表现为设置isMask,false:普通形式,true:弹层形式。

1. 让开发人员更方便地定位日期
①:在肯定时间范围的时候,使用一个 length 为 3 的数组,数组的每一位分别对应【年】【月】【日】
好比beginTime
、endTime
和recentTime
的设定②:在对特定日期指定样式或操做的时候,使用该日期的时间戳。
好比设置beforeRenderArr
的时候,须要传入一个符合规范的对象数组
参数 | 类型 | 举例 | 说明 |
---|---|---|---|
stamp | {Number} | eg:1514822400000 | 指定一个特定的时间戳 |
className | {String} | eg: "enable" | 指定一个用户本身设置的css的类名 |
2. 灵活控制星期的排列、星期的显示格式、月份的显示格式
①:
isSundayFirst
控制星期日是否要放在第一列,true为星期日放第一列②:
isChinese
控制星期的显示方式,true为显示中文,false为显示英文③:
monthType
控制月份的显示格式,以一月份为例,0: 1月, 1: 一月, 2:Jan, 3: January
3. 对最重要的滑动手势作一些配置
①:
angle
控制滑动的角度,间接控制灵敏度,建议取值范围5-20②:
isToggleBtn
是否须要展现切换按钮, true为须要展现③:
canViewDisabled
是否能够查询不在规定范围内的月份,true为能够查询
4. 可供开发者自定义的灵活的回调函数
①:
success
点击某个日期以后的回调,用户自定义点击后的操做。自带参数(item, arr)
。item
为当前点击的时间戳,arr
为智能判断后的连续两次点击的两个时间戳的数组②:
switchRender
切换月份时的回调,用户自定义切换后须要进行的操做,如发起请求更新数据等。自带参数(year, month, cal)
。year
为新生成的年份,month
为新生成的月份(从0开始),cal
指向当前实例
名称 | 传入参数的类型 | 做用 |
---|---|---|
renderCallbackArr(arr) | {Array} | 渲染指定的arr,arr的格式和beforeRenderArr 的对象数组的格式同样 |
prevent() | - | 在微信浏览器中,你可能须要用到的阻止默认事件的api |
hideBackground() | - | 在弹层模式的success 回调中,你可能须要用到的关闭弹层的api |
适当解释一下api的用意:
1.向renderCallbackArr
中传入一个数组,(数组格式和beforeRenderArr
同样,再也不说明),这个方法可以往你须要的时间点上添加指定样式。设想一种场景:
经过滑动切换,查看三个月前的打卡状况,已打卡和未打卡的日期都有不一样的高亮样式。
显然,这个月的打卡状况是须要你在
switchRender
回调中发起http请求后获得。在http返回结果后,构造一个符合
beforeRenderArr
格式的数组,而后调用renderCallbackArr
,传入构造好的数组,就能对指定的日期渲染指定的className了。
// 举个栗子? switchRender: function(year, month,cal) { console.log('计算机识别的: 年份: ' + year + ' 月份: ' + month); $.ajax({ url: 'xxxx', type: 'GET', data: { applyYear: year, applyMonth: (month + 1), }, success: function(newArr) { cal.renderCallbackArr(newArr); } }) }
2. 使用prevent()
的场景应该不会太多。主要是为了阻止微信浏览器的默认滑动。
// 这是prevent 方法的源码 prevent: function (e) { e.preventDefault(); },
3.使用hideBackground()
的场景通常是在弹层模式的success
回调中。设想一种场景:
触发了日历弹层以后,若是你只想【选择一个时间点】,那么点击某个日期以后就能够直接调用
hideBackground()
收起弹层。若是你想【选择某个时间区间】,那么能够在第二个时间点肯定以后再调用
hideBackground()
收起弹层。固然,也能够不收起弹层。
其实我在写第一个版本的日历的时候,采起的解决办法是当新的月份产生以后,往body中不断append dom。不过当时的业务的场景比较简单,撑死也只有10个月。可是显然若是有100个月,我这样的作法明显不行。
因此必需要让dom能够复用,实现无限滑动
首先明确,这里指的一个dom就是一个月份,每次切换月份就是切换包裹着月份的dom
以下图,假设当前月份为【2017年9月】,因为滑动是实时的,当个人手指从右向左滑的过程,【2017年10月】也会渐渐的露出来一些,考虑一种特殊状况:
以打卡为例,2017年10月是有打卡记录的,若是等使用者松开手指,停在2017年10月的时候忽然闪现出打卡记录的高亮样式,会给使用者很不温馨的感受。
为避免这种状况,就须要在当前月份为【2017年9月】的时候,就已经渲染好【2017年10月】的高亮样式了,左边的【2017年8月】也是同理,因此至少必需要渲染出完整的、带有数据高亮的三个月
因此咱们获得告终论,月份的dom至少为3个,而且这三个dom是已经连高亮样式都渲染好,不会在实时滑动结束后有任何变更的。
可是为何最后是要用5个dom来实现无限滑动呢?
参考一下swiper的效果,为了能让这三个dom两边的极端dom也可以正常的实时滑动。因此在头尾分别加一个dom,因此一共须要5个dom来实现无限滑动。
以下图,绿色线框的部分为最初开始分析的3个dom。

直接参考一下swiper的效果就可以获得答案,我如今举一个实例来作一些说明:
先考虑如下状况:
手势操做:连续从右向左滑
操做结果:连续查看下个月
如下是图例,红色箭头的更新操做:

以当前进入页面的初始月份是2017年9月为例:
初始状态:

紫色的数字是表明月份dom的下标,相同下标对应的月份也相同。
中间的一、二、3对应的是以前说过的 -----【至少要提早渲染好3个月份的dom】。
那首尾填充的月份为何是 3 和 1 呢?
假设咱们如今不限制5个dom,而是无限个dom,那么表明月份dom的下标组合就会是:
一、二、三、一、二、三、一、二、三、一、二、3......
咱们以一个一、二、3为中心,取到连续的5个月份dom,那么取到的下标组合就是:
一、二、【三、一、二、三、1】、二、三、一、二、3......
没懂不要紧,看下去就会明白。
实际上,将来,我会须要取到dom的下标进行更新月份数据的操做,因此我试图发现【三、一、二、三、1】这个下标数组中的规律。
我发现这个下标循环是3的循环,我能够经过取3的模的方式取到每一个位置上的dom下标。
如今我要对这个下标作一点小的改动。
我要把3改为0。即【0、一、二、0、1】
缘由很简单,是为了在计算滑动距离的时候,将 dom下标 和 translateX 对应起来比较方便。即当滑到最左侧的月份dom的时候,月份的dom的translateX
的值为0,能够和下标 0 % 3 的结果相对应。
这样,这个下标,就和translateX
直接联系起来了。
好,以初始月份是2017年9月为例,最终初始化的结果为:


接下来,从右向左滑,查看下一个月份,touchend
以后,操做以下:

当滑到了最右边的月份的dom的时候(其实只要滑到边界都作同样的处理),在touchstart
中执行一个特殊操做:
就是在touchstart的时候,瞬间translate3d
到和它dom下标同样的月份去:
好比上面【2017.11】已经到最右边的,那在我下次滑动的touchstart
的时候定位到下图的位置中:


这就是实现无限滑动的核心原理。固然还能够接着一直滑:


以此类推,无限左滑也是相似的道理。
从上面讲述无限滑动的原理中,你能够大概感受到: 滑动的距离是经过控制中间的灰色矩形相对于手机屏幕的translateX
来决定的。
如何控制translateX
的值实现滑动效果,这个问题不是此次的重点。
假设下图中的蓝色曲线表明用户的滑动曲线:
当用户的滑动曲线是A的状况时,用户的意图明显是想把页面往上拉
当用户的滑动曲线是B的状况时,用户的意图明显是想查看上一个月
可实际上,若是只经过控制translateX
的值实现滑动效果的时候,不管是曲线A或者B都会被认为是想查看上一个月

也就是说,若是控制了translateX
,那么,在这个占据着文档流巨大的面积的dom范围内,永远没法上下滑动。这是万万不被容许的。
因此咱们须要预判手势,来实如今日历的dom范围内,既可以上下滑动,又可以左右滑动。效果以下:
好比以前提到的【滑动曲线A和B】的示例图,若是以绿线为标准,
斜率小于绿线的曲线,都归为和滑动曲线B同样的左右滑动
斜率大于绿线的曲线,都归为和滑动曲线A同样的上下滑动
这样不就能够了吗?
但其实用户的手势曲线通常都是下面的橙色曲线....

并且计算用户手势的斜率必定是在touchmove
中实时计算(为何?固然是为了实时滑动),因此最后,靠斜率预判用户手势的思路,就到这里结束了。
用户的手势其实是一条弧线,当前只考虑从左下角向右上角滑的状况,就能把用户的手势曲线简化在第一象限中。
以下图,咱们从微积分的概念出发,获得如下结论。

先看看中间的红色矩形部分,这个红色矩形是把某个细长条矩形夸张的放大后的矩形,其宽为△X,其高为△Y。
经过touchmove
实时计算每一次滑动的△X 和 △Y,而后累加面积。面积的累加实际上直接按照△X × △Y
的结果正负进行累加,这样就把第一象限的手势推广到全部象限的手势中去了。
计算手势的核心代码以下,其中cal指向当前实例:


咱们能够利用用户手势的曲线面积来把用户手势操做量化。
但量化是量化了,要如何知道我量化的结果是上下滑动仍是左右滑动呢?
因此就须要像计算斜率时的标准线(那条绿线)同样,必须有一个标准面积。
以下图,咱们有三条曲线,这三条曲线与X轴围起来的面积,就是咱们前面辛辛苦苦量化的结果。其中:
蓝色的曲线围成的面积就是咱们理想中的标准面积,虽然还不知道怎么算
黄色的曲线围成的面积比标准面积大,咱们将断定全部大于蓝色曲线的量化曲线为【用户试图上下滑动】
绿色的曲线围成的面积比标准面积小,咱们将断定全部小于蓝色曲线的量化曲线为【用户试图左右滑动】

问题回到了,如何计算标准面积?
观察上图能够发现有一个明显的蓝色的角A,这个角A和实例化的参数angle是同一个东西。
开发者能够经过控制angle的值(angle的单位是°)来控制标准面积的大小。
固然经过个人测试,angle的取值在 [5 , 20]最佳。
那源码中是如何经过开发者传入的angle进行标准面积的计算的呢?
首先,我会将用户的角度转化为tan值。


为何须要tan值呢,由于我就能够根据△X
计算获得 △Y = △X * tanA
。


因此标准面积也能经过累加获得了。


至此,咱们就能够经过用户手势的面积和标准面积的比较来获得一个比较理想的预判。
经过预判,让用户在页面的任何地方滑动,都感到温馨。
Github地址:『为移动端而生』的自定义日历插件 https://github.com/AppianZ/calendar
欢迎你们提出宝贵建议和技术交流 ٩(•̤̀ᵕ•̤́๑)
我是嘉宝Appian,一个卖萌出家的算法妹纸(❁ᴗ͈ˬᴗ͈)