先看效果图
1.x版本在这篇文章有介绍
2.0版本 picker 选择器代码如下:
大致思路:
- 在
touchmove计算出手指滑动时的瞬时速度, 因为这块move更新频率很快,所以,位移足够小时间足够短,可以大致理解这里的速度就是瞬时速度velocity = deltaY / (now - lastTime) - picker选择器,滚动停止时,必须与滚动项对齐,不能有偏差。 为了达到这个目的。 我需要提前算出总的位移量和时间。
- 根据这个速度
velocity,计算出总的位移和总时间,具体实现看calcTotalDisAndTime。calcTotalDisAndTime在每次循环时,速度都会在原来的基础上衰减deceleration = 0.03, 即每次循环都在上一次速度上衰减3% - 我们知道requestAnimationFrame每次执行时间大约是16.67ms。 有这样一个公式:
1000ms/fps = 一帧多用时间; 大多数浏览器的帧数fps= 60帧; 即一秒60帧,一秒中页面60个帧数刷新页面。 - 这样我就知道每次循环(每帧)位移量:
velocity * 16.67ms = 每帧的位移量; 这样就大概统计出了所有时间和位移。 stepSize步长就是滚动项的高度。- 写一个惯性滚动函数:
startInertiaScroll; 这里需要将最终滚动停止时,位移量正好对齐滚动项,因此需要4舍5入,取步长stepSize整数倍:具体代码moveY = Math.round((oldCurrentY + moveY) / stepSize) * stepSize; - 最后编写一个过渡函数
initTranstion并利用easing缓动函数, 来过渡每一帧的位移。具体代码initTranstion
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>最简单的 picker 选择器</title>
<style>
html, body, ul, li {
margin: 0;
padding: 0;
}
ul {
list-style: none; /* 移除默认的列表样式 */
padding: 10px;
}
ul li {
height: 40px;
margin-bottom: 10px;
background-color: #ffffff;
border-radius: 6px;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
text-align: center;
font-size: 18px;
line-height: 40px;
}
.scrollable-content{
position: relative;
user-select: none;
height: 600px;
box-sizing: border-box;
overflow: hidden;
}
.scrollable-content:before {
content: '';
display: block;
width: 100%;
height: 250px;
background-image: linear-gradient(to top,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.6) 33%,
rgba(255, 255, 255, 0.9) 66%,
rgba(255, 255, 255, 1) 100%);
border-bottom: 0.5px solid #b7b7b7;
position: absolute;
top:0;
z-index: 99;
}
.scrollable-content::after{
content: '';
display: block;
width: 100%;
height: 290px;
background-image: linear-gradient(to bottom,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.6) 33%,
rgba(255, 255, 255, 0.9) 66%,
rgba(255, 255, 255, 1) 100%);
border-top: 0.5px solid #b7b7b7;
position: absolute;
bottom: 0;
z-index: 99;
}
</style>
</head>
<body>
<div class="scrollable-content">
<ul class="viewport"></ul>
</div>
<script>
let startY = 0; // 滑动起始位置
let disY = 0; // 当前滑动位置
let velocity = 0; // 滑动速度
let isMoving = false; // 是否在滑动
let lastTime = 0; // 上一次移动的时间
let deceleration = 0.03; // 减速度,每帧速度减少为上次速度的10%
let currentY = disY; // 当前滚动位置
let direction = 0; // 滑动方向,1表示向下,-1表示向上
let transition = null; // 动画过渡
const stepSize = 50; // 步长大小
const content = document.querySelector('.scrollable-content'); // 可滚动内容
const viewport = document.querySelector('.viewport'); // 可视区域
const contentHeight = content.offsetHeight; // 获取内容的总高度
const viewportHeight = viewport.offsetHeight;; // 获取可视区域的高度
const maxScrollY = viewportHeight - contentHeight; // 计算最大滚动距离
let lis = '';
for(let i = 0; i < 100; i++) {
lis += `<li>${i+1}</li>`
}
viewport.innerHTML = lis;
document.addEventListener('touchstart', (event) => {
event.preventDefault(); // 阻止默认的触摸滚动
transition && transition.stop(); // 停止之前的动画
startY = event.touches[0].clientY;
disY = 0;
velocity = 0;
isMoving = true;
lastTime = Date.now();
}, { passive: false });
document.addEventListener('touchmove', (event) => {
event.preventDefault(); // 阻止默认的触摸滚动
if (!isMoving) return;
const now = Date.now();
const deltaY = event.touches[0].clientY - startY;
disY += deltaY;
// 1 向下滚动, -1 向上滚动
direction = Math.abs(disY)/disY;
velocity = deltaY / (now - lastTime); // 计算当前速度
// 限制最大速度
if(Math.abs(velocity) > 2.5) {
velocity = 2.5 * direction;
}
// 实时更新页面位置
updatePagePosition(currentY + disY);
startY = event.touches[0].clientY;
lastTime = now;
}, { passive: false });
document.addEventListener('touchend', (event) => {
isMoving = false;
currentY = currentY + disY;
startInertiaScroll();
});
// 根据初速度,计算总位移和所有时间
function calcTotalDisAndTime(velocity) {
let duration = 0;
let moveY = 0;
const frameTime = 16.67;
const lastTime = Date.now();
console.time('calcTotalDisAndTime');
while(Math.abs(velocity) >= 0.01) {
velocity *= (1 - deceleration);
moveY += velocity * frameTime;
duration += frameTime;
}
console.timeEnd('calcTotalDisAndTime');
return { duration, moveY };
}
function startInertiaScroll() {
let { duration, moveY } = calcTotalDisAndTime(velocity);
const oldCurrentY = currentY;
moveY = Math.round((oldCurrentY + moveY) / stepSize) * stepSize;
transition = initTranstion(Math.max(duration, 300), easeOutQuad);
transition.framer((percent) => {
// 实时y轴位置
currentY = oldCurrentY + (moveY - oldCurrentY) * percent;
updatePagePosition(currentY)
})
// 过渡结束的回调
// .end(() => {})
.start();
}
function updatePagePosition(y, duration = 0) {
const element = document.querySelector('.viewport');
element.style.transform = `translate3d(0, ${y}px, 0)`;
}
function initTranstion(duration = 300, easeFunction) {
let startTime = null;
let timer = null;
let framer = null;
let transitionEnd = null;
// 开启动画
function startAnimated() {
function step() {
let time = Date.now();
if (!startTime) startTime = time;
let progress = (time - startTime) / duration;
if (progress > 1) progress = 1;
const percent = easeFunction(progress);
// 每帧回调
framer && framer(percent);
if (progress < 1) {
timer = requestAnimationFrame(step);
}else {
// 结束回调
transitionEnd && transitionEnd();
}
}
requestAnimationFrame(step);
}
// initiator
return {
framer(callback) {
typeof callback == 'function' && (framer = callback);
return this;
},
// 过渡j结束回调
end(callback) {
typeof callback == 'function' && (transitionEnd = callback);
return this;
},
start() {
startAnimated();
return this;
},
stop() {
cancelAnimationFrame(timer);
// 停止也可以认为是过渡结束
transitionEnd && transitionEnd();
return this;
}
};
}
// easing 动画函数,参数x的取值范围[0~1]
function easeOutQuad(x) {
return 1 - (1 - x) * (1 - x);
}
</script>
</body>
</html>
演示效果