溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

選擇器插件如何實(shí)現(xiàn)

發(fā)布時間:2020-10-16 15:48:04 來源:億速云 閱讀:142 作者:小新 欄目:web開發(fā)

這篇文章給大家分享的是有關(guān)選擇器插件如何實(shí)現(xiàn)的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個參考。一起跟隨小編過來看看吧。

一個正常的選擇器插件是非常細(xì)致的,一步一步來描述就是。手指滑動內(nèi)容跟隨手指滾動,當(dāng)內(nèi)容到底或觸頂?shù)臅r候就不能在滾動并且內(nèi)容要一直保持在正確的位置上。

第一步分析插件結(jié)構(gòu)

首先要有一個插件容器,整個插件容器包含漸變背景,選中實(shí)線,內(nèi)容容器。效果類似于下面:

選擇器插件如何實(shí)現(xiàn)

所以對應(yīng)的代碼如下:

<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 代碼主要作為樣式展示,通過外鏈的方式引入。這里就不做過多的解釋。

第二步實(shí)現(xiàn)手指滾動容器

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)

這樣當(dāng)手指觸摸到 component 插件容器的時候就會觸發(fā)開始,移動,結(jié)束事件。

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

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

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

拆解用戶上拉的值,當(dāng)用戶手觸摸到屏幕的時候就會觸發(fā) touchstart 事件,移動的時候會觸發(fā) touchmove 事件。離開的時候會觸發(fā) touchend 事件。用戶上拉的初始值肯定是觸發(fā) touchstart 時手指的位置。結(jié)束值就是 touchend 時手指的位置。但是這樣就不能夠做到內(nèi)容跟隨手指實(shí)時運(yùn)動了。所以需要拆解 touchmove 事件

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

let content = component.querySelector('[data-role=content]') // 內(nèi)容容器
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 可以不記錄。因?yàn)橛脩舻谝淮斡|摸的位置和下次觸摸的位置在同一個地方的可能性幾乎微乎其微,所以需要在 touchstart 里面重置觸摸位置。否則當(dāng)用戶重新觸摸的時候內(nèi)容會閃動

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

上面的 touchMoveHandler 方法中出現(xiàn)了 __callback 的方法。這個方法是用來控制內(nèi)容容器的位置的, __publish 方法是對改變?nèi)萜魑恢玫囊粚臃庋b,可以實(shí)現(xiàn)跟用戶的手指動作同步,也要實(shí)現(xiàn)用戶手指離開之后位置不正確的判斷等。目前先實(shí)現(xiàn)跟隨用戶手指移動

代碼到這里,你用瀏覽器調(diào)節(jié)成手機(jī)模式應(yīng)該已經(jīng)可以做到內(nèi)容跟隨鼠標(biāo)滾動了,但是還存在很多問題,下面會一一把這些問題修復(fù)

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

目前用戶可以無限上拉下拉,很明顯是不對的。應(yīng)該當(dāng)?shù)谝粋€值稍微超越選中實(shí)線下方時就不能在下拉了,當(dāng)最后一個值稍微超越選中實(shí)線上方時就不能在上拉了。所以我們需要倆個值最小滾動值: __minScrollTop 和最大滾動值: __maxScrollTop

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

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

最小值應(yīng)該是用戶上拉時最后一個元素達(dá)到中間的位置,因此應(yīng)該是內(nèi)容容器-最大值。

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

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

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

第四步元素的位置準(zhǔn)確卡在選中實(shí)線中

目前手指抬起的時候元素停留的位置是存在問題,這個也很容易理解。因?yàn)橐粋€元素是有高度的,當(dāng)你手指移動的距離只要不是元素高度的整數(shù)倍他就會卡在選中實(shí)線上。因此我們只需要對移動的距離除以元素的高度進(jìn)行四舍五入取整之后再乘以元素的高度就能夠保證元素位置是元素高得的倍數(shù)了

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)
}

這樣子產(chǎn)生了倆個問題,一是當(dāng)極值四舍五入之后超越了極值就會出錯,二是元素跳動太大用戶體驗(yàn)不好。所以需要處理極值情況和添加動畫滑動效果

處理上面問題中產(chǎn)生的極值問題

我們新建一個函數(shù) __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) // 這里傳入了第二個參數(shù)動畫時長,先留一個伏筆。后面會講
}

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

添加動畫滑動效果

這個比較麻煩,關(guān)于動畫效果是可以單獨(dú)開一章來說的。這里我簡單說一下我這個動畫的思路吧。盡量長話短說。

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

所以當(dāng)手指抬起的時候發(fā)現(xiàn)位置不正確這個時候應(yīng)該實(shí)現(xiàn)一個滾動到正確位置的減速動畫效果。這里我直接將 vux 里面的 animate.js 文件簡化了一下直接拿過來用了

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

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

  // 判斷給定的動畫是否還在運(yùn)行
  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外鏈單獨(dú)引入,不知道取什么名就用 animate.js 好了。

簡單講解一下,主要是弄了一個叫 Animate 的對象,里面包含三個屬性 stop, isRunning, start。 分別是停止動畫,動畫是否在執(zhí)行,開始一個動畫。start 是關(guān)鍵,因?yàn)槠渌麄z個函數(shù)在這個項(xiàng)目中我都沒有用過,哈哈。

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

結(jié)束動畫有倆種方式,第一種是傳入的動畫時長達(dá)成,另一種是驗(yàn)證動畫是否還需要執(zhí)行的函數(shù)驗(yàn)證通過。否則動畫會一直運(yùn)動

有了動畫函數(shù)了,接下來就是如何使用了。這里我們補(bǔ)充一下 __publish 函數(shù),并且添加一個是否開啟動畫的全局變量 __isAnimating 和 倆個曲線函數(shù) easeOutCubic, easeInOutCubic

