成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

wenshi11019 / 2717人閱讀

摘要:一個(gè)正常的選擇器插件是非常細(xì)致的,一步一步來描述就是。第二步實(shí)現(xiàn)手指滾動(dòng)容器添加手指觸摸事件這樣當(dāng)手指觸摸到插件容器的時(shí)候就會(huì)觸發(fā)開始,移動(dòng),結(jié)束事件。對(duì)位置進(jìn)行四舍五入變成元素高度的倍數(shù)。就實(shí)現(xiàn)了一個(gè)錯(cuò)誤位置到正確位置的過度。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
* {
    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)手指滾動(dòng)容器 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 插件容器的時(shí)候就會(huì)觸發(fā)開始,移動(dòng),結(jié)束事件。

2.分析手指滑動(dòng)容器移動(dòng)效果

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

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

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

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

let content = component.querySelector("[data-role=content]") // 內(nèi)容容器
let __startTouchTop = 0 // 記錄開始滾動(dòng)的位置
let __scrollTop = 0 // 記錄最終滾動(dòng)的位置
// 這個(gè)方法下面馬上講解
let __callback = (top) => {
    const distance = top
    content.style.transform = "translate3d(0, " + distance + "px, 0)"
}
// 這個(gè)方法下面馬上講解
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)橛脩舻谝淮斡|摸的位置和下次觸摸的位置在同一個(gè)地方的可能性幾乎微乎其微,所以需要在 touchstart 里面重置觸摸位置。否則當(dāng)用戶重新觸摸的時(shí)候內(nèi)容會(huì)閃動(dòng)

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

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

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

第三步,限制手指滑動(dòng)最大值和最小值

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

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

let __maxScrollTop = component.clientHeight / 2 // 滾動(dòng)最大值

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

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

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

if (scrollTop > __maxScrollTop || scrollTop < __minScrollTop) {
    if (scrollTop > __maxScrollTop) {
        scrollTop = __maxScrollTop
    } else {
        scrollTop = __minScrollTop
    }
}
第四步元素的位置準(zhǔn)確卡在選中實(shí)線中

目前手指抬起的時(shí)候元素停留的位置是存在問題,這個(gè)也很容易理解。因?yàn)橐粋€(gè)元素是有高度的,當(dāng)你手指移動(dòng)的距離只要不是元素高度的整數(shù)倍他就會(huì)卡在選中實(shí)線上。因此我們只需要對(duì)移動(dòng)的距離除以元素的高度進(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)生了倆個(gè)問題,一是當(dāng)極值四舍五入之后超越了極值就會(huì)出錯(cuò),二是元素跳動(dòng)太大用戶體驗(yàn)不好。所以需要處理極值情況和添加動(dòng)畫滑動(dòng)效果

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

我們新建一個(gè)函數(shù) __scrollTo 專門解決元素位置不對(duì)的問題

// 滾動(dòng)到正確位置的方法
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) // 這里傳入了第二個(gè)參數(shù)動(dòng)畫時(shí)長(zhǎng),先留一個(gè)伏筆。后面會(huì)講
}

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

添加動(dòng)畫滑動(dòng)效果

這個(gè)比較麻煩,關(guān)于動(dòng)畫效果是可以多帶帶開一章來說的。這里我簡(jiǎn)單說一下我這個(gè)動(dòng)畫的思路吧。盡量長(zhǎng)話短說。

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

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

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

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

  // 判斷給定的動(dòng)畫是否還在運(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
  }
}

以上代碼作為一個(gè)js外鏈多帶帶引入,不知道取什么名就用 animate.js 好了。

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

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

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

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

let __isAnimating = false // 是否開啟動(dòng)畫
// 開始快后來慢的漸變曲線
let easeOutCubic = (pos) => {
    return (Math.pow((pos - 1), 3) + 1)
}
// 以滿足開始和結(jié)束的動(dòng)畫
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ǔ)充完整你就會(huì)發(fā)現(xiàn)滾動(dòng)到正確位置的動(dòng)畫效果實(shí)現(xiàn)了,下面就講講實(shí)現(xiàn)的原理。

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

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

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

