<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;
};
}