實現選擇器(picker)插件

一個正常的選擇器插件是非常細緻的,一步一步來描述就是。手指滑動內容跟隨手指滾動,當內容到底或觸頂的時候就不能在滾動並且內容要一直保持在正確的位置上。

第一步分析插件結構

首先要有一個插件容器,整個插件容器包含漸變背景,選中實線,內容容器。效果類似於下面:

picker圖片

所以對應的代碼如下:

<div class="scroller-component" data-role="component">
    <div class="scroller-mask" data-role="mask"></div>
    <div class="scroller-indicator" data-role="indicator"></div>
    <div class="scroller-content" data-role="content">
        <div class="scroller-item" data-value='1'>1</div>
        <div class="scroller-item" data-value="2">2</div>
        <div class="scroller-item" data-value="3">3</div>
        <div class="scroller-item" data-value="4">4</div>
        <div class="scroller-item" data-value="5">5</div>
        <div class="scroller-item" data-value="6">6</div>
        <div class="scroller-item" data-value="7">7</div>
        <div class="scroller-item" data-value="8">8</div>
        <div class="scroller-item" data-value="9">9</div>
        <div class="scroller-item" data-value="10">10</div>
        <div class="scroller-item" data-value="11">11</div>
        <div class="scroller-item" data-value="12">12</div>
        <div class="scroller-item" data-value="13">13</div>
        <div class="scroller-item" data-value="14">14</div>
        <div class="scroller-item" data-value="15">15</div>
        <div class="scroller-item" data-value="16">16</div>
        <div class="scroller-item" data-value="17">17</div>
        <div class="scroller-item" data-value="18">18</div>
        <div class="scroller-item" data-value="19">19</div>
        <div class="scroller-item" data-value="20">20</div>
    </div>
</div>
* {
    margin: 0;
    padding: 0;
}
.scroller-component {
    display: block;
    position: relative;
    height: 238px;
    overflow: hidden;
    width: 100%;
}

.scroller-content {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    z-index: 1;
}

.scroller-mask {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    margin: 0 auto;
    width: 100%;
    z-index: 3;
    transform: translateZ(0px);
    background-image:
        linear-gradient(to bottom, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)),
        linear-gradient(to top, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));
    background-position: top, bottom;
    background-size: 100% 102px;
    background-repeat: no-repeat;
}

.scroller-item {
    text-align: center;
    font-size: 16px;
    height: 34px;
    line-height: 34px;
    color: #000;
}

