原生js实现移动端滑动吸附效果
原生js实现移动端时间滑动选择器(之前在群里讨论过的吸附的效果)
https://blog.csdn.net/xiao_chen00012/article/details/126411458
使用CSS实现滚动吸附效果
https://mp.weixin.qq.com/s/z_MMaO3tAd9Sqwaulxwrag
作者:简单coder
链接:https://juejin.cn/post/6886464152374771719
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0, user-scalable=no"> <title>picker - 滑动选择组件</title> <style> html, body { margin: 0; padding: 0; } .picker-demo { width: 360px; height: 264px; margin: auto; } /* 遮罩层区域 */ .picker { width: 100%; height: 100%; position: relative; } .mask { position: absolute; top: 0; left: 0; z-index: 2; width: 100%; height: 100%; background-image: linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)), linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)); background-repeat: no-repeat; background-position: top, bottom; pointer-events: none; } .cover-border { position: absolute; z-index: 3; top: 50%; left: 16px; right: 16px; transform: translateY(-50%); pointer-events: none; } .cover-border::after { position: absolute; box-sizing: border-box; content: ' '; pointer-events: none; top: -50%; right: -50%; bottom: -50%; left: -50%; /* border: 0 solid #ebedf0; */ border: 0 solid #aaa; -webkit-transform: scale(0.5); transform: scale(0.5); border-width: 1px 0; } /* 内容区域 */ ul, li { list-style: none; margin: 0; padding: 0; } .picker-column { width: 100%; height: 100%; position: relative; overflow: hidden; } .column-item { display: flex; align-items: center; justify-content: center; padding: 0 4px; color: #000; } </style> </head> <body> <!-- 外部容器 --> <div> <!-- 组件 --> <div> <!-- 滚动内容 --> <div> <ul ref="wrapper"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> </ul> </div> <!-- 遮照层 --> <div></div> <div :style="frameStyle"></div> </div> </div> <script> const DEMO_DATA = ['2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018', '2019', '2020'] class Picker { DEFAULT_DURATION = 200; MIN_DISTANCE = 10; // 惯性滑动思路: // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move // 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动 MOMENTUM_LIMIT_TIME = 30; MOMENTUM_LIMIT_DISTANCE = 15; supportsPassive = false; constructor(options = {}) { this.initValue(options) this._initValue(options) this.resetTouchStatus() this.initComputed(options) this.setEleStyle() this.onMounted() } // 私有变量 _initValue(options) { this.offset = 0 this.duration = 0 this.options = this.initOptions this.direction = options.direction || 'vertical' this.deltaX = 0 this.deltaY = 0 this.offsetX = 0 this.offsetY = 0 this.startX = 0 this.startY = 0 this.moving = false this.startOffset = 0 this.transitionEndTrigger = null // 滚动函数 this.touchStartTime = 0 // 记录开始滑动时间 this.momentumOffset = 0 // 记录开始滑动位置 this.currentIndex = this.defaultIndex } // 初始化--用户变量 initValue(options) { // 可是区域子元素个数 this.visibleItemCount = Number(options.visibleItemCount || 6) || 6; // 子元素高度 this.itemPxHeight = Number(this.itemPxHeight) || 44; // 初始化传入的数据列表(当前案例微用到,可结合框架使用) this.initOptions = options.initOptions || DEMO_DATA; // 是否只读 this.readonly = options.readonly || false; // 初始显示元素(当前案例未使用,可结合框架扩展) this.defaultIndex = Number(options.defaultIndex) || 0; } // 根据传入变量--获取计算属性 initComputed(options) { // 外层容器高度 this.wrapHeight = this.itemPxHeight * this.visibleItemCount this.maskStyle = { backgroundSize: `100% ${(this.wrapHeight - this.itemPxHeight) / 2}px` } this.frameStyle = { height: `${this.itemPxHeight}px` } // this.count = this.options.length this.count = document.querySelector('.wrapper-container').children.length this.baseOffset = (this.itemPxHeight * (this.visibleItemCount - 1)) / 2 // 内层元素高度计算 this.wrapperStyle = { transform: `translate3d(0, ${this.offset + this.baseOffset}px, 0)`, transitionDuration: `${this.duration}ms`, transitionProperty: this.duration ? 'all' : 'none', } } // 设置外部容器的样式及遮罩层 setEleStyle() { let mask = document.querySelector('.mask') let coverBorder = document.querySelector('.cover-border') let columnItem = document.querySelectorAll('.column-item') mask.style.backgroundSize = this.maskStyle.backgroundSize coverBorder.style.height = this.frameStyle.height this.setUlStyle() this.setColumnHeight(columnItem) } // 滑动主要逻辑--动态设置容器的垂直方向偏移量 setUlStyle() { let wrapperContainer = document.querySelector('.wrapper-container') wrapperContainer.style.transform = this.wrapperStyle.transform wrapperContainer.style.transitionDuration = this.wrapperStyle.transitionDuration wrapperContainer.style.transitionProperty = this.wrapperStyle.transitionProperty } setUlTransform() { this.initComputed() this.setUlStyle() } // 设置每个行元素的高度及点击事件 setColumnHeight(columnItem) { columnItem.forEach((item, index) => { item.style.height = `${this.itemPxHeight}px` item.tabindex = index item.onclick = () => { this.onClickItem(index) this.setUlTransform() } }) } // 点击单个行元素 onClickItem(index) { if (this.moving || this.readonly) { return; } this.transitionEndTrigger = null; this.duration = this.DEFAULT_DURATION; this.setIndex(index, true); } // 初始化完成--执行事件绑定 onMounted() { let el = document.querySelector('.picker-column') this.bindTouchEvent(el) } bindTouchEvent(el) { const { onTouchStart, onTouchMove, onTouchEnd, onTransitionEnd } = this let wrapper = document.querySelector('.wrapper-container') this.on(el, 'touchstart', onTouchStart); this.on(el, 'touchmove', onTouchMove); this.on(wrapper, 'transitionend', onTransitionEnd) if (onTouchEnd) { this.on(el, 'touchend', onTouchEnd); this.on(el, 'touchcancel', onTouchEnd); } } on(target, event, handler, passive = false) { target.addEventListener( event, handler, this.supportsPassive ? { capture: false, passive } : false ); } // 动画结束事件 onTransitionEnd = () => { this.stopMomentum(); } // 滑动结束后数据获取及优化处理 stopMomentum() { this.moving = false; this.duration = 0; if (this.transitionEndTrigger) { this.transitionEndTrigger(); this.transitionEndTrigger = null; } } // 开始滑动 onTouchStart = (event) => { // 控制只读 if (this.readonly) return let wrapper = document.querySelector('.wrapper-container') this.touchStart(event) if (this.moving) { const translateY = this.getElementTranslateY(wrapper); this.offset = Math.min(0, translateY - this.baseOffset); this.startOffset = this.offset; } else { this.startOffset = this.offset; } this.duration = 0; this.transitionEndTrigger = null; this.touchStartTime = Date.now(); this.momentumOffset = this.startOffset; // 设置滑动 this.setUlTransform() } touchStart(event) { this.resetTouchStatus(); this.startX = event.touches[0].clientX; this.startY = event.touches[0].clientY; } // 重置滑动数据变量 resetTouchStatus() { this.direction = ''; this.deltaX = 0; this.deltaY = 0; this.offsetX = 0; this.offsetY = 0; } // 动态获取元素滑动距离--关键 getElementTranslateY(element) { const style = window.getComputedStyle(element); const transform = style.transform || style.webkitTransform; const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; return Number(translateY); } onTouchMove = (event) => { if (this.readonly) return this.touchMove(event) if (this.direction === 'vertical') { this.moving = true; this.preventDefault(event, true); } this.offset = this.range(this.startOffset + this.deltaY, -(this.count * this.itemPxHeight), this .itemPxHeight); const now = Date.now() if (now - this.touchStartTime > this.MOMENTUM_LIMIT_TIME) { this.touchStartTime = now; this.momentumOffset = this.offset; } // 滑动中 this.setUlTransform() } onTouchEnd = (event) => { if (this.readonly) return const distance = this.offset - this.momentumOffset; const duration = Date.now() - this.touchStartTime; const allowMomentum = duration < this.MOMENTUM_LIMIT_TIME && Math.abs(distance) > this.MOMENTUM_LIMIT_DISTANCE if (allowMomentum) { this.momentum(distance, duration); return; } const index = this.getIndexByOffset(this.offset); this.duration = this.DEFAULT_DURATION; this.setIndex(index, true) // 滑动结束 this.setUlTransform() // compatible with desktop scenario // use setTimeout to skip the click event triggered after touchstart setTimeout(() => { this.moving = false }, 0); } // 滑动动画函数--关键 momentum(distance, duration) { const speed = Math.abs(distance / duration); distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1); const index = this.getIndexByOffset(distance); this.duration = +this.swipeDuration; this.setIndex(index, true); } // 获取当前展示的元素数据信息--关键 setIndex(index, emitChange) { index = this.adjustIndex(index) || 0; const offset = -index * this.itemPxHeight; const trigger = () => { if (index !== this.currentIndex) { this.currentIndex = index; if (emitChange) { // this.$emit('change', index); console.log(index) } } }; // trigger the change event after transitionend when moving if (this.moving && offset !== this.offset) { this.transitionEndTrigger = trigger; } else { trigger(); } this.offset = offset; } getValue() { return this.options[this.currentIndex]; } adjustIndex(index) { index = this.range(index, 0, this.count); for (let i = index; i < this.count; i++) { if (!this.isOptionDisabled(this.options[i])) return i; } for (let i = index - 1; i >= 0; i--) { if (!this.isOptionDisabled(this.options[i])) return i; } } isOptionDisabled(option) { return this.isObject(option) && option.disabled; } isObject(val) { return val !== null && typeof val === 'object'; } // 滑动偏移量 getIndexByOffset(offset) { return this.range(Math.round(-offset / this.itemPxHeight), 0, this.count - 1); } // 阻止默认行为 preventDefault(event, isStopPropagation) { /* istanbul ignore else */ if (typeof event.cancelable !== 'boolean' || event.cancelable) { event.preventDefault(); } if (isStopPropagation) { this.stopPropagation(event); } } stopPropagation(event) { event.stopPropagation(); } touchMove(event) { const touch = event.touches[0]; this.deltaX = touch.clientX - this.startX; this.deltaY = touch.clientY - this.startY; this.offsetX = Math.abs(this.deltaX); this.offsetY = Math.abs(this.deltaY); this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY); } // 确定滑动方向 getDirection(x, y) { if (x > y && x > this.MIN_DISTANCE) { return 'horizontal'; } if (y > x && y > this.MIN_DISTANCE) { return 'vertical'; } return ''; } // 滑动范围限制--关键代码 range(num, min, max) { return Math.min(Math.max(num, min), max); } } new Picker() </script> </body> </html>
Leave a comment
0 Comments.