__scrollTop = oldTop + (diffTop * percent)

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

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

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

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

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

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

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

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

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

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

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

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

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

// 上面提到的倆個(gè)全局變量的代碼
let __lastTouchMove = 0 // 最后滾動(dòng)時(shí)間記錄
let __positions = [] // 記錄位置和時(shí)間

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

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

__publish(scrollTop)

__startTouchTop = currentTouchTop
__lastTouchMove = e.timeStamp

其中如果 __positions 的長(zhǎng)度超過40我們就取后20個(gè)。因?yàn)閿?shù)組太大占用內(nèi)存,而且循環(huán)遍歷的時(shí)候還非常浪費(fèi)時(shí)間。根據(jù)上面的邏輯我們手指快速移動(dòng)不會(huì)取時(shí)間過長(zhǎng)的數(shù)據(jù),所以20足夠了。當(dāng)有了寶貴的位置和時(shí)間數(shù)據(jù)我們就需要在 touchEndHandler 方法中分析出來移動(dòng)的速度了。這里我將完整的代碼先切出來。

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

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

// 開始減速動(dòng)畫
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)去的時(shí)候,選擇器插件基本上就已經(jīng)完成了。下面講解一下這段讓你頭痛的代碼。

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

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

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

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

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

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

下載正確代碼

代碼的正確順序?qū)嵲谑遣缓迷傥闹畜w現(xiàn)出來,所以我將正確代碼放到了我的github上,讓大家不知道放到哪里的時(shí)候好有個(gè)參照

https://github.com/mrxu0/nativePhoneUI

感謝

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

注意:這個(gè)樣式雖然已經(jīng)滿足大部分項(xiàng)目需求了,但是還是不夠漂亮。我看過很多手機(jī)端pickder的插件。發(fā)現(xiàn)京東的nutui的是最漂亮的,他里面用到了css3的旋轉(zhuǎn)特性把他做的想一個(gè)滾輪一樣。有興趣的可以學(xué)習(xí)更精進(jìn)一波。
https://github.com/jdf2e/nutui

后期規(guī)劃

選擇器控件只是開始,后面我會(huì)將他衍生出來地區(qū)級(jí)聯(lián)控件,日期控件。還會(huì)擴(kuò)展輪播圖,上拉加載下拉刷新,浮層等等控件。并且集成到vue中作為一個(gè)手機(jī)端框架。有興趣的可以關(guān)注我的另外一個(gè)項(xiàng)目。目前是什么都沒有的,所以非常適合想要學(xué)習(xí)的人一步一步跟進(jìn)。
https://github.com/mrxu0/iphone-ui

交流反饋

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

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/53860.html