.scroller-indicator {
    width: 100%;
    height: 34px;
    position: absolute;
    left: 0;
    top: 102px;
    z-index: 3;
    background-image:
        linear-gradient(to bottom, #d0d0d0, #d0d0d0, transparent, transparent),
        linear-gradient(to top, #d0d0d0, #d0d0d0, transparent, transparent);
    background-position: top, bottom;
    background-size: 100% 1px;
    background-repeat: no-repeat;
}

.scroller-item {
    line-clamp: 1;
    -webkit-line-clamp: 1;
    overflow: hidden;
    text-overflow: ellipsis;
}

css 代碼主要作爲樣式展示,通過外鏈的方式引入。這裏就不做過多的解釋。

第二步實現手指滾動容器

1.添加手指觸摸事件


let component = document.querySelector('[data-role=component]')

let touchStartHandler = (e) => { }
let touchMoveHandler = (e) => { }
let touchEndHandler = (e) => { }

component.addEventListener('touchstart', touchStartHandler)

component.addEventListener('touchmove', touchMoveHandler)

component.addEventListener('touchend', touchEndHandler)

這樣當手指觸摸到 component 插件容器的時候就會觸發開始,移動,結束事件。

2.分析手指滑動容器移動效果

手指上滑內容上滑,手指下拉內容下拉。只需要控制 content 的位置改動的距離跟手指滑動的距離保持一致即可。這裏用到了 transform 樣式的 translate3d(x, y, z) 屬性。其中 x, z 保持不變,y的值就是手指移動的值。

我們繼續做拆解,當手指下拉時 content 位置下移就會跟手勢保持一致。也就是 y 值變大(需要注意 y 軸正方向是往下的)。手指上拉正好與下拉上滑。當再次下拉或上拉時內容要在原來的基礎上保持不變。因此我們需要一個全局變量 __scrollTop 保存這個值。這個值等於用戶每次上拉下拉值的和,所以我們需要求出來用戶每次上拉下拉的值。

拆解用戶上拉的值,當用戶手觸摸到屏幕的時候就會觸發 touchstart 事件,移動的時候會觸發 touchmove 事件。離開的時候會觸發 touchend 事件。用戶上拉的初始值肯定是觸發 touchstart 時手指的位置。結束值就是 touchend 時手指的位置。但是這樣就不能夠做到內容跟隨手指實時運動了。所以需要拆解 touchmove 事件

touchmove 事件會在用戶手指運動的時候不停的觸發,也就相當於用戶多次極小的上下移動。所以我們需要記錄下來用戶剛開始時觸摸的位置。 __startTouchTop 。用手指當前位置減去剛開始觸發位置就是用戶移動的距離 __scrollTop。具體代碼如下

let content = component.querySelector('[data-role=content]') // 內容容器
let __startTouchTop = 0 // 記錄開始滾動的位置
let __scrollTop = 0 // 記錄最終滾動的位置
// 這個方法下面馬上講解
let __callback = (top) => {
    const distance = top
    content.style.transform = 'translate3d(0, ' + distance + 'px, 0)'
}
// 這個方法下面馬上講解
let __publish = (top, animationDuration) => {
    __scrollTop = top
    __callback(top)
}
let touchStartHandler = (e) => {
    e.preventDefault()
    const target = e.touches ? e.touches[0] : e
    __startTouchTop = target.pageY
}
let touchMoveHandler = (e) => {
    const target = e.touches ? e.touches[0] : e
    let currentTouchTop = target.pageY
    let moveY = currentTouchTop - __startTouchTop
    let scrollTop = __scrollTop
    scrollTop = scrollTop + moveY
    __publish(scrollTop)
    __startTouchTop = currentTouchTop
}

注意1: touchstart 必須要記錄觸摸位置, touchend 可以不記錄。因爲用戶第一次觸摸的位置和下次觸摸的位置在同一個地方的可能性幾乎微乎其微,所以需要在 touchstart 裏面重置觸摸位置。否則當用戶重新觸摸的時候內容會閃動

**注意2:e.preventDefault() 方法是處理某些瀏覽器的兼容問題並且能夠提高性能。像QQ瀏覽器用手指下拉的時候會出現瀏覽器描述導致方法失敗。 可以參考文檔 https://segmentfault.com/a/1190000014134234
https://www.cnblogs.com/ziyunfei/p/5545439.html**

上面的 touchMoveHandler 方法中出現了 __callback 的方法。這個方法是用來控制內容容器的位置的, __publish 方法是對改變容器位置的一層封裝,可以實現跟用戶的手指動作同步,也要實現用戶手指離開之後位置不正確的判斷等。目前先實現跟隨用戶手指移動

代碼到這裏,你用瀏覽器調節成手機模式應該已經可以做到內容跟隨鼠標滾動了,但是還存在很多問題,下面會一一把這些問題修復

第三步,限制手指滑動最大值和最小值

目前用戶可以無限上拉下拉,很明顯是不對的。應該當第一個值稍微超越選中實線下方時就不能在下拉了,當最後一個值稍微超越選中實線上方時就不能在上拉了。所以我們需要倆個值最小滾動值: __minScrollTop 和最大滾動值: __maxScrollTop

計算方式應該是這個樣子:用戶下拉會產生一個最大值,而最大值應該是第一個元素下拉到中間的位置。中間應該就是元素容器中間的位置

let __maxScrollTop = component.clientHeight / 2 // 滾動最大值

最小值應該是用戶上拉時最後一個元素達到中間的位置,因此應該是內容容器-最大值。

let __minScrollTop =  - (content.offsetHeight - __maxScrollTop) // 滾動最小值

最大值最小值有了,只需要在手指上拉下拉的過程中保證 __scrollTop 不大於或者不小於極值即可,因此在 touchMoveHandler 函數中補充如下代碼

if (scrollTop > __maxScrollTop || scrollTop < __minScrollTop) {
    if (scrollTop > __maxScrollTop) {
        scrollTop = __maxScrollTop
    } else {
        scrollTop = __minScrollTop
    }
}

第四步元素的位置準確卡在選中實線中

目前手指擡起的時候元素停留的位置是存在問題,這個也很容易理解。因爲一個元素是有高度的,當你手指移動的距離只要不是元素高度的整數倍他就會卡在選中實線上。因此我們只需要對移動的距離除以元素的高度進行四捨五入取整之後再乘以元素的高度就能夠保證元素位置是元素高得的倍數了

let indicator = component.querySelector('[data-role=indicator]')
let __itemHeight = parseFloat(window.getComputedStyle(indicator).height)

let touchEndHandler = () => { 
    let scrollTop = Math.round(__scrollTop / __itemHeight).toFixed(5) * __itemHeight
    __publish(scrollTop)
}

這樣子產生了倆個問題,一是當極值四捨五入之後超越了極值就會出錯,二是元素跳動太大用戶體驗不好。所以需要處理極值情況和添加動畫滑動效果

處理上面問題中產生的極值問題

我們新建一個函數 __scrollTo 專門解決元素位置不對的問題

// 滾動到正確位置的方法
let __scrollTo = (top) => {
    top = Math.round((top / __itemHeight).toFixed(5)) * __itemHeight
    let newTop = Math.max(Math.min(__maxScrollTop, top), __minScrollTop)
    if (top !== newTop) {
        if (newTop >= __maxScrollTop) {
            top = newTop - __itemHeight / 2
        } else {
            top = newTop + __itemHeight / 2
        }
    }
    __publish(top, 250) // 這裏傳入了第二個參數動畫時長,先留一個伏筆。後面會講
}

簡單分析一下,函數內第一行跟之前的一樣。對位置進行四捨五入變成元素高度的倍數。第二行判斷元素是否大於極值,如果大於最大值就取最大值,小於最小值就取最小值。當滾動值跟新的滾動值不一樣的時候說明用戶移動超過了極值。然後進行處理。大於等於最大值的時候元素的位置正好超出半個元素高度的,所以減掉高度的一半,小於最小值的時候恰好相反。添加一半

添加動畫滑動效果

這個比較麻煩,關於動畫效果是可以單獨開一章來說的。這裏我簡單說一下我這個動畫的思路吧。儘量長話短說。

首先講解一下動畫實現的原理,動畫可以理解爲多張連續的照片快速移動超過眼睛可以捕獲的速度就會形成連貫的動作。這就是我理解的動畫,像上面的 touchMoveHandler 方法其實是會被多次調用的,而且調用頻率非常的高,高到了幾毫秒調用一次,這個速度你肉眼肯定是分辨不出來的,而且每次移動的距離賊短。所以你看起來就有了跟隨手指滾動的效果

所以當手指擡起的時候發現位置不正確這個時候應該實現一個滾動到正確位置的減速動畫效果。這裏我直接將 vux 裏面的 animate.js 文件簡化了一下直接拿過來用了

let running = {} // 運行
let counter = 1 // 計時器
let desiredFrames = 60 // 每秒多少幀
let millisecondsPerSecond = 1000 // 每秒的毫秒數

const Animate = {
  // 停止動畫
  stop (id) {
    var cleared = running[id] != null
    if (cleared) {
      running[id] = null
    }
    return cleared
  },

  // 判斷給定的動畫是否還在運行
  isRunning (id) {
    return running[id] != null
  },
  start (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
    let start = Date.now()
    let percent = 0 // 百分比
    let id = counter++
    let dropCounter = 0

    let step = function () {
      let now = Date.now()

      if (!running[id] || (verifyCallback && !verifyCallback(id))) {
        running[id] = null
        completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false)
        return
      }

      if (duration) {
        percent = (now - start) / duration
        if (percent > 1) {
          percent = 1
        }
      }
      let value = easingMethod ? easingMethod(percent) : percent
      if (percent !== 1 && ( !verifyCallback || verifyCallback(id))) {
        stepCallback(value)
        window.requestAnimationFrame(step)
      }
    }

    running[id] = true
    window.requestAnimationFrame(step)
    return id
  }
}

以上代碼作爲一個js外鏈單獨引入,不知道取什麼名就用 animate.js 好了。

簡單講解一下,主要是弄了一個叫 Animate 的對象,裏面包含三個屬性 stop, isRunning, start。 分別是停止動畫,動畫是否在執行,開始一個動畫。start 是關鍵,因爲其他倆個函數在這個項目中我都沒有用過,哈哈。

start 函數包含很多個參數,stepCallback:每次動畫執行的時候用戶處理的界面元素滾動邏輯;verifyCallback:驗證動畫是否還需要進行的函數;completedCallback:動畫完成時的回調函數;duration:動畫時長;easingMethod:規定動畫的運動方式,像快進慢出,快進快出等等;root:不用管了,沒用到。

結束動畫有倆種方式,第一種是傳入的動畫時長達成,另一種是驗證動畫是否還需要執行的函數驗證通過。否則動畫會一直運動

有了動畫函數了,接下來就是如何使用了。這裏我們補充一下 __publish 函數,並且添加一個是否開啓動畫的全局變量 __isAnimating 和 倆個曲線函數 easeOutCubic, easeInOutCubic

let __isAnimating = false // 是否開啓動畫
// 開始快後來慢的漸變曲線
let easeOutCubic = (pos) => {
    return (Math.pow((pos - 1), 3) + 1)
}
// 以滿足開始和結束的動畫
let easeInOutCubic = (pos) => {
    if ((pos /= 0.5) < 1) {
        return 0.5 * Math.pow(pos, 3)
    }
    return 0.5 * (Math.pow((pos - 2), 3) + 2)
}

let __publish = (top, animationDuration) => {
    if (animationDuration) {
        let oldTop = __scrollTop
        let diffTop = top - oldTop
        let wasAnimating = __isAnimating
        let step = function (percent) {
            __scrollTop = oldTop + (diffTop * percent)
            __callback(__scrollTop)
        }
        let verify = function (id) {
            return __isAnimating === id
        }
        let completed = function (renderedFramesPerSecond, animationId, wasFinished) {
            if (animationId === __isAnimating) {
                __isAnimating = false
            }
        }
        __isAnimating = Animate.start(step, verify, completed, animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic)
    } else {
        __scrollTop = top
        __callback(top)
    }
}

將上面的代碼補充完整你就會發現滾動到正確位置的動畫效果實現了,下面就講講實現的原理。

這裏按照函數執行的順序講解吧。 首先是定義的幾個變量, oldTop:用來保存元素的錯誤位置; diffTop: 傳入的 top 是元素滾動的正確位置; step, verify, completed 是 Animate 對象需要的三個回調函數。裏面的參數先不用管後面會講,最下面給 __isAnimating 付了個值。 Animate.start 函數是有返回值的,返回值是當前動畫的ID

其中需要注意 wasAnimating ? easeOutCubic : easeInOutCubic 這個。意思就是如果原來的動畫存在就將 easeInOutCubic(倆頭慢中間快的參數傳入進去)函數傳入進去, 如果不存在就傳入進去 easeOutCubic(開始快後來慢)函數傳入進去。符合的場景就是你手指快速滑動擡起動畫會執行一段時間吧,這個過程動畫就是從快到慢的過程,然後動畫還沒結束你又接着快速滑動是不是又從慢到快了。如果你不接着執行是不是動畫就由快到慢結束了。這裏爲啥傳入這倆個參數就不講解了,完全可以再開一篇博客進行講解比較麻煩。

step函數,接受一個 percent 翻譯過來是百分比的意思。 下面的第一行代碼

__scrollTop = oldTop + (diffTop * percent)

可以理解成, 老的位置 + 移動的距離 * 百分比 就是新的位置。百分比一直增大當百分比爲百分之百的時候 __scrollTop === top。就實現了一個錯誤位置到正確位置的過度。

百分比的計算方式是根據時間來計算的,然後被動畫曲線進行了加工

if (duration) {
    percent = (now - start) / duration
    if (percent > 1) {
      percent = 1
    }
}
let value = easingMethod ? easingMethod(percent) : percent

上面的是核心代碼。start 是調用Animate.start屬性的時候記錄的一個當前時間,now是內部函數執行的時候記錄的一個當前時間。 now - start 就是經過了多長時間,除以 duration動畫時長就可以得出動畫時長的百分比。下面判斷 easingMethod 是否傳入如果傳入了就對本來勻速增加的百分比進行加工變成了動畫曲線變化的百分比。

首先是 step 函數,每次運動調用的函數。接受了一個 percent ,翻譯過來是百分比意思。 在外面我定了一個幾個局部變量,分別是 oldTop: , , 正確位置減掉錯誤位置也就是元素滾動的距離。在 step 函數裏賦予 __scrollTop 新值

step函數接受了一個叫百分比的參數。 用處就是當元素不在正確位置的時候會產生一個值 __scrollTop, 而元素應該的正確位置的值是 top,元素移動的距離就是 diffTop = top - oldTop 如何一步一步的移動到這個位置呢。就通過動畫函數穿過來的這個百分比參數。這也是爲啥在 __scrollTo 方法中調用 __publish 時加入第二個參數動畫時長的原因了,這樣就實現了一個自由滾動的動畫

verify函數接受一個當前動畫的id參數,驗證規則就是 __isAnimating === id 時說明開啓了下一個動畫 __isAnimating 就會改變。導致驗證失敗,這個時候就會停止上一個動畫

completed函數接受好幾個參數,第一個參數是每秒多少幀,第二個參數是當前動畫id,第三個參數是完成狀態。這裏主要用到了第二個參數當前動畫id。動畫完成的時候應該獎動畫id變爲false否則會一直走驗證的邏輯。

第五步快速短暫觸摸,讓內容自己快速動起來

像目前內容滑動的距離基本是等於用戶手指觸摸的距離的,這樣就跟實際使用不符合,實際中手指使勁一滑內容也會蹭蹭的滾動。就目前這個樣子內容一多也能累死用戶,所以需要添加用戶使勁滑動內容快速滾動起來的邏輯

首先內容自己快速動起來很明顯是有個觸發條件的,這裏的觸發條件是 touchEndHandler 函數執行時的時間減去當最後一次執行 touchMoveHandler 函數的時間小於100毫秒。滿足這種狀態我們認爲用戶開啓快速滾動狀態。所以添加一個全局變量 __lastTouchMove 來記錄最後一次執行 touchMoveHandler 函數的時間。

知道應該快速滾動了,如何判斷應該滾動多長的距離呢?想一下當前的條件,有一個 __lastTouchMove 和執行 touchEndHandler 函數的時間。這倆個是不是能夠的出來一個時間差。在想一下是不是有個 __scrollTop 滾動的位置,如果在獲取到上一個滾動的位置是不是能夠得到一個位置差。那位置 / 時間是等於速度的。我們讓 __scrollTop + 速度 是不是可以得到新的位置。然後我們一直減小速度撿到最後等於 0 是不是就得到了滾動的位置,並且能夠根據用戶的快速滑動情況的出來應該滾動多長的距離,用戶滑的越快速度越快距離越遠,相反的用戶滑動的速度越慢距離越近

遺憾的是在 touchEndHandler 函數中拿不到目標移動的距離 pageY。所以我們需要在 touchMoveHandler 方法中做手腳,去記錄每次執行這個方法時的時間和位置。所以我們再添加一個全局變量 __positions 爲數組類型。

// 上面提到的倆個全局變量的代碼
let __lastTouchMove = 0 // 最後滾動時間記錄
let __positions = [] // 記錄位置和時間

然後我們將增加 __positions 的代碼添加到 touchMoveHandler 方法中

if (__positions.length > 40) {
    __positions.splice(0, 20)
}
__positions.push(scrollTop, e.timeStamp)

__publish(scrollTop)

__startTouchTop = currentTouchTop
__lastTouchMove = e.timeStamp

其中如果 __positions 的長度超過40我們就取後20個。因爲數組太大佔用內存,而且循環遍歷的時候還非常浪費時間。根據上面的邏輯我們手指快速移動不會取時間過長的數據,所以20足夠了。當有了寶貴的位置和時間數據我們就需要在 touchEndHandler 方法中分析出來移動的速度了。這裏我將完整的代碼先切出來。

let __deceleratingMove = 0 // 減速狀態每幀移動的距離
let __isDecelerating = false // 是否開啓減速狀態
let touchEndHandler = (e) => {
    if (e.timeStamp - __lastTouchMove < 100) { // 如果擡起時間和最後移動時間小於 100 證明快速滾動過
        let positions = __positions
        let endPos = positions.length - 1
        let startPos = endPos
        // 由於保存的時候位置跟時間都保存了, 所以 i -= 2
        // positions[i] > (self.__lastTouchMove - 100) 判斷是從什麼時候開始的快速滑動
        for (let i = endPos; i > 0 && positions[i] > (__lastTouchMove - 100); i -= 2) {
            startPos = i
        }
        if (startPos !== endPos) {
            // 計算這兩點之間的相對運動
            let timeOffset = positions[endPos] - positions[startPos] // 快速開始時間 - 結束滾動時間
            let movedTop = __scrollTop - positions[startPos - 1] // 最終距離 - 快速開始距離
            
            __deceleratingMove = movedTop / timeOffset * (1000 / 60) // 1000 / 60 代表 1秒60每幀 也就是 60fps。玩遊戲的可能理解 60fps是啥意思
    
            let minVelocityToStartDeceleration = 4 // 開始減速的最小速度 
            // 只有速度大於最小加速速度時纔會出現下面的動畫
            if (Math.abs(__deceleratingMove) > minVelocityToStartDeceleration) {
                __startDeceleration()
            }
        }
    }
    if (!__isDecelerating) {
        __scrollTo(__scrollTop)
    }
    
    __positions.length = 0
}

新添加了倆個全局變量運動速度和減速狀態記錄。當減速狀態爲true的時候肯定不能執行 __scrollTo 方法的因爲這倆個方法是衝突的。所以需要 __isDecelerating 記錄一下。裏面新定義了一個函數 __startDeceleration。 我們的減速方法也主要是在這個方法裏面實現的。給你一下代碼

// 開始減速動畫
let __startDeceleration = () => {
    let step = () => {
        let scrollTop = __scrollTop + __deceleratingMove
        let scrollTopFixed = Math.max(Math.min(__maxScrollTop, scrollTop), __minScrollTop) // 不小於最小值,不大於最大值
        if (scrollTopFixed !== scrollTop) {
            scrollTop = scrollTopFixed
            __deceleratingMove = 0
        }
        if (Math.abs(__deceleratingMove) <= 1) {
            if (Math.abs(scrollTop % __itemHeight) < 1) {
                __deceleratingMove = 0
            }
        } else {
            __deceleratingMove *= 0.95
        }
        __publish(scrollTop)
    }
    let minVelocityToKeepDecelerating = 0.5
    let verify = () => {
        // 保持減速運行需要多少速度
        let shouldContinue = Math.abs(__deceleratingMove) >= minVelocityToKeepDecelerating
        return shouldContinue
    }
    let completed = function (renderedFramesPerSecond, animationId, wasFinished) {
        __isDecelerating = false
        if (__scrollTop <= __minScrollTop || __scrollTop >= __maxScrollTop) {
            __scrollTo(__scrollTop)
            return
        }
    }
    __isDecelerating = Animate.start(step, verify, completed)
}

當你把這些代碼都加進去的時候,選擇器插件基本上就已經完成了。下面講解一下這段讓你頭痛的代碼。

這裏面用到了動畫,所以肯定包含三大回調函數 step, verify, completed。然後一個一個講解一下

step函數:這個函數是讓內容一步一步運動的,這個函數基本上跟滾動到正確位置的函數相似度很高。 新的位置是老位置 __scrollTop 加上每幀移動的位置 __deceleratingMove。 然後讓每幀移動的位置一直減少,但是需要注意 scrollTop 不能超出極值,所以做了最大值最小值判斷當到達極值的時候就將 __deceleratingMove 賦值爲0 。

if (Math.abs(__deceleratingMove) <= 1) {
    if (Math.abs(scrollTop % __itemHeight) < 1) {
        __deceleratingMove = 0
    }
}

這段代碼,你可能佷懵。他的作用是當滾動的位置沒有到達極值的時候如何讓他卡在正確位置上。 Math.abs(__deceleratingMove) 這是每幀移動的距離的絕對值。當他小於1的時候說明移動的距離已經非常小了,用戶基本上都察覺不到移動了。然後再用新位置對元素高度取餘,如果餘數爲0表示正好卡在正確位置上,但是即使稍微比 0 大那麼一丟丟也看不出來,而且基本不會那麼巧取到 0,所以當餘數滿足小於 1 的時候講每幀移動的距離賦值爲0.

verify函數:定義了一個最小每幀移動距離的局部變量 minVelocityToKeepDecelerating, 當 __deceleratingMove 值小於他的時候說明用戶基本上不會發現內容還在移動可以停下來了。

completed函數:既然是完成函數就一定要將 __isDecelerating 參數變爲false,否則下次進行的不是快速移動內容就沒法跑到正確位置上了。這裏多加了一步是否是極值的判斷,如果是極值就執行 __scrollTo 函數到正確位置上。

下載正確代碼

代碼的正確順序實在是不好再文中體現出來,所以我將正確代碼放到了我的github上,讓大家不知道放到哪裏的時候好有個參照

https://github.com/mrxu0/nativePhoneUI

感謝

這個項目是參考的 vux 做出來的。基本上代碼都是對 vux 裏面picker的解讀。爲了更加凸顯picker的核心代碼,我在裏面刪除了很多東西像兼容性,雙指滑動的代碼。有興趣可以去看看這個項目
https://github.com/airyland/vux

注意:這個樣式雖然已經滿足大部分項目需求了,但是還是不夠漂亮。我看過很多手機端pickder的插件。發現京東的nutui的是最漂亮的,他裏面用到了css3的旋轉特性把他做的想一個滾輪一樣。有興趣的可以學習更精進一波。
https://github.com/jdf2e/nutui

後期規劃

選擇器控件只是開始,後面我會將他衍生出來地區級聯控件,日期控件。還會擴展輪播圖,上拉加載下拉刷新,浮層等等控件。並且集成到vue中作爲一個手機端框架。有興趣的可以關注我的另外一個項目。目前是什麼都沒有的,所以非常適合想要學習的人一步一步跟進。
https://github.com/mrxu0/iphone-ui

交流反饋

歡迎大家加QQ羣交流反饋:954917384

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章