Jst502

Jst502 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 9notes.cn 编辑
编辑

细雨带风湿透黄昏的街道

个人动态

Jst502 赞了文章 · 2017-07-03

如何造一个『为移动端而生』的日历

之前写了一篇Calendar -『为移动端而生』的自定义日历,一直有童鞋对这个插件的手势处理存在一些问题,所以想写篇文章,来说说它的成长史~

在阅读本文之前,确保你有稍微看过 calendar 的效果 喔~

gif0.

一、 确认需求

想做一个日历最主要的原因,当然还是因为在开发过程中频繁的遇到。而且对日历的需求又是奇葩到不行,市面上的插件都满足不了我们产品的需求。所以,我不得不动手自己造。

这段话,好像在造 上一个插件 - 级联选择器 的时候也说过
大家就当无事发生过(⁎⁍̴̛ᴗ⁍̴̛⁎)

首要问题依然是处理需求:

第1个问题:『日历的出现场景有哪些特点?』

  1. 用户不确定自己要选择的时间点或时间范围,需要一些基本的时间参照单位,比如“下星期一”、“下个周末”。

  2. 用户需要查看某个时间区间,之后再有选择性的选取时间点或时间范围,比如“尽可能避开周末的20天翘班请假计划”。

  3. 用户需要查看某个时间区间的行为记录,比如“查看过去几周的打卡情况

当出现以上问题的时候,日历的时间定位优势就显示出来了。

第2个问题:『日历会有哪些奇葩需求?』

  1. 日历存在着点击事件,点击事件是 跳转事件 还是 高亮事件 无法预知。

  2. 日历存在着选取操作,选取的结果是 时间点 还是 时间范围 无法预知。

  3. 日历有多种展现形式,是直接 文档流显示 还是 弹层显示 无法预知。

针对这些不稳定因素,接下来,会带你一步步解决。

二、构造函数的参数设计

确定了日历的需求,就来设计一下构造函数的参数吧~

第3个问题:『日历有哪些常见的展现形式?』

从现在市面上的常见的app上看,我们会发现,日历常见的展现形式有两种:

  1. 普通文档流形式

  2. 弹层形式

在参数的设置中,表现为设置isMask,false:普通形式,true:弹层形式。

png1

第4个问题:『参数要怎么灵活和高效地设置?』

1. 让开发人员更方便地定位日期

①:在确定时间范围的时候,使用一个 length 为 3 的数组,数组的每一位分别对应【年】【月】【日】
比如beginTimeendTimerecentTime的设定

②:在对特定日期指定样式或操作的时候,使用该日期的时间戳。

比如设置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指向当前实例

三、暴露在原型上的、可使用的api

名称传入参数的类型作用
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()收起弹层。当然,也可以不收起弹层。

四、如何利用五个DOM做到无限滑动

其实我在写第一个版本的日历的时候,采取的解决办法是当新的月份产生之后,往body中不断append dom。不过当时的业务的场景比较简单,撑死也只有10个月。但是显然如果有100个月,我这样的做法明显不行。

所以必须要让dom可以复用,实现无限滑动

思考第5个问题:『无限滑动的话至少需要几个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。

png1

思考第6个问题:『头尾两个作为填充的dom要显示哪个月呢?』

直接参考一下swiper的效果就能够得到答案,我现在举一个实例来做一些说明:
先考虑以下情况:

手势操作:连续从右向左滑
操作结果:连续查看下个月

以下是图例,红色箭头的更新操作:

png1

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

png1

紫色的数字是代表月份dom的下标,相同下标对应的月份也相同
中间的1、2、3对应的是之前说过的 -----【至少要提前渲染好3个月份的dom】。
那首尾填充的月份为什么是 3 1 呢?

假设我们现在不限制5个dom,而是无限个dom,那么代表月份dom的下标组合就会是:
1、2、3、1、2、3、1、2、3、1、2、3......

我们以一个1、2、3为中心,取到连续的5个月份dom,那么取到的下标组合就是:
1、2、【3、1、2、3、1】、2、3、1、2、3......

没懂没关系,看下去就会明白。

思考第7个问题:『为了配合无限滑动,要怎么控制显示的月份呢?』

实际上,未来,我会需要取到dom的下标进行更新月份数据的操作,所以我试图发现【3、1、2、3、1】这个下标数组中的规律。

我发现这个下标循环是3的循环,我可以通过取3的模的方式取到每个位置上的dom下标。

现在我要对这个下标做一点小的改动。
我要把3改成0。即【0、1、2、0、1】

