This article brings you an introduction to the implementation method (code) of the picker plug-in. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
A normal selector plug-in is very detailed and can be described step by step. When the finger slides, the content scrolls with the finger. When the content reaches the bottom or reaches the top, it cannot scroll and the content must always remain in the correct position.
The first step is to analyze the plug-in structure
First there must be a plug-in container. The entire plug-in container contains a gradient background. Select the solid line and the content container. The effect is similar to the following:
So the corresponding code is as follows:
<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 code is mainly used as a style display and is introduced through external links. I won’t explain too much here.
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)
So that when the finger touches the component plug-in container, the start, move, and end will be triggered event.
When the finger slides up, the content slides up, and when the finger slides down, the content slides down. You only need to control the distance at which the position of the content changes to be consistent with the distance at which your finger slides. The translate3d(x, y, z) attribute of the transform style is used here. Among them, x and z remain unchanged, and the value of y is the value of the finger movement.
Let's continue to dismantle it. When the finger is pulled down, the content position will move down to be consistent with the gesture. That is, the y value becomes larger (note that the positive direction of the y-axis is downward). The finger pulls up just in time to slide up. When you pull down or pull up again, the content should remain unchanged on the original basis. Therefore we need a global variable __scrollTop to save this value. This value is equal to the sum of the user's pull-up and pull-down values each time, so we need to find out the user's pull-up and pull-down value each time.
Disassemble the value pulled up by the user. When the user touches the screen, the touchstart event will be triggered, and when the user moves, the touchmove event will be triggered. The touchend event will be triggered when leaving. The initial value of the user's pull-up must be the position of the finger when touchstart is triggered. The end value is the finger position at touchend. But in this way, the content cannot follow the real-time movement of the finger. Therefore, it is necessary to disassemble the touchmove event
The touchmove event will be triggered continuously when the user's finger moves, which is equivalent to the user's multiple extremely small up and down movements. So we need to record the location where the user first touched. __startTouchTop . Subtracting the initial trigger position from the current position of the finger is the distance moved by the user __scrollTop. The specific code is as follows
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 }
Note 1: touchstart must record the touch position, touchend does not need to record. Because the possibility that the user's first touch position and the next touch position are almost at the same place is almost slim, the touch position needs to be reset in touchstart. Otherwise, the content will flash when the user touches it again
**Note 2: The e.preventDefault() method handles compatibility issues with certain browsers and can improve performance. For example, when using QQ browser to pull down with your finger, the browser description will appear, causing the method to fail. You can refer to the document https://segmentfault.com/a/1190000014134234
https://www.cnblogs.com/ziyunfei/p/5545439.html**
appears in the touchMoveHandler method above_ _callback method. This method is used to control the position of the content container. The __publish method is a layer of encapsulation for changing the position of the container. It can synchronize with the user's finger movements, and also determine whether the position of the user's finger is incorrect after leaving it. At present, the code to follow the user's finger movement
is here. If you use the browser to adjust to mobile mode, you should be able to make the content scroll with the mouse, but there are still many problems, which will be discussed below. Fix these problems
Currently, users can pull up and down infinitely, which is obviously wrong. When the first value slightly exceeds the selected solid line, it cannot be pulled down. When the last value slightly exceeds the selected solid line, it cannot be pulled up. So we need two values, the minimum scroll value: __minScrollTop and the maximum scroll value: __maxScrollTop
The calculation method should be like this: the user's pull-down will produce a maximum value, and the maximum value should be the first element pulled down to the middle s position. The middle should be the position in the middle of the element container
let __maxScrollTop = component.clientHeight / 2 // 滚动最大值
The minimum value should be the position where the last element reaches the middle when the user pulls up, so it should be the maximum value of the content container.
let __minScrollTop = - (content.offsetHeight - __maxScrollTop) // 滚动最小值
Now that the maximum and minimum values are available, you only need to ensure that __scrollTop is not greater than or not less than the extreme value during the process of pulling your finger up and down, so add the following code to the touchMoveHandler function
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>
The above is the detailed content of Introduction to the implementation method of the picker plug-in (code). For more information, please follow other related articles on the PHP Chinese website!