前端造轮子:原生JS用60行代码实现picker滚动选择效果

lxf2023-03-18 19:02:01

前言

picker是一种最常见的前端组件,无论是日期选择、地区选择、计时器定时间都会用到,因为方便实用、大方美观而受到用户的喜爱。很多免费的组件库比如MintUI都提供了picker组件,但是别人编写的通用组件不一定适用于自己的项目,而且功能过于复杂的组件也违背了“keep it simple”的软件开发原则。所以今天我就来探讨一下picker组件的原理,尝试用最少的代码实现一个简单的picker,并且把过程分享给大家。

最终效果展示

如果无法正常使用,点击“查看详情”进入全屏试一试

picker组件的关键技术

picker组件中最关键的技术有3点:

  1. CSS的overflow属性。
  2. CSS的transform属性。
  3. 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部分组成:

  1. picker column:picker选项列,容纳全部picker选项。
  2. 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>

代码中如果有不清楚的,欢迎提问。