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展示 ,具体的代码实现可以看 这里