原因很简单,是为了在计算滑动距离的时候,将 dom下标translateX 对应起来比较方便。即当滑到最左侧的月份dom的时候,月份的dom的translateX的值为0,可以和下标 0 % 3 的结果相对应。

这样,这个下标,就和translateX直接联系起来了。

好,以初始月份是2017年9月为例,最终初始化的结果为:

png1

接下来,从右向左滑,查看下一个月份,touchend之后,操作如下:

png1

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

png1

这就是实现无限滑动的核心原理。当然还可以接着一直滑:

png1

以此类推,无限左滑也是类似的道理。

五、为什么需要预判用户手势

从上面讲述无限滑动的原理中,你可以大概感觉到: 滑动的距离是通过控制中间的灰色矩形相对于手机屏幕的translateX来决定的。

如何控制translateX的值实现滑动效果,这个问题不是这次的重点。

重点是,思考第8个问题:『如果只在日历的dom区域中控制translateX,当我想滑动整个页面的时候,滑不动,怎么办?』

假设下图中的蓝色曲线代表用户的滑动曲线:

当用户的滑动曲线是A的情况时,用户的意图明显是想把页面往上拉
当用户的滑动曲线是B的情况时,用户的意图明显是想查看上一个月

可实际上,如果只通过控制translateX的值实现滑动效果的时候,无论是曲线A或者B都会被认为是想查看上一个月

png1

也就是说,如果控制了translateX,那么,在这个占据着文档流巨大的面积的dom范围内,永远无法上下滑动。这是万万不被允许的。

所以我们需要预判手势,来实现在日历的dom范围内,既能够上下滑动,又能够左右滑动。效果如下:

思考第9个问题:『是否有简单的方式能够预判手势?』

比如之前提到的【滑动曲线A和B】的示例图,如果以绿线为标准,

  1. 斜率小于绿线的曲线,都归为和滑动曲线B一样的左右滑动

  2. 斜率大于绿线的曲线,都归为和滑动曲线A一样的上下滑动

这样不就可以了吗?
但其实用户的手势曲线一般都是下面的橙色曲线....

png1

而且计算用户手势的斜率一定是在touchmove中实时计算(为什么?当然是为了实时滑动),所以最后,靠斜率预判用户手势的思路,就到这里结束了。

六、如何利用微积分预判用户手势

思考第10个问题:『对用户手势进行积分,就能够解决问题吗?』

用户的手势实际上是一条弧线,当前只考虑从左下角向右上角滑的情况,就能把用户的手势曲线简化在第一象限中。
如下图,我们从微积分的概念出发,得到以下结论。

png1

先看看中间的红色矩形部分,这个红色矩形是把某个细长条矩形夸张的放大后的矩形,其宽为△X,其高为△Y。
通过touchmove实时计算每一次滑动的△X 和 △Y,然后累加面积。面积的累加实际上直接按照△X × △Y的结果正负进行累加,这样就把第一象限的手势推广到所有象限的手势中去了。

计算手势的核心代码如下,其中cal指向当前实例:

png1

我们可以利用用户手势的曲线面积来把用户手势操作量化。
但量化是量化了,要如何知道我量化的结果是上下滑动还是左右滑动呢?
所以就需要像计算斜率时的标准线(那条绿线)一样,必须有一个标准面积。

思考第10个问题:『如何计算标准面积?』

如下图,我们有三条曲线,这三条曲线与X轴围起来的面积,就是我们前面辛辛苦苦量化的结果。其中:

蓝色的曲线围成的面积就是我们理想中的标准面积,虽然还不知道怎么算

黄色的曲线围成的面积比标准面积大,我们将判定所有大于蓝色曲线的量化曲线为【用户试图上下滑动】

绿色的曲线围成的面积比标准面积小,我们将判定所有小于蓝色曲线的量化曲线为【用户试图左右滑动】

png1

问题回到了,如何计算标准面积
观察上图可以发现有一个明显的蓝色的角A,这个角A和实例化的参数angle是同一个东西。

开发者可以通过控制angle的值(angle的单位是°)来控制标准面积的大小。
当然通过我的测试,angle的取值在 [5 , 20]最佳。

那源码中是如何通过开发者传入的angle进行标准面积的计算的呢?

首先,我会将用户的角度转化为tan值。

png1

为什么需要tan值呢,因为我就可以根据△X 计算得到 △Y = △X * tanA

png1

所以标准面积也能通过累加得到了。

png1

至此,我们就可以通过用户手势的面积和标准面积的比较来得到一个比较理想的预判。
通过预判,让用户在页面的任何地方滑动,都感到舒适。

