希望做一个类似IOS拾取器的控件,在IOS上该控件的效果是这样的:,我也把该效果称之为为*效果。
要实现这个效果,能够用到的技术点非常简单,无非是transform的translate3d和rotate,不过要想很好的实现,还要建立一个精确的数学模型,来解决如何【摆放】的问题。特别是这个效果不是静态的,需要满足鼠标滑动的时,这个*要转起来,这就需要仔细思索了。当然,在最开始重点还是要搞清楚自变量是什么、因变量是什么、它们之间的关系是什么以及该需求的一些性质。找到了好的性质,可以减轻工作量,并且让代码可以更加简洁。
做这个效果,首先想到的,就是用div表示每个小条目,然后想象成这些div是贴在一个圆上面,滚动这个圆。先用html把结构展示一下:
1 <div class="border"> 2 <div class="item">0</div> 3 <div class="item">1</div> 4 <div class="item">2</div> 5 <div class="item">3</div> 6 <div class="item">4</div> 7 <div class="item">5</div> 8 <div class="item">6</div> 9 <div class="item">7</div> 10 <div class="item">8</div> 11 <div class="item">9</div> 12 <div class="item">10</div> 13 <div class="item">11</div> 14 <div class="item">12</div> 15 <div class="item">13</div> 16 <div class="item">14</div> 17 <div class="item">15</div> 18 <div class="item">16</div> 19 <div class="item">17</div> 20 <div class="item">18</div> 21 </div>
接着把其中的css代码也写出来:
1 <style> 2 * { 3 -webkit-user-select: none; 4 } 5 6 .item { 7 height: 30px; 8 line-height: 30px; 9 font-size: 15px; 10 font-weight: bolder; 11 color: #222; 12 width: 400px; 13 background: white; 14 text-align: center; 15 position: absolute; 16 top: 50%; 17 margin-top: -15px; 18 } 19 20 .border { 21 overflow: hidden; 22 cursor: pointer; 23 position: relative; 24 height: 240px; 25 width: 400px; 26 border: 2px solid black; 27 } 28 29 .hide { 30 display: none; 31 } 32 </style>
注意上面的border采取的是相对(relative)定位,里面的条目(item)采用的是绝对定位,而且是垂直居中,这样的话,就会有如下的效果:
由于给item设定了背景色,所以只会看到最后一个item。接下来我们要做的工作是,把这19个item先摆成像是贴在圆上一样。这需要先挖掘一下这个需求的性质,因为性质会成为我们的突破口。
想要摆出这个效果,需要先进行分析。我们知道,由于每个item的高度相同,所以每两个相邻的item之间的旋转角度之差相同,而这个角度之差可以通过公式2 * Math.asin( height / 2 / r )计算出来。另外,转轮的旋转角度和item的旋转角度相同,那么我们需要知道已经旋转的角度,用这个角度以及角度之差我们可以知道任何一个item的旋转角度,然后再根据这个角度计算出item摆放的位置。
有了上述的分析,想要摆放出一个很漂亮的拾取器控件样式就不那么难了。不过在此之前,要做一下简单的准备。
首先,需要一个设定css样式的javascript函数,因为在摆放的过程中,要频繁用到javascript来设定css的transform样式,因此极有必要将该操作提取出来写成为一个函数,不妨将其命名为css,这部分的代码如下:
1 function css( el, style ) { 2 loopObj( style, function ( name, value ) { 3 el.style.setProperty( name, value, ""); 4 } ) 5 }
代码非常简单,但是非常实用。其中setProperty方法的第三个参数可以为important或者是一个空字符串,这里设成空字符串。
接下来进行布局。由于部署在球面上的item具有对称性,所以对19个item,从第10个开始布局,然后循环10次,从中间到两边依次对item进行布局。这部分代码如下所示:
1 var r = 90; 2 var height = 30; 3 var dAngle = 2 * Math.asin( height / 2 / r ); 4 var first = items.length / 2 << 0; 5 6 function setPosition( curAngle ) { 7 d.loop( first + 1, function ( i ) { 8 doSet( items[first + i], curAngle - i * dAngle ); 9 doSet( items[first - i], curAngle + i * dAngle ); 10 } ); 11 }
其中doSet的代码如下所示:
1 function doSet( el, angle ) { 2 Math.abs( angle ) > Math.PI / 2 ? el.classList.add( "hide" ) : el.classList.remove( "hide" ); 3 d.css( el, { 4 "-webkit-transform" : "translate3d(0," + (r * Math.sin( -angle )) + "px,0) rotateX(" + 180 * angle / Math.PI + "deg)" 5 } ); 6 }
添加初始化的javascript代码:setPosition( 0 ),然后在浏览器中打开后就能看到这样的效果,可以看出布局成功了,正是我们想要的效果。
接下来的工作中,要让它转动起来,由于上面考虑的非常充分,已经不必再引入新的变量,只要记录鼠标滑动的距离,得出相应的curAngle,然后传给setPosition即可。相关代码如下所示:
1 var end = d.Events(); 2 3 d.dragY( border, border, { 4 moving : function () { 5 var moving = d.Events(); 6 moving.regist( function ( e ) { 7 setPosition( -e.dy / 50 ); 8 } ); 9 return moving; 10 }, 11 end : end, 12 setMove : function ( e ) { 13 console.log( e.dy ); 14 } 15 } );
在浏览器中打开,然后就可以看到已经可以转动了。
但是这个目前仍然美中不足,就是对旋转的角度没有记录,导致第二次转动时,还是从新开始,所以还需要改动一下:
1 var alreadyS = 0; 2 3 d.dragY( border, border, { 4 moving : function () { 5 var moving = d.Events(); 6 moving.regist( function ( e ) { 7 console.log( alreadyS - e.dy ); 8 setPosition( (alreadyS - e.dy) / 50 ); 9 } ); 10 return moving; 11 }, 12 end : function () { 13 var end = d.Events(); 14 end.regist( function ( e ) { 15 alreadyS = alreadyS - e.dy; 16 } ); 17 return end; 18 }, 19 setMove : function ( e ) { 20 } 21 } );
这里添加了一个全局变量alreadyS,记录滑动的距离,这样就不用每次滑动都要重新开始了。更新alreadyS的时机在每次滚动结束结束的时候,所以新添加了end事件。