この記事では、ピッカープラグインの実装方法(コード)を紹介します。参考価値はありますので、困っている方は参考にしていただければ幸いです。
通常のセレクター プラグインは非常に詳細であり、ステップごとに説明できます。指をスライドさせると、コンテンツも指と一緒にスクロールしますが、コンテンツが一番下に達したり、一番上に達したりするとスクロールできなくなり、コンテンツは常に正しい位置に留まる必要があります。
最初のステップは、プラグイン構造を分析することです
まず、プラグイン コンテナーが必要です。プラグイン コンテナー全体には、グラデーションの背景が含まれています。実線とコンテンツコンテナ。効果は次のようになります:
したがって、対応するコードは次のとおりです:
<div> <div></div> <div></div> <div> <div>1</div> <div>2</div> <div>3</div> <div>4</div> <div>5</div> <div>6</div> <div>7</div> <div>8</div> <div>9</div> <div>10</div> <div>11</div> <div>12</div> <div>13</div> <div>14</div> <div>15</div> <div>16</div> <div>17</div> <div>18</div> <div>19</div> <div>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 コードは主にスタイル表示として使用され、導入されています。外部リンク経由。ここではあまり説明しません。
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)
指がコンポーネント プラグイン コンテナに触れると、開始、 move、end がトリガーされるイベントです。
指を上にスライドさせるとコンテンツが上にスライドし、指を下にスライドさせるとコンテンツが下にスライドします。指をスライドさせる距離と一致するように、コンテンツの位置が変化する距離を制御するだけで済みます。ここでは、変換スタイルのtranslate3d(x, y, z)属性が使用されます。このうち、xとzはそのままで、yの値が指の動きの値となります。
分解を続けましょう。指を下に引くと、ジェスチャと一致するようにコンテンツの位置が下に移動します。つまり、y 値が大きくなります (y 軸の正方向は下方向であることに注意してください)。ちょうど上にスライドするタイミングで指が引き上げられます。プルダウンまたは再度プルアップしても、内容は元の状態のままである必要があります。したがって、この値を保存するにはグローバル変数 __scrollTop が必要です。この値は毎回のユーザーのプルアップ値とプルダウン値の合計に等しいため、毎回ユーザーのプルアップ値とプルダウン値を調べる必要があります。
ユーザーが引き上げた値を逆アセンブルすると、ユーザーが画面に触れるとtouchstartイベントがトリガーされ、ユーザーが移動するとtouchmoveイベントがトリガーされます。離れるときに 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 は記録する必要はありません。ユーザーの最初のタッチ位置と次のタッチ位置がほぼ同じ場所である可能性はほとんどないため、タッチスタート時にタッチ位置を再設定する必要があります。そうしないと、ユーザーが再度タッチしたときにコンテンツが点滅します。
**注 2: e.preventDefault() メソッドは、特定のブラウザとの互換性の問題を処理し、パフォーマンスを向上させることができます。たとえば、QQ ブラウザを使用して指でプルダウンすると、ブラウザの説明が表示され、メソッドが失敗します。 touchMoveHandler メソッドに含まれるドキュメント https://segmentfault.com/a/1190000014134234
https://www.cnblogs.com/ziyunfei/p/5545439.html**
を参照してください。上記の_ _callbackメソッド。このメソッドは、コンテンツ コンテナの位置を制御するために使用されます。__publish メソッドは、コンテナの位置を変更するためのカプセル化の層です。ユーザーの指の動きと同期し、ユーザーの指の位置が間違っているかどうかも判断できます放置した後。現時点では、ユーザーの指の動きを追跡するコード
がここにあります。ブラウザをモバイル モードに調整する場合は、マウスでコンテンツをスクロールできるはずですが、これらの問題を修正してください
現在、ユーザーは次のことを行うことができます。無限に上下に引っ張りますが、これは明らかに間違っています。最初の値が選択した実線をわずかに超えた場合はプルダウンできず、最後の値が選択した実線をわずかに超えた場合はプルアップできません。したがって、最小スクロール値: __minScrollTop と最大スクロール値: __maxScrollTop
という 2 つの値が必要です。計算方法は次のようになります。ユーザーのプルダウンによって最大値が生成され、最大値は次のようになります。最初の要素が中央の位置まで引き下げられました。中央は要素コンテナの中央の位置である必要があります
let __maxScrollTop = component.clientHeight / 2 // 滚动最大值
最小値はユーザーが引き上げたときに最後の要素が中央に到達する位置である必要があるため、コンテンツの最大値になる必要があります容器。
let __minScrollTop = - (content.offsetHeight - __maxScrollTop) // 滚动最小值
最大値と最小値が利用できるようになったので、必要なのは、指を上下に引っ張るプロセス中に __scrollTop が極値より大きくないか、極値より小さくないことを確認することだけです。 touchMoveHandler 関数への次のコード
if (scrollTop > __maxScrollTop || 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) { 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 (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) { // 保持减速运行需要多少速度 let shouldContinue = Math.abs(__deceleratingMove) >= minVelocityToKeepDecelerating return shouldContinue } let completed = function (renderedFramesPerSecond, animationId, wasFinished) { __isDecelerating = false if (__scrollTop = __maxScrollTop) { __scrollTo(__scrollTop) return } } __isDecelerating = Animate.start(step, verify, completed) }
当你把这些代码都加进去的时候,选择器插件基本上就已经完成了。下面讲解一下这段让你头痛的代码。
这里面用到了动画,所以肯定包含三大回调函数 step, verify, completed。然后一个一个讲解一下
step函数:这个函数是让内容一步一步运动的,这个函数基本上跟滚动到正确位置的函数相似度很高。 新的位置是老位置 __scrollTop 加上每帧移动的位置 __deceleratingMove。 然后让每帧移动的位置一直减少,但是需要注意 scrollTop 不能超出极值,所以做了最大值最小值判断当到达极值的时候就将 __deceleratingMove 赋值为0 。
if (Math.abs(__deceleratingMove) <p>这段代码,你可能佷懵。他的作用是当滚动的位置没有到达极值的时候如何让他卡在正确位置上。 Math.abs(__deceleratingMove) 这是每帧移动的距离的绝对值。当他小于1的时候说明移动的距离已经非常小了,用户基本上都察觉不到移动了。然后再用新位置对元素高度取余,如果余数为0表示正好卡在正确位置上,但是即使稍微比 0 大那么一丢丢也看不出来,而且基本不会那么巧取到 0,所以当余数满足小于 1 的时候讲每帧移动的距离赋值为0.</p><p>verify函数:定义了一个最小每帧移动距离的局部变量 minVelocityToKeepDecelerating, 当 __deceleratingMove 值小于他的时候说明用户基本上不会发现内容还在移动可以停下来了。</p><p>completed函数:既然是完成函数就一定要将 __isDecelerating 参数变为false,否则下次进行的不是快速移动内容就没法跑到正确位置上了。这里多加了一步是否是极值的判断,如果是极值就执行 __scrollTo 函数到正确位置上。</p><p>本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的<a href="http://www.php.cn/course/list/12.html" target="_blank">CSS视频教程</a>栏目!</p><p></p>
以上がピッカープラグインの実装方法(コード)の紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。