如何造一個『爲移動端而生』的日曆

以前寫了一篇Calendar -『爲移動端而生』的自定義日曆,一直有童鞋對這個插件的手勢處理存在一些問題,因此想寫篇文章,來講說它的成長史~css

在閱讀本文以前,確保你有稍微看過 calendar 的效果 喔~git

gif0.
gif0.

1、 確認需求

想作一個日曆最主要的緣由,固然仍是由於在開發過程當中頻繁的遇到。並且對日曆的需求又是奇葩到不行,市面上的插件都知足不了咱們產品的需求。因此,我不得不動手本身造。github

這段話,好像在造 上一個插件 - 級聯選擇器 的時候也說過
你們就當無事發生過(⁎⁍̴̛ᴗ⁍̴̛⁎) ajax

首要問題依然是處理需求:算法

第1個問題:『日曆的出現場景有哪些特色?』

  1. 用戶不肯定本身要選擇的時間點或時間範圍,須要一些基本的時間參照單位,好比「下星期一」、「下個週末」。
  2. 用戶須要查看某個時間區間,以後再有選擇性的選取時間點或時間範圍,好比「儘量避開週末的20天翹班請假計劃」。
  3. 用戶須要查看某個時間區間的行爲記錄,好比「查看過去幾周的打卡狀況

當出現以上問題的時候,日曆的時間定位優點就顯示出來了。npm

第2個問題:『日曆會有哪些奇葩需求?』

  1. 日曆存在着點擊事件,點擊事件是 跳轉事件 仍是 高亮事件 沒法預知。
  2. 日曆存在着選取操做,選取的結果是 時間點 仍是 時間範圍 沒法預知。
  3. 日曆有多種展示形式,是直接 文檔流顯示 仍是 彈層顯示 沒法預知。

針對這些不穩定因素,接下來,會帶你一步步解決。segmentfault

2、構造函數的參數設計

肯定了日曆的需求,就來設計一下構造函數的參數吧~api

第3個問題:『日曆有哪些常見的展示形式?』

從如今市面上的常見的app上看,咱們會發現,日曆常見的展示形式有兩種:數組

  1. 普通文檔流形式
  2. 彈層形式

在參數的設置中,表現爲設置isMask,false:普通形式,true:彈層形式。瀏覽器


第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指向當前實例

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

名稱 傳入參數的類型 做用
renderCallbackArr(arr) {Array} 渲染指定的arr,arr的格式和beforeRenderArr的對象數組的格式同樣
prevent() - 在微信瀏覽器中,你可能須要用到的阻止默認事件的api
hideBackground() - 在彈層模式的success回調中,你可能須要用到的關閉彈層的api

適當解釋一下api的用意:

1.renderCallbackArr中傳入一個數組,(數組格式和beforeRenderArr同樣,再也不說明),這個方法可以往你須要的時間點上添加指定樣式。設想一種場景:

經過滑動切換,查看三個月前的打卡狀況,已打卡和未打卡的日期都有不一樣的高亮樣式。

顯然,這個月的打卡狀況是須要你在switchRender回調中發起http請求後獲得。

在http返回結果後,構造一個符合beforeRenderArr格式的數組,而後調用renderCallbackArr,傳入構造好的數組,就能對指定的日期渲染指定的className了。

