使用vue写一个picker插件,使用3d滚轮的原理

tech2022-09-02  97

<template> <div class="picker-container"> <div> <transition name="myOpacity"> <section class="pop-cover" @touchstart="close" v-show="value"></section> </transition> <transition name="myPopup"> <section v-if="value"> <div class="btn-box"><button @touchstart="close">取消</button><button @touchstart="sure">确认</button></div> <section class="aaa"> <div class="gg" :style="ggStyle"> <div class="col-wrapper" :style="getWrapperHeight"> <ul class="wheel-list" :style="getListTop" ref="wheel"> <li class="wheel-item" v-for="(item,index) in values" :style="initWheelItemDeg(index)">{{item}}</li> </ul> <div class="cover" :style="getCoverStyle"></div> <div class="divider" :style="getDividerStyle"></div> </div> </div> </section> </section> </transition> </div> </div> </template> <script> import Animate from '../../../utils/animate'; const a = -0.003; // 加速度 let radius = 2000; // 半径--console.log(Math.PI*2*radius/lineHeight)=>估算最多可能多少条,有较大误差,半径2000应该够用了,不够就4000 const lineHeight = 36; // 文字行高 let isInertial = false; // 是否正在惯性滑动 // 反正切=>得到弧度再转换为度数,这个度数是单行文字所占有的。 let singleDeg = (Math.atan(lineHeight/radius) * 180)/ Math.PI; const remUnit = 37.5; // px转rem的倍数 export default { props:{ cuIdx: {type: Number,default: 0}, value: false, values: {type: Array,default:() => []} }, data() { return { finger: { startY: 0, startTime: 0, // 开始滑动时间(单位:毫秒) currentMove: 0, prevMove: 0, }, currentIndex: 0, }; }, computed: { // 限制滚动区域的高度,内容正常显示--以下皆未使用this,所以可以用箭头函数简化写法 ggStyle: () => ({ maxHeight: `${radius/remUnit}rem`, transform: `translateY(-${(radius - (300+40-lineHeight)/2)/remUnit}rem` }), // 3d滚轮的内容区域样式--ref=wheel的元素样式 getListTop: () => ({ top: `${(radius - (lineHeight / 2))/remUnit}rem`, height: `${lineHeight/remUnit}rem` }), // 滚轮的外包装理想样式--展示半径的内容可见,另外的半径隐藏 getWrapperHeight: () => ({ height: `${2 * radius/remUnit}rem` }), // 参照一般居中的做法,50%*父页面的高度(整个圆的最大高度是直径)-居中内容块(文本的行高)的一半高度 getCoverStyle: () => ({ backgroundSize: `100% ${(radius - (lineHeight / 2))/remUnit}rem` }), // 应该也是参照居中的做法 getDividerStyle:() => ({ top: `${(radius - (lineHeight / 2))/remUnit}rem`,height: `${(lineHeight - 2)/remUnit}rem` }), animate: () => (new Animate()), }, mounted() { this.$el.addEventListener('touchstart', this.listenerTouchStart, false); this.$el.addEventListener('touchmove', this.listenerTouchMove, false); this.$el.addEventListener('touchend', this.listenerTouchEnd, false); }, beforeDestory() { this.$el.removeEventListener('touchstart', this.listenerTouchStart, false); this.$el.removeEventListener('touchmove', this.listenerTouchMove, false); this.$el.removeEventListener('touchend', this.listenerTouchEnd, false); }, methods: { initWheelItemDeg(index) {// 初始化时转到父页面传递的下标所对应的选中的值 return { transform: `rotate3d(1, 0, 0, ${(-1 * index +Number(this.cuIdx)) * singleDeg}deg) translate3d(0, 0, ${radius/remUnit}rem)`, height: `${lineHeight/remUnit}rem`, lineHeight: `${lineHeight/remUnit}rem` }; }, listenerTouchStart(ev) { ev.stopPropagation(); ev.preventDefault(); isInertial = false; // 初始状态没有惯性滚动 this.finger.startY = ev.targetTouches[0].pageY; // 获取手指开始点击的位置 this.finger.prevMove = this.finger.currentMove; this.finger.startTime = Date.now(); }, listenerTouchMove(ev) { ev.stopPropagation(); ev.preventDefault(); this.finger.currentMove = (this.finger.startY - ev.targetTouches[0].pageY) + this.finger.prevMove; this.$refs.wheel.style.transform = `rotate3d(1, 0, 0, ${(this.finger.currentMove / lineHeight) * singleDeg}deg)`; }, listenerTouchEnd(ev) { ev.stopPropagation(); ev.preventDefault(); const _endY = ev.changedTouches[0].pageY; const _entTime = Date.now(); const v = (this.finger.startY - _endY)/ (_entTime - this.finger.startTime);// 滚动完毕求移动速度 v = (s初始-s结束) / t const absV = Math.abs(v); isInertial = true;// 最好惯性滚动,才不会死板 this.inertia(absV, Math.round(absV / v), 0);// Math.round(absV / v)=>+/-1 }, /**用户结束滑动,应该慢慢放慢,最终停止。从而需要 a(加速度) * @param start 开始速度 @param position 速度方向,值: 正负1--向上是+1,向下是-1 @param target 结束速度 */ inertia(start, position, target) { if (start <= target || !isInertial) { this.animate.stop(); this.finger.prevMove = this.finger.currentMove; this.getSelectValue(this.finger.currentMove);// 得到选中的当前下标 return; } // 因为在开始的时候加了父页面传递的下标,这里需要减去才能够正常使用 const minIdx = 0-this.cuIdx; const maxIdx = this.values.length-1-this.cuIdx; const freshTime = 1000 / 60;// 动画帧刷新的频率大概是1000 / 60 // 这段时间走的位移 S = vt + 1/2at^2 + s1; const move = (position * start * freshTime) + (0.5 * a * Math.pow(freshTime,2)) + this.finger.currentMove; const newStart = (position * start) + (a * freshTime);// 根据求末速度公式: v末 = (+/-)v初 + at let moveDeg = Math.round(move / lineHeight) * singleDeg;// 正常的滚动角度 let actualMove = move; // 最后的滚动距离 this.$refs.wheel.style.transition = ''; // 已经到达目标 if (newStart <= target) { // 让滚动在文字区域内,超出区域的滚回到边缘的第一个文本处 if(Math.round(move / lineHeight) < minIdx) { moveDeg = minIdx*singleDeg; actualMove =minIdx*lineHeight; }else if(Math.round(move / lineHeight) > maxIdx) { moveDeg = maxIdx*singleDeg; actualMove = maxIdx*lineHeight; } this.$refs.wheel.style.transition = 'transform 700ms cubic-bezier(0.19, 1, 0.22, 1)'; } // this.finger.currentMove赋值是为了点击确认的时候可以使用=>获取选中的值 this.finger.currentMove = actualMove; this.$refs.wheel.style.transform = `rotate3d(1, 0, 0, ${moveDeg}deg)`; this.animate.start(this.inertia.bind(this, newStart, position, target)); }, // 滚动时及时获取最新的当前下标--因为在初始化的时候减去了,所以要加上cuIdx,否则下标会不准确 getSelectValue(move) { this.currentIndex = Math.round(move / lineHeight) + Number(this.cuIdx); }, sure() {// 点击确认按钮 this.getSelectValue(this.finger.currentMove); this.$nextTick(()=>{ this.$emit('select', this.values[this.currentIndex]); this.close(); }); }, close() { this.$nextTick(()=>{ this.$emit('input',false); }); },// 点击取消按钮 }, }; </script> <style lang="scss" scoped> @function px2rem($px) { $item: 37.5px; @return $px/$item+rem; } .myOpacity-enter,.myOpacity-leave-to {opacity: 0;} .myOpacity-enter-active,.myOpacity-leave-active {transition: all .5s ease;} .myPopup-enter,.myPopup-leave-to {transform: translateY(100px);} .myPopup-enter-active,.myPopup-leave-active {transition: all .5s ease;} .picker-container {position: fixed;bottom: 0;left: 0;right: 0;} .pop-cover {position: fixed;top: 0;left: 0;right: 0;height: 100vh;background: rgba(0,0,0,0.5);z-index: -1;} .btn-box {height: px2rem(40px);background: rgb(112,167,99);display: flex;justify-content: space-between;} .aaa {height: px2rem(300px);overflow: hidden;}//overflow: hidden=>截掉多余的部分,显示弹窗内容部分 ul, li{list-style: none;padding: 0;margin: 0;} // 为了方便掌握重点样式,简单的就直接一行展示,其他的换行展示,方便理解 .col-wrapper{ position: relative; border: 1px solid #CCC;text-align: center;background: #fff; .wheel-list{ position: absolute; width: 100%; transform-style: preserve-3d; transform: rotate3d(1, 0, 0, 0deg); .wheel-item{ backface-visibility: hidden; position: absolute; left: 0; top: 0; width: 100%; } } .cover{ position: absolute;left: 0;top: 0;right: 0;bottom: 0; background: linear-gradient(180deg,hsla(0,0%,100%,.95),hsla(0,0%,100%,.6)),linear-gradient(0deg,hsla(0,0%,100%,.95),hsla(0,0%,100%,.6)); background-position: top,bottom; background-repeat: no-repeat; } .divider{ position: absolute;width: 100%;left: 0; border-top: 1px solid #cccccc;border-bottom: 1px solid #cccccc; } } </style>

 

 

