原文: https://www.luoyangfu.com/art...

前言

一个移动端的touch 事件或者 mouse 事件,具体看看怎么玩。

先看看效果:

这里年月日都是使用创建好的Picker组件来实现的,在之前感谢博客园 @糊糊糊糊糊了, 原文地址.

原文中讲了实现Picker核心思路,我也是受益颇多,然后根据思路以及Github源码,终于写了自己想要的Picker,于是就有了记录,再次感谢.

单独Picker

HTML 结构

开发这个Picker, 观察Picker节点结构,Picker 是由定高隐藏元素的块, 一个定高不隐藏元素的的块,以及一个选择列表组成这三个部分。我们得知了这个Picker组成后很容易就可以写出来下面HTML的结构:

<div class="picker">    <div class="picker-wrapper" id="wrapper">        <div>2016</div>        <div>2017</div>        <div>2018</div>        <div>2019</div>        <div>2020</div>    </div>    <div class="center-highlight"></div></div>

这个就是最简单的 picker 结构了。

CSS 样式

咱们给这块结构添加样式,显示如下, 在这里我们默认元素的高度在css 中写死为 50px.

我们这里就写了关于 Picker 基础样式。

.picker {  overflow: hidden;  position: relative;  z-index: 1;}.picker-wrapper {  overflow: visible;  height: calc(50px * 3);}.picker-item {  height: 50px;  line-height: 50px;  text-align: center;  color: #999;}.picker-item.active {  color: #000;}.picker-center-highlight {  height: 50px;  position: absolute;  width: 100%;  top: 50%;  margin-top: -25px;  z-index: 2;}.picker-center-highlight::before, .picker-center-highlight::after {  content: '';  display: block;  height: 2px;  background-color: #000;  width: 100%;  transform: scaleY(0.5);  position: absolute;}.picker-center-highlight::before {  top: 0;}.picker-center-highlight::after {  bottom: 0;}

上面仅仅仅仅包含图片样式组成,后续会逐渐添加各种样式。

初始化Picker组件

已经有了基本的 Picker 组件,现在我们开始进行一个初始化。让第一个元素显示在正确的位置。
这里有两个基本问题需要先考虑一下:

  • 元素的移动范围
  • 元素的下标和位置交换

元素的移动范围

我们元素移动范围用下标来说的话,应该是 1 到 length, 这里length 就是传入Picker数据长度。

因为我们第一个元素需要向下移动一列,所以第一列应该是空的, 这个时候位置也是我们位移最大位置。当不断将Picker 向上位移什么时候为最小的位置呢?应该是有这样计算 (length - Math.ceil(count / 2)) * 50. 这里我们说的是显示3列情况, length 为数据长度, 50为单个Picker高度,上文已有。

下面直接看元素结算范围:

const getMoveRange = function() {  const max = Math.floor(visibleCount / 2) * itemHeight;  const min = (itemLength - Math.ceil(visibleCount / 2)) * -itemHeight;  return [min, max]}

这里就解决了第一个元素的范围问题。

这里使用两个数学函数:
Math.floor 取不大于该数的最大整数
Math.ceil 取不小于该数的最小整数

元素的下标和位置交换

这里需要两个函数分别来计算使用下标获取位置,使用位置来获取下标的。

通过下标获取元素位置时候有一点需要注意的是,我们元素位置是需要根据当前显示的个数进行偏移的,也就说,在计算之前,需要减去偏移量。偏移量刚好等于 Math.floor(count / 2)

const getTranslByIndex = function(index) {  const offset = Math.floor(visibleCount / 2)    if(index >= 0) {    return (index - offset) * -itemHeight  }}

通过位移获取元素位置就简单很多了。

const getIndexByTransl = function(transl) {  transl = Math.round(transl / itemHeight) * itemHeight;  const index = - (transl - Math.floor(visibleCount / 2) * itemHeight) / itemHeight;    return index;}

计算用了三行代码, 第一行主要是转化当前位置靠近哪一个元素,得到具体translate,第二行 计算具体的 index, 这里计算语句前面使用 -减号, 主要因为 translate 减去显示个数的1/2后,总是负值,这里需要将负值转正,也可以使用 Math.abs 来替代。

上面我们就解决了两个问题。

初始化组件

首先需要组件初始化为上图的样子,这个时候,我们开始监听事件,这了我们主要监听 touchstart, touchmove, touchend 事件。

const translateEl = function(transl) {  el.style.transform = `translateY(${transl}px)`;};const setSelectedEl = function(index) {  Array.prototype.forEach.call(pickerItems, (item, idx) => {    item.classList.remove("active");  });  pickerItems[index].classList.add("active");};function initEvent() {  el.addEventListener("touchstart", function(e) {    console.log("touchstart", e);  });  el.addEventListener("touchmove", function(e) {    console.log("touchmove", e);  });  el.addEventListener("touchend", function(e) {    console.log("touchenv", e);  });}window.onload = function onload() {  translateY = getTranslByIndex(0);  setSelectedEl(0);  translateEl();  initEvent();};

上面的 pickerItems 就是选中的picker 元素集合.

这里就对元素进行了touchstart,touchmove, touchend 监听。下面我进一步对事件做处理,让picker动起来。
为了让picker 在移动过程中有过度效果,增加如下css

.picker-wrapper {    overflow: visible;    height: calc(50px * 3);    transition: all 0.3s ease-in-out; /* 这里就对 元素做了过渡动画处理*/}

开始对事件进行处理

el.addEventListener('touchstart', function(e) {    startAt = Date.now();    startTop = e.touches[0].pageY;    // 开始滚动的位置    startTranslateY = translateY;  });  el.addEventListener('touchmove', function(e) {    const deltaY = e.touches[0].pageY - startTop;    translateY = startTranslateY + deltaY;    velocityTranslate = translateY - prevTranslateY || translateY;    prevTranslateY = translateY;    translateEl();  });  el.addEventListener('touchend', function(e) {    let momentumTranslate = 0;    // 小于 300 就开始弹性滚动    if (Date.now() - startAt < 300) {      momentumTranslate = translateY + velocityTranslate * momentumRatio;    }    let translate = Math.round(translateY / itemHeight) * itemHeight;    if (momentumTranslate) {      translate = Math.round(momentumTranslate / itemHeight) * itemHeight;    }    const range = getMoveRange();    translateY = Math.max(Math.min(translate, range[1]), range[0]);    translateEl();    const index = getIndexByTransl(translateY);    setSelectedEl(index);  });  // 每个item 点击生效  Array.prototype.forEach.call(pickerItems, function(item, index) {    item.addEventListener('click', function(e) {      setSelectedEl(index);      translateY = getTranslByIndex(index);      translateEl();    });  });

这里就对触摸事件做了处理,也对单个元素的点击做了监听。

const el = document.querySelector('#wrapper');const itemHeight = 50;const visibleCount = 3;const itemLength = 5;const pickerItems = document.querySelectorAll('.picker-item');let startAt = Date.now();let startTop = 0;let translateY = 0;let startTranslateY = 0;let prevTranslateY = 0;// 动力参数let momentumRatio = 7;// 速度速度位移let velocityTranslate = 0;

这里再开头申明了一些内容,主要说明一下 momentumRatiovelocityTranslate 这两个,前者是动力系数用于短时间修改位移后惯性移动,后者是速度位移用于在用户移动过程中移动的量。

现在就完成了一个基本的Picker。看图:

最后

目前只是一个最基本的例子。如果说需要选择两侧有一个缩放呢。