Maison > Article > interface Web > Introduction à la méthode d'implémentation du plug-in picker (code)
Le contenu de cet article concerne la méthode d'implémentation (code) du plug-in picker. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.
Un plug-in de sélection normal est très détaillé et peut être décrit étape par étape. Lorsque le doigt glisse, le contenu défile avec le doigt. Lorsque le contenu atteint le bas ou atteint le haut, il ne peut pas défiler et le contenu doit toujours rester dans la bonne position.
La première étape consiste à analyser la structure du plug-in
Tout d'abord, il doit y avoir un conteneur de plug-in. L'ensemble du conteneur de plug-in contient un arrière-plan dégradé. ligne et le conteneur de contenu. L'effet est similaire au suivant :
Le code correspondant est donc le suivant :
<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; }
Le code CSS est principalement utilisé comme un affichage de style, via des liens externes Introduction. Je ne vais pas trop expliquer ici.
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)
De cette façon, lorsque le doigt touche le connecteur du composant-. dans le conteneur, cela déclenchera le démarrage et le déplacement, terminera l'événement.
Faites glisser votre doigt vers le haut pour faire glisser le contenu vers le haut et tirez votre doigt vers le bas pour dérouler le contenu. Il vous suffit de contrôler la distance à laquelle la position du contenu change pour être cohérente avec la distance à laquelle votre doigt glisse. L'attribut translate3d(x, y, z) du style de transformation est utilisé ici. Parmi eux, x et z restent inchangés et la valeur de y est la valeur du mouvement du doigt.
Continuons le démontage. Lorsque le doigt est tiré vers le bas, la position du contenu descendra pour être cohérente avec le geste. Autrement dit, la valeur y devient plus grande (notez que la direction positive de l'axe y est vers le bas). Le doigt se relève juste à temps pour glisser vers le haut. Lorsque vous déroulez ou remontez vers le bas, le contenu doit rester inchangé par rapport à la base d'origine. Nous avons donc besoin d'une variable globale __scrollTop pour enregistrer cette valeur. Cette valeur est égale à la somme des valeurs de pull-up et de pull-down de l'utilisateur à chaque fois, nous devons donc connaître la valeur de pull-up et de pull-down de l'utilisateur à chaque fois.
Démontez la valeur extraite par l'utilisateur. L'événement touchstart sera déclenché lorsque l'utilisateur touche l'écran, et l'événement touchmove sera déclenché lorsque l'utilisateur se déplace. L'événement touchend sera déclenché en quittant. La valeur initiale du pull-up de l'utilisateur doit être la position du doigt lorsque le touchstart est déclenché. La valeur finale est la position du doigt au moment du toucher. Mais de cette façon, le contenu ne peut pas suivre le mouvement du doigt en temps réel. Par conséquent, il est nécessaire de démonter l'événement touchmove
L'événement touchmove sera déclenché en continu lorsque le doigt de l'utilisateur bouge, ce qui équivaut aux multiples mouvements de haut en bas extrêmement petits de l'utilisateur. Nous devons donc enregistrer l’endroit où l’utilisateur a touché pour la première fois. __startTouchTop . La soustraction de la position initiale du déclencheur de la position actuelle du doigt correspond à la distance parcourue par l'utilisateur __scrollTop. Le code spécifique est le suivant
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 }
Remarque 1 : touchstart doit enregistrer la position tactile, touchend n'a pas besoin de l'enregistrer. Étant donné que la possibilité que la première position tactile de l'utilisateur et la position tactile suivante soient presque au même endroit est presque mince, la position tactile doit être réinitialisée dans touchstart. Sinon, le contenu clignotera lorsque l'utilisateur le touchera à nouveau
**Remarque 2 : La méthode e.preventDefault() permet de résoudre les problèmes de compatibilité de certains navigateurs et peut améliorer les performances. Par exemple, lorsque vous utilisez le navigateur QQ pour dérouler avec votre doigt, la description du navigateur apparaîtra, provoquant l'échec de la méthode. Vous pouvez vous référer au document https://segmentfault.com/a/1190000014134234
https://www.cnblogs.com/ziyunfei/p/5545439.html**
Apparu dans la méthode touchMoveHandler méthode de rappel ci-dessus_ _. Cette méthode est utilisée pour contrôler la position du conteneur de contenu. La méthode __publish est une couche d'encapsulation pour modifier la position du conteneur. Elle peut se synchroniser avec les mouvements du doigt de l'utilisateur et également déterminer si la position du doigt de l'utilisateur est incorrecte. après l'avoir quitté. Actuellement, le code pour suivre le mouvement du doigt de l'utilisateur
est ici. Si vous utilisez le navigateur pour passer en mode mobile, vous devriez pouvoir faire défiler le contenu avec la souris, mais il existe. encore de nombreux problèmes, qui seront abordés ci-dessous. Résoudre ces problèmes
Actuellement, les utilisateurs peuvent tirez de haut en bas à l'infini, ce qui est évidemment faux. Lorsque la première valeur dépasse légèrement la ligne continue sélectionnée, elle ne peut pas être tirée vers le bas. Lorsque la dernière valeur dépasse légèrement la ligne continue sélectionnée, elle ne peut pas être tirée vers le haut. Nous avons donc besoin de deux valeurs, la valeur de défilement minimale : __minScrollTop et la valeur de défilement maximale : __maxScrollTop
La méthode de calcul devrait être comme ceci : le menu déroulant de l'utilisateur produira une valeur maximale, et la valeur maximale doit être le premier élément a été tiré vers l'emplacement central. Le milieu doit être la position au milieu du conteneur d'éléments
let __maxScrollTop = component.clientHeight / 2 // 滚动最大值
La valeur minimale doit être la position où le dernier élément atteint le milieu lorsque l'utilisateur tire vers le haut, il doit donc s'agir du conteneur de contenu. valeur maximale.
let __minScrollTop = - (content.offsetHeight - __maxScrollTop) // 滚动最小值
Maintenant que les valeurs maximales et minimales sont disponibles, il vous suffit de vous assurer que __scrollTop n'est ni supérieur ni inférieur à la valeur extrême pendant le processus de traction de votre doigt de haut en bas, alors ajoutez le code suivant à la fonction 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>
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!