父页面

<template> <div id="app"> <!-- <router-view/> --> <ul> <li> <span>{{selected}}</span> </li> </ul> <ios-select @select="getSelectValue" :cuIdx="cuIdx" :values="values" v-model="show" class="picker"></ios-select> </div> </template> <script> import iosSelect from './views/iosSelect/components/SelectColumn'; export default { components :{ iosSelect }, data() { return { selected:'', cuIdx: 1, show: true, values: [ 999,1,452,153,4,5,6,7,8999,9,10, 11,12,13,14,15,16,17,18,19,20, ], } }, mounted() { //获取屏幕宽度(viewport) let htmlWidth = document.documentElement.clientWidth || document.body.clientWidth; console.log(htmlWidth); //获取htmlDom let htmlDom = document.getElementsByTagName('html')[0]; //设置html的font-size htmlDom.style.fontSize = htmlWidth/10+'px'; window.addEventListener('resize',(e)=>{ let htmlWidth = document.documentElement.clientWidth || document.body.clientWidth; console.log(htmlWidth); //获取htmlDom let htmlDom = document.getElementsByTagName('html')[0]; //设置html的font-size htmlDom.style.fontSize = htmlWidth/10+'px'; }); }, methods: { getSelectValue(value) { this.selected = value; } } } </script> <style lang="scss"> @function px2rem($px) { $item: 37.5px; @return $px/$item+rem; } * { margin: 0; padding: 0; } body ,html{ width: 100%; } .picker { } </style>

anmate.js

export default class Animate { constructor() { this.timer = null; } start = (fn) => { if (!fn) { throw new Error('需要执行函数'); } if (this.timer) { this.stop(); } this.timer = requestAnimationFrame(fn); }; stop = () => { if (!this.timer) { return; } cancelAnimationFrame(this.timer); this.timer = null; }; }

 

最新回复(0)