h5日历选择组件

Published: by Creative Commons Licence

ios中原生的select组件

ios中select标签在focus的时候,会唤起原生的ios选择组件,样式上是一个3d的滚筒,滚动选择时流畅、平滑,截图如下:

img

而在android设备里,原生的select组件就没有ios设备里的漂亮和流畅了

img

统一UI

原生的select组件在不同平台有不同的UI表现,事件处理也各有各的坑,统一UI才是王道。那么该如何模拟实现这种3d滚动选择组件呢?

3d特效

img

试着想象一下,将一张长方形的纸卷曲形成圆柱体的过程,无非就是将无穷多个细小的长方形,以圆柱体的中心轴为基准,旋转一定的角度。只要角度是连续的,就可以形成曲面。 这段话一时难以理解。我们认为每一个选择项的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;

  • x-axis定义视图被置于x轴的何处
  • y-axis定义视图被置于y轴的何处
  • z-axis定义视图被置于z轴的何处

到底怎么理解呢?来点实际的

img

蒙眼飞刀的transform-origin是x(center),y(center)

img

钢管舞的transform-origin是x(0),y(随意)

那么圆柱体呢?其实它的transform-origin是x(center),y(center),z(圆柱体底面的圆的半径)。

这个圆个半径如何计算呢?

img

图中可视区域的高度,其实就是圆的直径(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曲面半径,求曲面应该转动的角度。等同于下面的三角形,已经三条边的长度,求夹角

img

余弦定理的计算公式如下:

img

求的余弦值后,通过反余弦函数求得角度,代码如下:

	/**
	 * 弧度转换成角度
	 * @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)事件处理

在touchend(或者mousemove)事件处理函数中,需要实现减速效果。

  1. 滑动的最后300ms内,计算滑动的速度
  2. 根据滑动速度,计算速度降到0需要的时间,根据速度和时间,计算出减速运动滑动的距离
  3. 将滑动的距离转换成角度,然后调用缓动函数,计算每个时间点应该转动的角度,直至转动停止

     /**
      * 计算最终应该旋转的角度和时间
      * @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曲面

img

demo展示,具体的代码实现可以看这里