前言:
在移动端中的交互中通常都是有动画效果的,例如打开一个模态框(或对话框),用户按住并向上或向下滑动以选择其中一个选项。在滑动过程中,用户会感受到阻尼效果(即滑动的速度会逐渐减慢),当滑动停止时,最接近停止位置的选项会被选中。
原理:
我们需要记录手指按下的时间,手指滑动到松开的的时间和已经滑动的位移量,这个时候同时也要位移目标元素,再根据按下的时间到松开的时间和位移量是计算出速度,当松开手之后这个速度会在加速度的的作用下不断减少,最后再根据这个速度计算出该元素在Y轴方向的位移量,在requestAnimationFrame这个动画的回调中不断更新就行了
此外,还有一个问题需要解决,因为我们便利出来的每个item都是有高度的,如何保证每次都能刚好停在一个item上面呢,怎么理解这句话呢就是,停在的位置要是item元素高度的整数倍,这样子停止的时候就会刚好停在item上面,就不会出现停在itemA和itemB中间之类的情况,我需要在速度很小的时候,也就是快停止的时候决定什么停下,才会刚好停在item上面
分析:
现在我要实现一个带有阻尼效果的选项框,要如何实现呢?
首先要分析几个变量:
deltaTime:手指按下的时间到松开的时间差
deltaY:在Y轴方向上偏移的量,单位为px
acceleration:为负的时候会让让速度velocity的绝对值不断地降低,直到减少至0;反之会让速度越来越快。
displacement:记录手指按下到松开的瞬间滑动的距离,用于计算初始速度velocity的大小
velocity:由displacement和deltaTime计算出来的初始速度
分析完毕直接看代码,代码是最好的解释
<template>
<div class="selectBox" ref="BoxRef">
<div class="trasform" ref="tRef">
<div class="selectItem" v-for="item in 30" :key="item">{{ item }}</div>
</div>
<div class="checked"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
const Emit = defineEmits(["checked"]);
const BoxRef = ref<HTMLDivElement>();
const tRef = ref<HTMLDivElement>();
let pointDown = false; // 鼠标按下
let touching = false; // 触摸
let deltaY = 0; // 偏移量
let prevPageY = 0; // 触摸初始值
let prevTime = 0;
let deltaTime = 0;
const acceleration = 10;
let displacement = 0;
let velocity = 0;
let floorMun = 0;
onMounted(() => {
if (BoxRef.value && tRef.value) {
BoxRef.value.addEventListener("mousedown", ({ clientY }) => {
pointDown = true;
prevPageY = clientY;
prevTime = performance.now();
deltaTime = 0;
displacement = 0;
});
BoxRef.value.addEventListener("touchstart", ({ touches }) => {
touching = true;
prevPageY = touches[0].pageY;
prevTime = performance.now();
deltaTime = 0;
displacement = 0;
});
window.addEventListener("mouseup", () => {
pointDown = false;
deltaTime = (performance.now() - prevTime) / 1000;
velocity =
Math.floor(displacement / deltaTime) -
(Math.floor(displacement / deltaTime) % 10);
});
window.addEventListener("touchend", () => {
touching = false;
deltaTime = (performance.now() - prevTime) / 1000;
velocity =
Math.floor(displacement / deltaTime) -
(Math.floor(displacement / deltaTime) % 10);
});
BoxRef.value.addEventListener("mousemove", ({ clientY }) => {
if (pointDown) {
deltaY += (clientY - prevPageY) * 1;
displacement += clientY - prevPageY;
prevPageY = clientY;
}
});
BoxRef.value.addEventListener("touchmove", ({ touches }) => {
if (touching) {
deltaY += (touches[0].pageY - prevPageY) * 1;
displacement += touches[0].pageY - prevPageY;
prevPageY = touches[0].pageY;
}
});
}
function translateY() {
if (velocity != 0) {
if (velocity < 0) {
velocity += acceleration;
} else {
velocity -= acceleration;
}
// 速度快停止的时候保证刚好停在item上面
if (Math.abs(velocity) === 10) {
deltaY = deltaY - (deltaY % 40);
floorMun = Math.abs((deltaY - 80) / 40) + 1;
Emit("checked", floorMun);
}
}
deltaY += velocity * 0.01;
// 限制移动的范围
deltaY = Math.min(deltaY, 80);
deltaY = Math.max(deltaY, -1080);
tRef.value?.style.setProperty("--tx", deltaY + "px");
requestAnimationFrame(translateY);
}
translateY();
});
</script>
<style scoped>
.selectBox {
height: 100%;
width: min(100%, 500px);
margin: 0 auto;
overflow: hidden;
position: relative;
}
.selectItem {
height: 40px;
width: 100%;
text-align: center;
line-height: 40px;
font-size: 18px;
user-select: none;
}
.trasform {
--tx: 0;
transform: translateY(var(--tx));
}
.checked {
position: absolute;
height: 40px;
width: 80%;
border-bottom: 1px solid rgb(59, 59, 59);
border-top: 1px solid rgb(59, 59, 59);
top: 80px;
left: 50%;
transform: translateX(-50%);
}
</style>
结束
还可以继续优化就是,当我们向下滑动到尽头或者往上滑动到尽头的时候还继续滑动,可以让元素继续滑动出去一段距离,松手后又弹回来,这里留给小伙伴们自己实现吧,快去动手吧