ios中select标签在focus的时候,会唤起原生的ios选择组件,样式上是一个3d的滚筒,滚动选择时流畅、平滑,截图如下:
而在android设备里,原生的select组件就没有ios设备里的漂亮和流畅了
原生的select组件在不同平台有不同的UI表现,事件处理也各有各的坑,统一UI才是王道。那么该如何模拟实现这种3d滚动选择组件呢?
试着想象一下,将一张长方形的纸卷曲形成圆柱体的过程,无非就是将无穷多个细小的长方形,以圆柱体的中心轴为基准,旋转一定的角度。只要角度是连续的,就可以形成曲面。 这段话一时难以理解。我们认为每一个选择项的dom节点一开始是平面的,2d的,通过transform的rotateX(angle),rotateY(angle),rotateZ(angle)来实现3d旋转。
先简单理解一下rotateX,rotateY,rotateZ的直观效果
(注意,要搭配透视perspective属性才能看到上面的效果)
很明显我们应该使用rotateX属性来形成曲面。但是,但是,有一个属性刚刚被我们忽略了。那就是transform-origin属性,为了形成3d的曲面,除了rotate的角度,还有一个关键性 的因为,就是transform-origin了。平时在缩放元素的时候,都会用到tansform-origin,但我们只用到了x轴和y轴, 其实还有一个z轴。为了形成曲面,需要指定这个z轴的值。这个z轴的值应该是多少呢(一脸懵b)。
下面来细细解释一下,transform-origin的定义如下:
transform-origin: x-axis y-axis z-axis;
到底怎么理解呢?来点实际的
蒙眼飞刀的transform-origin是x(center),y(center)
钢管舞的transform-origin是x(0),y(随意)
那么圆柱体呢?其实它的transform-origin是x(center),y(center),z(圆柱体底面的圆的半径)。
这个圆个半径如何计算呢?
图中可视区域的高度,其实就是圆的直径(179px),那么半径就是89.5px。
<ul class="mui-pciker-list" style="transform-origin: center center -89.5px; transform: perspective(1000px) rotateY(0deg) rotateX(60deg); transition: 100ms ease-out;"> <li class="visible" style="transform-origin: center center -89.5px; transform: rotateX(0deg);">选项1</li> <li class="visible" style="transform-origin: center center -89.5px; transform: rotateX(-20deg);">选项2</li> <li class="visible" style="transform-origin: center center -89.5px; transform: rotateX(-40deg);">选项3</li> <li class="visible highlight" style="transform-origin: center center -89.5px; transform: rotateX(-60deg);">选项4</li> <li class="visible" style="transform-origin: center center -89.5px; transform: rotateX(-80deg);">选项5</li> <li style="transform-origin: center center -89.5px; transform: rotateX(-100deg);" class="visible">选项6</li> <li style="transform-origin: center center -89.5px; transform: rotateX(-120deg);" class="visible">选项7</li> <li style="transform-origin: center center -89.5px; transform: rotateX(-140deg);" class="visible">选项8</li> </ul>
在上面的代码中,将每个选择项的 transform-origin
的值设置为 center center -89.5px
,同时将ul的 transform-origin
也设置为相同的值,这样ul渲染后会有上面的3d曲面效果。接下来,就是监听touchstart,touchmove,touchend(或者mousedown,mousemove,mouseup)事件,在touchmove(或者mousemove)事件处理函数中 对拨动的角度的计算。
已知手势滑动的绝对值(或鼠标滑动的绝对值),和3d曲面半径,求曲面应该转动的角度。等同于下面的三角形,已经三条边的长度,求夹角
余弦定理的计算公式如下:
求的余弦值后,通过反余弦函数求得角度,代码如下:
/** * 弧度转换成角度 * @param rad * @returns {number} */ function rad2deg(rad) { return rad / (Math.PI / 180); } /** * 计算旋转角度 * @param deltaPageY 手势滑动的绝对值(或者鼠标滑动的绝对值) * @returns {*} */ function calcAngle(deltaPageY) { var self = this; var a = b = parseFloat(self.r); //直径的整倍数部分直接乘以 180 c = Math.abs(c); //只算角度不关心正否值 var intDeg = parseInt(c / self.d) * 180; c = c % self.d; //余弦 var cosC = (a * a + b * b - c * c) / (2 * a * b); var angleC = intDeg + rad2deg(Math.acos(cosC)); console.log('angleC=' + angleC); return angleC; } /** * 更新ul的旋转角度 * @param angle */ function setAngle(angle) { var self = this; self.list.angle = angle; self.list.style.webkitTransformOrigin = 'center center -89.5px'; self.list.style.webkitTransform = "perspective(1000px) rotateY(0deg) rotateX(" + angle + "deg)"; self.calcElementItemVisibility(angle); }
调用calcAngle(deltaPageY)后,再调用setAngle(),将ul的rotateX值进行更新。
在touchend(或者mousemove)事件处理函数中,需要实现减速效果。
将滑动的距离转换成角度,然后调用缓动函数,计算每个时间点应该转动的角度,直至转动停止
/** * 计算最终应该旋转的角度和时间 * @param event */ function startInertiaScroll(event) { var self = this; var point = event.changedTouches ? event.changedTouches[0] : event; var nowTime = event.timeStamp || Date.now(); console.log('移动距离=' + (point.pageY - self.lastMoveStart)); console.log('移动时间=' + (nowTime - self.lastMoveTime)); var v = (point.pageY - self.lastMoveStart) / (nowTime - self.lastMoveTime); //最后一段时间手指划动速度 var dir = v > 0 ? -1 : 1; //加速度方向 var deceleration = dir * 0.0006 * -1; console.log('速度' + v); console.log('@@@@@@@' + deceleration + '@@@@@@@@@@'); var duration = Math.abs(v / deceleration); // 速度消减至0所需时间 console.log('#########'+ duration + '########'); var dist = v * duration / 2; //最终移动多少 console.log('最终dist' + dist); var startAngle = self.list.angle; var distAngle = self.calcAngle(dist) * dir; console.log('需要转的角度' + distAngle); //---- var srcDistAngle = distAngle; if (startAngle + distAngle < self.beginExceed) { distAngle = self.beginExceed - startAngle; duration = duration * (distAngle / srcDistAngle) * 0.6; } if (startAngle + distAngle > self.endExceed) { distAngle = self.endExceed - startAngle; duration = duration * (distAngle / srcDistAngle) * 0.6; } //---- if (distAngle == 0) { self.endScroll(); return; } self.scrollDistAngle(nowTime, startAngle, distAngle, duration); } /** * 缓动函数 * @param nowTime * @param startAngle * @param distAngle * @param duration */ function scrollDistAngle(nowTime, startAngle, distAngle, duration) { var self = this; self.stopInertiaMove = false; (function(nowTime, startAngle, distAngle, duration) { var frameInterval = 13; var stepCount = duration / frameInterval; var stepIndex = 0; (function inertiaMove() { if (self.stopInertiaMove) return; var newAngle = self.quartEaseOut(stepIndex, startAngle, distAngle, stepCount); self.setAngle(newAngle); stepIndex++; if (stepIndex > stepCount - 1 || newAngle < self.beginExceed || newAngle > self.endExceed) { self.endScroll(); return; } setTimeout(inertiaMove, frameInterval); })(); })(nowTime, startAngle, distAngle, duration); }
上面讲的是一般选择组件的实现,日历选择组件,只需要改一下UI,将一个曲面变成多个曲面,比例,生日的选择,需要年、月、日三个3d曲面
demo展示 ,具体的代码实现可以看 这里