前端如何实现带有阻尼效果的模态框(或对话框)

前言:

在移动端中的交互中通常都是有动画效果的,例如打开一个模态框(或对话框),用户按住并向上或向下滑动以选择其中一个选项。在滑动过程中,用户会感受到阻尼效果(即滑动的速度会逐渐减慢),当滑动停止时,最接近停止位置的选项会被选中。

原理:

我们需要记录手指按下的时间,手指滑动到松开的的时间和已经滑动的位移量,这个时候同时也要位移目标元素,再根据按下的时间到松开的时间和位移量是计算出速度,当松开手之后这个速度会在加速度的的作用下不断减少,最后再根据这个速度计算出该元素在Y轴方向的位移量,在requestAnimationFrame这个动画的回调中不断更新就行了

此外,还有一个问题需要解决,因为我们便利出来的每个item都是有高度的,如何保证每次都能刚好停在一个item上面呢,怎么理解这句话呢就是,停在的位置要是item元素高度的整数倍,这样子停止的时候就会刚好停在item上面,就不会出现停在itemA和itemB中间之类的情况,我需要在速度很小的时候,也就是快停止的时候决定什么停下,才会刚好停在item上面

分析:

现在我要实现一个带有阻尼效果的选项框,要如何实现呢?

首先要分析几个变量:

deltaTime:手指按下的时间到松开的时间差

deltaY:在Y轴方向上偏移的量,单位为px

acceleration:为负的时候会让让速度velocity的绝对值不断地降低,直到减少至0;反之会让速度越来越快。

displacement:记录手指按下到松开的瞬间滑动的距离,用于计算初始速度velocity的大小

velocity:displacementdeltaTime计算出来的初始速度

分析完毕直接看代码,代码是最好的解释

<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>

结束

还可以继续优化就是,当我们向下滑动到尽头或者往上滑动到尽头的时候还继续滑动,可以让元素继续滑动出去一段距离,松手后又弹回来,这里留给小伙伴们自己实现吧,快去动手吧

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值