```js
// 舉個栗子🌰
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()收起彈層。固然,也能夠不收起彈層。

4、如何利用五個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。


思考第6個問題:『頭尾兩個做爲填充的dom要顯示哪一個月呢?』

直接參考一下swiper的效果就可以獲得答案,我如今舉一個實例來作一些說明:
先考慮如下狀況:

手勢操做:連續從右向左滑
操做結果:連續查看下個月

如下是圖例,紅色箭頭的更新操做:

以當前進入頁面的初始月份是2017年9月爲例:
初始狀態:

紫色的數字是表明月份dom的下標,相同下標對應的月份也相同
中間的一、二、3對應的是以前說過的 -----【至少要提早渲染好3個月份的dom】。
那首尾填充的月份爲何是 3 1 呢?

假設咱們如今不限制5個dom,而是無限個dom,那麼表明月份dom的下標組合就會是:
一、二、三、一、二、三、一、二、三、一、二、3......

咱們以一個一、二、3爲中心,取到連續的5個月份dom,那麼取到的下標組合就是:
一、二、【三、一、二、三、1】、二、三、一、二、3......

沒懂不要緊,看下去就會明白。

思考第7個問題:『爲了配合無限滑動,要怎麼控制顯示的月份呢?』

實際上,將來,我會須要取到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的時候定位到下圖的位置中:

這就是實現無限滑動的核心原理。固然還能夠接着一直滑:



以此類推, 無限左滑也是相似的道理。

5、爲何須要預判用戶手勢

從上面講述無限滑動的原理中,你能夠大概感受到: 滑動的距離是經過控制中間的灰色矩形相對於手機屏幕的translateX來決定的。

如何控制translateX的值實現滑動效果,這個問題不是此次的重點。

重點是,思考第8個問題:『若是隻在日曆的dom區域中控制translateX,當我想滑動整個頁面的時候,滑不動,怎麼辦?』

假設下圖中的藍色曲線表明用戶的滑動曲線:

當用戶的滑動曲線是A的狀況時,用戶的意圖明顯是想把頁面往上拉
當用戶的滑動曲線是B的狀況時,用戶的意圖明顯是想查看上一個月

可實際上,若是隻經過控制translateX的值實現滑動效果的時候,不管是曲線A或者B都會被認爲是想查看上一個月



也就是說,若是控制了translateX,那麼,在這個佔據着文檔流巨大的面積的dom範圍內,永遠沒法上下滑動。這是萬萬不被容許的。

因此咱們須要預判手勢,來實如今日曆的dom範圍內,既可以上下滑動,又可以左右滑動。效果以下:

思考第9個問題:『是否有簡單的方式可以預判手勢?』

好比以前提到的【滑動曲線A和B】的示例圖,若是以綠線爲標準,

  1. 斜率小於綠線的曲線,都歸爲和滑動曲線B同樣的左右滑動
  2. 斜率大於綠線的曲線,都歸爲和滑動曲線A同樣的上下滑動

這樣不就能夠了嗎?
但其實用戶的手勢曲線通常都是下面的橙色曲線....





並且計算用戶手勢的斜率必定是在touchmove中實時計算(爲何?固然是爲了實時滑動),因此最後,靠斜率預判用戶手勢的思路,就到這裏結束了。

6、如何利用微積分預判用戶手勢

思考第10個問題:『對用戶手勢進行積分,就可以解決問題嗎?』

用戶的手勢其實是一條弧線,當前只考慮從左下角向右上角滑的狀況,就能把用戶的手勢曲線簡化在第一象限中。
以下圖,咱們從微積分的概念出發,獲得如下結論。





先看看中間的紅色矩形部分,這個紅色矩形是把某個細長條矩形誇張的放大後的矩形,其寬爲△X,其高爲△Y。
經過touchmove實時計算每一次滑動的△X 和 △Y,而後累加面積。面積的累加實際上直接按照△X × △Y的結果正負進行累加,這樣就把第一象限的手勢推廣到全部象限的手勢中去了。

計算手勢的核心代碼以下,其中cal指向當前實例:





咱們能夠利用用戶手勢的曲線面積來把用戶手勢操做量化。
但量化是量化了,要如何知道我量化的結果是上下滑動仍是左右滑動呢?
因此就須要像計算斜率時的標準線(那條綠線)同樣,必須有一個標準面積。

思考第10個問題:『如何計算標準面積?』

以下圖,咱們有三條曲線,這三條曲線與X軸圍起來的面積,就是咱們前面辛辛苦苦量化的結果。其中:

藍色的曲線圍成的面積就是咱們理想中的標準面積,雖然還不知道怎麼算

黃色的曲線圍成的面積比標準面積大,咱們將斷定全部大於藍色曲線的量化曲線爲【用戶試圖上下滑動】

綠色的曲線圍成的面積比標準面積小,咱們將斷定全部小於藍色曲線的量化曲線爲【用戶試圖左右滑動】



問題回到了,如何計算標準面積
觀察上圖能夠發現有一個明顯的藍色的角A,這個角A和實例化的參數angle是同一個東西。

開發者能夠經過控制angle的值(angle的單位是°)來控制標準面積的大小。
固然經過個人測試,angle的取值在 [5 , 20]最佳。

那源碼中是如何經過開發者傳入的angle進行標準面積的計算的呢?

首先,我會將用戶的角度轉化爲tan值。



爲何須要tan值呢,由於我就能夠根據△X 計算獲得 △Y = △X * tanA



因此標準面積也能經過累加獲得了。



至此,咱們就能夠經過用戶手勢的面積和標準面積的比較來獲得一個比較理想的預判。
經過預判,讓用戶在頁面的任何地方滑動,都感到溫馨。

結束語

Github地址:『爲移動端而生』的自定義日曆插件 https://github.com/AppianZ/calendar

歡迎你們提出寶貴建議和技術交流 ٩(•̤̀ᵕ•̤́๑)

我是嘉寶Appian,一個賣萌出家的算法妹紙(❁ᴗ͈ˬᴗ͈)

相關文章
相關標籤/搜索