相關(guān)文章

  • 用Vue搭建一個(gè)應(yīng)用盒子(二):datetime-picker

    摘要:接著上次的進(jìn)度,我們已經(jīng)實(shí)現(xiàn)了一個(gè)。我們應(yīng)該完成的效果是一個(gè),日期選擇器。好了,到這一步,還不能實(shí)現(xiàn)這個(gè)插件。我們還需要添加一個(gè)方法,因?yàn)椴]有被執(zhí)行,所以我們需要添加如下代碼好了,這里事件選擇插件就能順利使用了。與的結(jié)合使用實(shí)例 接著上次的進(jìn)度,我們已經(jīng)實(shí)現(xiàn)了一個(gè)todo-list。它已經(jīng)具備了基本的功能,可以新建、編輯、刪除任務(wù)。但是美中不足的是,它的時(shí)間設(shè)定上只能通過輸入一段字符...

    Ververica 評(píng)論0 收藏0
  • 原生js實(shí)現(xiàn)拾色插件

    摘要:拾色器將會(huì)分別綁定每個(gè)元素。會(huì)回傳兩個(gè)參數(shù),第一個(gè)就是該拾色器生成時(shí)綁定的第二個(gè)參數(shù),代表是回傳的顏色值。起初是插件直接改變綁定元素的顏色,但是想到有些拾色器插件是綁定表單,改變表單顏色值,有些是改變綁定元素的顏色。 原生js實(shí)現(xiàn)拾色器插件 前言 插件功能只滿足我司業(yè)務(wù)需求,如果希望有更多功能的,可在下方留言,我盡量擴(kuò)展!如果你有需要或者喜歡的話,可以給我github來個(gè)star ? ...

    codeGoogle 評(píng)論0 收藏0
  • 開源一個(gè)丟人的、簡(jiǎn)單的顏色選擇

    摘要:簡(jiǎn)單的顏色選擇器不使用插件或是任何圖片無需任何依賴庫和近似的體驗(yàn)支持和格式輸入支持和輸出可監(jiān)聽的事件可通過自定義的扁平化設(shè)計(jì)同時(shí)可在與瀏覽器中正常工作演示請(qǐng)?jiān)L問該頁面查看在線您可以通過瀏覽頁面源代碼了解基本的使用方法安裝與使用安裝對(duì)象和 Simple Color Picker - 簡(jiǎn)單的顏色選擇器 不使用Flash插件或是任何圖片 無需任何依賴庫 和Photoshop近似的體驗(yàn) 支持...

    darryrzhong 評(píng)論0 收藏0
  • 開源一個(gè)丟人的、簡(jiǎn)單的顏色選擇

    摘要:簡(jiǎn)單的顏色選擇器不使用插件或是任何圖片無需任何依賴庫和近似的體驗(yàn)支持和格式輸入支持和輸出可監(jiān)聽的事件可通過自定義的扁平化設(shè)計(jì)同時(shí)可在與瀏覽器中正常工作演示請(qǐng)?jiān)L問該頁面查看在線您可以通過瀏覽頁面源代碼了解基本的使用方法安裝與使用安裝對(duì)象和 Simple Color Picker - 簡(jiǎn)單的顏色選擇器 不使用Flash插件或是任何圖片 無需任何依賴庫 和Photoshop近似的體驗(yàn) 支持...

    xushaojieaaa 評(píng)論0 收藏0
  • JS組件開發(fā)之面向?qū)ο蠹拔锢砟P途幊?/b>

    摘要:內(nèi)容簡(jiǎn)介,關(guān)于面向?qū)ο?,關(guān)于面向物理模型,示例,總結(jié),關(guān)于面向?qū)ο笾械拿嫦驅(qū)ο笫且粋€(gè)老生常談的問題,可能有人問你的話你也能霹靂啪啦的說一通,比如最常見的,對(duì)象的三要素對(duì)象的名字對(duì)象的屬性對(duì)象的方法例子對(duì)象名示例對(duì)象屬性對(duì)象方法或者稍微高級(jí)一 內(nèi)容簡(jiǎn)介: 1,關(guān)于面向?qū)ο? 2,關(guān)于面向物理模型 3,示例 4,總結(jié) 1,關(guān)于面向?qū)ο?javascript中的面向?qū)ο笫且粋€(gè)老生常談的問...

    cnTomato 評(píng)論0 收藏0
  • 基于zepto的移動(dòng)端輕量級(jí)日期插件

    摘要:本文簡(jiǎn)單介紹近來寫的一款基于的移動(dòng)端輕量級(jí)日期插件。再來看看兼容性對(duì)于移動(dòng)開發(fā)足矣最后就是在綁定事件的兼容性問題,不同廠商對(duì)于這個(gè)事件的定義并不一致,比如里面監(jiān)聽的是事件,但是在安卓里面監(jiān)聽事件完全沒反應(yīng),經(jīng)過一番,發(fā)現(xiàn)安卓需要監(jiān)聽事件。 前言 做過移動(dòng)Web開發(fā)的同學(xué)都知道,移動(dòng)端日期選擇是很常見的需求。在PC端,我們有很豐富的選擇,比較出名的就有Mobiscroll和jQuery ...

    ranwu 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<