前言
picker是一种最常见的前端组件,无论是日期选择、地区选择、计时器定时间都会用到,因为方便实用、大方美观而受到用户的喜爱。很多免费的组件库比如MintUI都提供了picker组件,但是别人编写的通用组件不一定适用于自己的项目,而且功能过于复杂的组件也违背了“keep it simple”的软件开发原则。所以今天我就来探讨一下picker组件的原理,尝试用最少的代码实现一个简单的picker,并且把过程分享给大家。
最终效果展示
如果无法正常使用,点击“查看详情”进入全屏试一试
picker组件的关键技术
picker组件中最关键的技术有3点:
- CSS的overflow属性。
- CSS的transform属性。
- JS的pointer event。
下面分别来说说这三个技术的功能:
1、CSS的transform属性配合position属性使用,可以随意旋转、缩放、拉伸、移动元素。比如:
/*
absolute定位使得div标签脱离文档流,以最近的非static定位的上级元素为准,
向下移动100px,向右移动200px
*/
div{
position:absolute
transform:translate(200xp,100xp)
}
2、当一个块级元素的content区域(content的定义请查询“CSS盒子模型”)超出其盒子模型时,overflow:hidden可以隐藏超出部分;overflow:scroll可以为这个块级元素增添滚动条(与浏览器窗口的滚动条一个道理)
3、当鼠标与屏幕互动时,比如触发click事件,会把一个MouseEvent对象传递给事件处理方法;而PointerEvent则更进一步,可以处理手指与屏幕的互动。所以如果用PointerEvent取代了MouseEvent,那么这个picker就可以应用于移动设备(手机)。用代码来解释一下:
/*click事件只能由鼠标触发,传递给事件处理方法一个MouseEvent对象*/
element.onclick=function(event){
console.log(event.clientY) //打印鼠标在这个元素中的y轴坐标
}
/*pointerdown事件由鼠标/手指点击屏幕触发,传递给事件处理方法一个PointerEvent对象*/
element.onpointerdown=function(event){
console.log(event.clientY) //打印鼠标/手指在这个元素中的y轴坐标
}
使用pointer event唯一需要注意的是,要为元素设置CSS属性touch-action:none,这样才能使自己写的pointer event相关事件生效。
picker组件的基本原理
一个最简单的picker组件由2部分组成:
- picker column:picker选项列,容纳全部picker选项。
- picker selector:picker选择框,只有当picker选项被滚动到选择框中,才算被选中。
以下是一个包含数字0-9作为选项的picker:
<!--picker-->
<div id="picker">
<!--picker中可以滚动选择的内容-->
<div id="picker_column">
<div class="picker_item">0</div>
<div class="picker_item">1</div>
<div class="picker_item" selected>2</div>
<div class="picker_item">3</div>
<div class="picker_item">4</div>
<div class="picker_item">5</div>
<div class="picker_item">6</div>
<div class="picker_item">7</div>
<div class="picker_item">8</div>
<div class="picker_item">9</div>
</div>
<!--picker_selector位于picker中间,用来选中picker_item-->
<div id="picker_selector"></div>
</div>
重点来了:
1、picker_column作为选项列,它的height远超picker,所以要为picker设置CSS属性overflow:hidden,只显示部分picker_column
2、picker_selector作为选择框,利用position:absolute定位移动到picker的中央。
3、当用户用手指/鼠标上下拖动picker_column时,picker_column的transform属性被改变,使得picker_column相对于picker上下滑动,实现了滚动
4、当用户手指/鼠标离开屏幕时,距离picker_selector最近的那个picker_item被选中
完成
以上就是对picker原理的简单描述,具体的代码:
<!--
用原生js实现mintui中的picker组件的效果。
思路:
1、picker分为picker_column(内含选项picker_item)和picker_selector(位于picker中心的选中框)。
设定picker_item为数字0-9,picker只展示5个选项,picker_selector固定位于从上往下数第三个选项处。
2、picker_column的高度为10个picker_item的总高度,超过picker的高度(5个picker_item的总高度)。
超过的部分被隐藏。
3、鼠标/手指上下拖拽picker_column,picker_column相对于picker和picker_selector的位置发生改变,由此实现“滚动”。
4、鼠标/手指离开picker_column,滚动结束。
5、如果picker_selector没有完全对准picker_item,会自动调整位置。
-->
<html>
<head>
<meta charset="UTF-8">
<title>picker:滚动选择</title>
<style>
#picker{
width:50%;
height:25rem;
border:1px solid black;
position:relative;
overflow: hidden;
touch-action: none;
}
#picker_column{
width:100%;
height:100%;
}
.picker_item{
height:5rem;
font-size:4rem;
color:rgb(194, 192, 192);
text-align: center;
user-select: none;
}
.picker_item[selected]{
color:black;
}
#picker_selector{
width:100%;
height:5rem;
position:absolute;
left:-1px;
top:40%;
border:1px solid black;
z-index:-1;
}
</style>
</head>
<body>
<!--picker-->
<div id="picker">
<!--picker中可以滚动选择的内容-->
<div id="picker_column">
<div class="picker_item">0</div>
<div class="picker_item">1</div>
<div class="picker_item" selected>2</div>
<div class="picker_item">3</div>
<div class="picker_item">4</div>
<div class="picker_item">5</div>
<div class="picker_item">6</div>
<div class="picker_item">7</div>
<div class="picker_item">8</div>
<div class="picker_item">9</div>
</div>
<!--picker_selector位于picker中间,用来选中picker_item-->
<div id="picker_selector"></div>
</div>
</body>
<script>
var picker_column=document.getElementById("picker_column")
var picker_selector=document.getElementById("picker_selector")
var picker_items=document.getElementsByClassName("picker_item")
var pointerDown=false //鼠标/手指是否按下
var itemHeight=80 //每个picker_item高5rem=5*16=80px
var highPoint=2*itemHeight //picker_column从初始状态到达最高点的y轴移动量(此时0被选中)
var lowPoint=-7*itemHeight //picker_column从初始状态到达最低点的y轴移动量(此时9被选中)
var lastPoint=0 //鼠标/手指按下且移动,刚才的y轴位置
var currentPoint=0 //鼠标/手指保持按下且移动,此时的y轴位置
var distance=0 //鼠标/手指的移动距离=currentPoint-lastPoint
var friction=0.8 //摩擦系数。鼠标拖动距离*friction=picker滚动距离
var selected=picker_items[2] //被选中的picker_item(默认是数字2)
//picker_column事件:鼠标/手指按下
picker_column.addEventListener("pointerdown",function(event){
pointerDown=true
lastPoint=event.clientY
picker_column.style.transition="initial"
})
//picker_column事件:鼠标/手指移动
picker_column.addEventListener("pointermove",function(event){
if(pointerDown){
//计算picker_cloumn在y轴上移动的距离distance
currentPoint=event.clientY
distance+=(currentPoint-lastPoint)*friction
//picker_column上下移动的距离被限制在highPoint与lowPoint之间
if(distance<=highPoint&&distance>=lowPoint){
picker_column.style.transform=`translate(0px,${distance}px)`
lastPoint=currentPoint
}else if(distance>highPoint){
distance=highPoint
}else if(distance<lowPoint){
distance=lowPoint
}
}
})
//picker_column事件:鼠标/手指离开
picker_column.addEventListener("pointerup",function(event){
pointerDown=false
//picker_selector自动对齐并选中picker_item
//1、判断移动距离distance与itemHeight的关系
var remainder=distance%itemHeight
//2、distance为itemHeight的整数倍,这样就可以对齐picker_selector
if(distance<0&&Math.abs(remainder)<0.5*itemHeight){
distance-=remainder
}else if(distance<0&&Math.abs(remainder)>=0.5*itemHeight){
distance=distance-remainder-itemHeight
}else if(distance>=0&&Math.abs(remainder)<0.5*itemHeight){
distance-=remainder
}else if(distance>=0&&Math.abs(remainder)>=0.5*itemHeight){
distance=distance-remainder+itemHeight
}
picker_column.style.transition="transform 1s"
picker_column.style.transform=`translate(0px,${distance}px)`
//3、选中对应的picker_item(标签多一个selected属性)
var index=2-distance/itemHeight
selected.removeAttribute("selected")
selected=picker_items[index]
selected.setAttribute("selected","")
})
</script>
</html>
代码中如果有不清楚的,欢迎提问。