let __isAnimating = false // 是否開啟動畫
// 開始快后來慢的漸變曲線
let easeOutCubic = (pos) => {
    return (Math.pow((pos - 1), 3) + 1)
}
// 以滿足開始和結(jié)束的動畫
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)
    }
}

將上面的代碼補(bǔ)充完整你就會發(fā)現(xiàn)滾動到正確位置的動畫效果實(shí)現(xiàn)了,下面就講講實(shí)現(xiàn)的原理。

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

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

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

__scrollTop = oldTop + (diffTop * percent)

可以理解成, 老的位置 + 移動的距離 * 百分比 就是新的位置。百分比一直增大當(dāng)百分比為百分之百的時候 __scrollTop === top。就實(shí)現(xiàn)了一個錯誤位置到正確位置的過度。

百分比的計算方式是根據(jù)時間來計算的,然后被動畫曲線進(jìn)行了加工

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

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

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

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

verify函數(shù)接受一個當(dāng)前動畫的id參數(shù),驗(yàn)證規(guī)則就是 __isAnimating === id 時說明開啟了下一個動畫 __isAnimating 就會改變。導(dǎo)致驗(yàn)證失敗,這個時候就會停止上一個動畫

completed函數(shù)接受好幾個參數(shù),第一個參數(shù)是每秒多少幀,第二個參數(shù)是當(dāng)前動畫id,第三個參數(shù)是完成狀態(tài)。這里主要用到了第二個參數(shù)當(dāng)前動畫id。動畫完成的時候應(yīng)該獎動畫id變?yōu)閒alse否則會一直走驗(yàn)證的邏輯。

第五步快速短暫觸摸,讓內(nèi)容自己快速動起來

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

首先內(nèi)容自己快速動起來很明顯是有個觸發(fā)條件的,這里的觸發(fā)條件是 touchEndHandler 函數(shù)執(zhí)行時的時間減去當(dāng)最后一次執(zhí)行 touchMoveHandler 函數(shù)的時間小于100毫秒。滿足這種狀態(tài)我們認(rèn)為用戶開啟快速滾動狀態(tài)。所以添加一個全局變量 __lastTouchMove 來記錄最后一次執(zhí)行 touchMoveHandler 函數(shù)的時間。

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

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

// 上面提到的倆個全局變量的代碼
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個。因?yàn)閿?shù)組太大占用內(nèi)存,而且循環(huán)遍歷的時候還非常浪費(fèi)時間。根據(jù)上面的邏輯我們手指快速移動不會取時間過長的數(shù)據(jù),所以20足夠了。當(dāng)有了寶貴的位置和時間數(shù)據(jù)我們就需要在 touchEndHandler 方法中分析出來移動的速度了。這里我將完整的代碼先切出來。

let __deceleratingMove = 0 // 減速狀態(tài)每幀移動的距離
let __isDecelerating = false // 是否開啟減速狀態(tài)
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) {
            // 計算這兩點(diǎn)之間的相對運(yùn)動
            let timeOffset = positions[endPos] - positions[startPos] // 快速開始時間 - 結(jié)束滾動時間
            let movedTop = __scrollTop - positions[startPos - 1] // 最終距離 - 快速開始距離
            
            __deceleratingMove = movedTop / timeOffset * (1000 / 60) // 1000 / 60 代表 1秒60每幀 也就是 60fps。玩游戲的可能理解 60fps是啥意思
    
            let minVelocityToStartDeceleration = 4 // 開始減速的最小速度 
            // 只有速度大于最小加速速度時才會出現(xiàn)下面的動畫
            if (Math.abs(__deceleratingMove) > minVelocityToStartDeceleration) {
                __startDeceleration()
            }
        }
    }
    if (!__isDecelerating) {
        __scrollTo(__scrollTop)
    }
    
    __positions.length = 0
}

新添加了倆個全局變量運(yùn)動速度和減速狀態(tài)記錄。當(dāng)減速狀態(tài)為true的時候肯定不能執(zhí)行 __scrollTo 方法的因?yàn)檫@倆個方法是沖突的。所以需要 __isDecelerating 記錄一下。里面新定義了一個函數(shù) __startDeceleration。 我們的減速方法也主要是在這個方法里面實(shí)現(xiàn)的。給你一下代碼

// 開始減速動畫
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 = () => {
        // 保持減速運(yùn)行需要多少速度
        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)
}

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

這里面用到了動畫,所以肯定包含三大回調(diào)函數(shù) step, verify, completed。然后一個一個講解一下

step函數(shù):這個函數(shù)是讓內(nèi)容一步一步運(yùn)動的,這個函數(shù)基本上跟滾動到正確位置的函數(shù)相似度很高。 新的位置是老位置 __scrollTop 加上每幀移動的位置 __deceleratingMove。 然后讓每幀移動的位置一直減少,但是需要注意 scrollTop 不能超出極值,所以做了最大值最小值判斷當(dāng)?shù)竭_(dá)極值的時候就將 __deceleratingMove 賦值為0 。

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

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

verify函數(shù):定義了一個最小每幀移動距離的局部變量 minVelocityToKeepDecelerating, 當(dāng) __deceleratingMove 值小于他的時候說明用戶基本上不會發(fā)現(xiàn)內(nèi)容還在移動可以停下來了。

completed函數(shù):既然是完成函數(shù)就一定要將 __isDecelerating 參數(shù)變?yōu)閒alse,否則下次進(jìn)行的不是快速移動內(nèi)容就沒法跑到正確位置上了。這里多加了一步是否是極值的判斷,如果是極值就執(zhí)行 __scrollTo 函數(shù)到正確位置上。

感謝各位的閱讀!關(guān)于選擇器插件如何實(shí)現(xiàn)就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI