打造接近native体验的h5日历选择组件

JerryXia 发表于 , 阅读 (0)

img

统一UI

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

3d特效

img

试着想象一下,将一张长方形的纸卷曲形成圆柱体的过程,无非就是将无穷多个细小的长方形,以圆柱体的中心轴为基准,旋转一定的角度。只要角度是连续的,就可以形成曲面。这段话一时难以理解。我们认为每一个选择项的dom节点一开始是平面的,2d的,通过transform的rotateX(angle),rotateY(angle),rotateZ(angle)来实现3d旋转。

先简单理解一下rotateX,rotateY,rotateZ的直观效果

See the Pen RRYZkJ by shelwinjue (@shelwinjue) on CodePen.

(注意,要搭配透视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); }