原文: 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;
这里再开头申明了一些内容,主要说明一下 momentumRatio
和 velocityTranslate
这两个,前者是动力系数用于短时间修改位移后惯性移动,后者是速度位移用于在用户移动过程中移动的量。
现在就完成了一个基本的 Picker。看图:
最后
目前只是一个最基本的例子。如果说需要选择两侧有一个缩放呢。