结束语

Github地址:『为移动端而生』的自定义日历插件 https://github.com/AppianZ/calendar

欢迎大家提出宝贵建议和技术交流 ٩(•̤̀ᵕ•̤́๑)

我是嘉宝Appian,一个卖萌出家的算法妹纸(❁ᴗ͈ˬᴗ͈)

查看原文

赞 39 收藏 141 评论 17

Jst502 回答了问题 · 2017-06-28

nodejs爬虫如何控制请求数量?

关注 5 回答 4

Jst502 关注了标签 · 2017-06-28

golang

Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发Go,是因为过去10多年间软件开发的难度令人沮丧。Go是谷歌2009发布的第二款编程语言。

七牛云存储CEO许式伟出版《Go语言编程
go语言翻译项目 http://code.google.com/p/gola...
《go编程导读》 http://code.google.com/p/ac-m...
golang的官方文档 http://golang.org/doc/docs.html
golang windows上安装 http://code.google.com/p/gomi...

关注 26307

Jst502 回答了问题 · 2017-06-28

解决js中有什么办法能让对象进行排序呢?

因为json对象没有顺序,所以“预先排好的顺序”其实并不存在
如果说前端要根据键名排序,可以先把键名取出,排序,再去取内容就行了吧

关注 12 回答 11

Jst502 提出了问题 · 2016-12-07

解决微信网页版是用angular写的吗?

无意中看到, 代码里面很多angular的指令, 很少实际中看到angular的项目.
clipboard.png

关注 3 回答 2

Jst502 关注了问题 · 2016-11-07

平时公司使用angular做什么页面

angular文件就有1m左右的大小,用来开发网页会不会得不偿失呢?使用vue不是更好么?

关注 3 回答 2

Jst502 回答了问题 · 2016-11-07

平时公司使用angular做什么页面

确实是啊, 对于并不是很复杂的单页面并不适合, 单页面应用是没问题

关注 3 回答 2

Jst502 关注了问题 · 2016-11-02

解决angular表单未实时显示赋值和进行验证

点击textarea框,弹出日历插件,选择日历日期(可多选)后,点击完成,但输入框并不显示数据,然后选填下一个输入框input,键盘输入值,这时textarea框中的值才会显示。
图片描述

图片描述

html结构代码如下,用ng-model来绑定数据,表单名为entrust_form,因为后面两个输入框输入时都能实时验证,所有html写法应该没问题。

 <textarea type="text" placeholder="请选择日期" name="selectedDate" id="input1" ng-model="param.selectedDate" readonly="readonly" required></textarea>
 <small class="error" ng-show="entrust_form.selectedDate.$dirty && entrust_form.selectedDate.$error.required">喂药日期不能为空</small>

后端js代码如下,通过日历插件calendar1的回调函数来给param.selectedDate赋值

 $scope.$on('$ionicView.afterEnter',function(){

            //初始化日历
            var input1=document.getElementById('input1');
            var calendar1=new mCalendar({
                //可选参数
                //'setDate':'2016-12-12', //注* 日期格式:2015-01-01
                'multiple':true,//多选
                //必填参数
                'toBind':input1,//绑定触发日历的元素
                'callback':function(){ //确定选择执行回调
                    input1.value = this.outputDate; //让输入框在点击完成时显示绑定数据   
                    $scope.param.selectedDate = this.outputDate;                  
                }
            });
      });

我的尝试:
1.对于未显示赋值,我用input1.value = this.outputDate;强行赋值,就可以解决,但实时验证问题解决不了。
2.$scope.param.selectedDate变量在函数外面是能够获取到值的,所有赋值是成功的。
3.发现需要其他事件才能触发验证,所有直接在回调函数里,赋值语句后面加个alert(),但行不通,它的执行顺序并不在点击“完成”事件的后面。
4.还有一个思路,就是在回调函数中赋值的时候,直接修改表单中entrust_form.$error中的值,但技术有限,没获取到值。
这个问题很重要,但不知道怎么办了,求大神帮忙

关注 4 回答 2

Jst502 回答了问题 · 2016-11-02

解决angular表单未实时显示赋值和进行验证

异步修改了$scope不是要$apply一下嘛

关注 4 回答 2

Jst502 关注了问题 · 2016-11-02

如何实现这样的网格扭曲效果啊?

grid-pulse.png

grid-suction.png

做h5游戏,用什么算法或者游戏引擎能实现这样的效果啊?

关注 3 回答 1

认证与成就

  • 获得 1 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-08-27
个人主页被 174 人浏览