You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

纯JavaScript自适应滑块移动端拖动异常问题求助

移动端滑块拖动异常问题修复

问题描述

基于纯JS实现的滑块组件,PC端拖动正常,但在手机端或自适应布局下滑块移动异常。已尝试多种移动端点击初始化方式,均未解决问题。


原实现代码

JavaScript

const containerMass = 5;
const mouseMass = 10;

let imageHasLoaded = false;

let mouseX = 0;
let prevMouseX = 0;
let mouseXOnDown = null;
let isMouseDown = false;

let containerPosition = 0;
let mouseVelocity = 0;
let containerVelocity = 0;

let imagesElement = null;

const createBeltScroller = (container, images) => {
  imageHasLoaded = true;
  const beltDOMItems = images.map((image, index) => {
    const element = document.createElement("div");
    element.classList.add("item");
    const elementDiv = document.createElement("div");
    elementDiv.style.height = "200px";
    elementDiv.style.width = "200px";
    elementDiv.style.backgroundColor = 'green'
    elementDiv.innerHTML = image
    element.appendChild(elementDiv);
    return element;
  });
  imagesElement = beltDOMItems.map((element) => element);
  beltDOMItems.forEach((beltDOMItem) => {
    container.appendChild(beltDOMItem);
  });
};
const container = document.querySelector(".container");

createBeltScroller(container, ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);

// Mouse event handlers
const onMouseUpdate = (event) => {
  mouseX = event.pageX;
}; // Mouse movement
const onMouseDown = () => {
  isMouseDown = true;
}; // Mouse button press
const onMouseUp = () => {
  isMouseDown = false;
}; // Mouse button release

// Add pointer event listeners
document.addEventListener("pointermove", onMouseUpdate, false);
document.getElementById("container").addEventListener("pointerdown", onMouseDown);
document.addEventListener("pointerup", onMouseUp);

const calculateMouseMomentum = () => {
  if (isMouseDown) {
    if (mouseXOnDown == null) {
      mouseXOnDown = mouseX;
      containerVelocity = 0;
    }
    const distance = mouseX - mouseXOnDown;
    mouseVelocity = mouseX - prevMouseX;
  } else {
    if (mouseXOnDown != null) {
      containerVelocity =
        ((2 * mouseMass) / (mouseMass + containerMass)) * mouseVelocity +
        ((containerMass - mouseMass) / (mouseMass + containerMass)) *
        containerVelocity;

      const maxVelocity = 60;

      if (containerVelocity > maxVelocity) {
        containerVelocity = maxVelocity;
      } else if (containerVelocity < -maxVelocity) {
        containerVelocity = -maxVelocity;
      }

      mouseXOnDown = null;
      mouseVelocity = 0;
    }
  }

  prevMouseX = mouseX;
};

const updateContainer = () => {
  const boundRight = -container.offsetWidth + window.innerWidth - 85;

  const isOutBound =
    containerPosition > 0 || containerPosition < boundRight;

  if (!isMouseDown) {
    const mu = 0.04;
    const g = 10;
    const flictionForce = containerMass * g * mu;
    const flictionA = flictionForce / containerMass;

    if (containerVelocity > 0) {
      containerVelocity -= flictionA;
      if (containerVelocity < 0) {
        containerVelocity = 0;
      }
    } else if (containerVelocity < 0) {
      containerVelocity += flictionA;
      if (containerVelocity > 0) {
        containerVelocity = 0;
      }
    }

    if (isOutBound) {
      const k = 0.01;
      const restLength = containerPosition > 0 ? 0 : boundRight;
      const currentLength = containerPosition;
      const dragForce = 1 * k * (restLength - currentLength);

      const dragForceA = dragForce / containerMass;
      containerVelocity += dragForce;

      const nextPosition = containerPosition + containerVelocity;

      if (containerPosition < boundRight && nextPosition > boundRight) {
        containerVelocity = 0;
        containerPosition = boundRight;
      } else if (containerPosition > 0 && nextPosition < 0) {
        containerVelocity = 0;
        containerPosition = 0;
      }
    }
  }

  containerPosition = containerPosition + containerVelocity + (isOutBound ? mouseVelocity / 2 : mouseVelocity);
  container.style.transform = `translate(${containerPosition}px)`;
};

const loop = () => {
  calculateMouseMomentum();
  updateContainer();
  window.requestAnimationFrame(() => {
    loop();
  });
};

loop();

CSS

.container {
  height: fit-content;
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  touch-action: pan-y;
  user-select: none;
  cursor: grab;
  margin: 2% 0;
  font-size: 150px;
}

.item {
  width: fit-content;
  height: fit-content;
  padding-left: 4.43vw;
  opacity: 1;
  transition: opacity 2s;
}

HTML

<div id="container" class="container"></div>

问题原因及修复方案

核心问题

  1. 移动端坐标获取偏差:使用event.pageX受页面滚动影响,导致触摸位置计算错误
  2. 未处理多点触摸:pointer事件未锁定活跃触摸点,多点操作时触发异常
  3. 边界值未实时更新:窗口尺寸变化时,滑块边界未重新计算,自适应布局下边界失效
  4. 拖动逻辑误差:原速度叠加逻辑在移动端触摸采样率下累积位移偏差

修复后的代码

JavaScript

const containerMass = 5;
const mouseMass = 10;

let imageHasLoaded = false;

let mouseX = 0;
let prevMouseX = 0;
let mouseXOnDown = null;
let isMouseDown = false;
let activePointerId = null; // 跟踪当前活跃触摸点

let containerPosition = 0;
let mouseVelocity = 0;
let containerVelocity = 0;

let imagesElement = null;

const createBeltScroller = (container, images) => {
  imageHasLoaded = true;
  const beltDOMItems = images.map((image, index) => {
    const element = document.createElement("div");
    element.classList.add("item");
    const elementDiv = document.createElement("div");
    elementDiv.style.height = "200px";
    elementDiv.style.width = "200px";
    elementDiv.style.backgroundColor = 'green'
    elementDiv.innerHTML = image
    element.appendChild(elementDiv);
    return element;
  });
  imagesElement = beltDOMItems.map((element) => element);
  beltDOMItems.forEach((beltDOMItem) => {
    container.appendChild(beltDOMItem);
  });
};
const container = document.querySelector(".container");

createBeltScroller(container, ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);

// 实时计算滑块边界
const calculateBoundRight = () => {
  return -container.offsetWidth + window.innerWidth - 85;
};

// 指针事件处理
const onPointerDown = (event) => {
  isMouseDown = true;
  activePointerId = event.pointerId;
  mouseX = event.clientX; // 使用clientX避免滚动影响
  mouseXOnDown = mouseX;
  prevMouseX = mouseX;
  containerVelocity = 0;
};

const onPointerMove = (event) => {
  if (!isMouseDown || event.pointerId !== activePointerId) return;
  mouseX = event.clientX;
};

const onPointerUp = (event) => {
  if (event.pointerId !== activePointerId) return;
  isMouseDown = false;
  activePointerId = null;
};

// 绑定事件
container.addEventListener("pointerdown", onPointerDown);
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
document.addEventListener("pointercancel", onPointerUp); // 处理触摸中断(如来电、弹窗)

// 窗口 resize 时校正滑块位置
window.addEventListener("resize", () => {
  const boundRight = calculateBoundRight();
  containerPosition = Math.max(boundRight, Math.min(0, containerPosition));
  container.style.transform = `translate(${containerPosition}px)`;
});

const calculateMouseMomentum = () => {
  if (isMouseDown) {
    mouseVelocity = mouseX - prevMouseX;
  } else {
    if (mouseXOnDown != null) {
      containerVelocity =
        ((2 * mouseMass) / (mouseMass + containerMass)) * mouseVelocity +
        ((containerMass - mouseMass) / (mouseMass + containerMass)) * containerVelocity;

      const maxVelocity = 60;
      containerVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, containerVelocity));

      mouseXOnDown = null;
      mouseVelocity = 0;
    }
  }

  prevMouseX = mouseX;
};

const updateContainer = () => {
  const boundRight = calculateBoundRight();
  const isOutBound = containerPosition > 0 || containerPosition < boundRight;

  if (!isMouseDown) {
    const mu = 0.04;
    const g = 10;
    const frictionForce = containerMass * g * mu;
    const frictionA = frictionForce / containerMass;

    if (containerVelocity > 0) {
      containerVelocity -= frictionA;
      containerVelocity = Math.max(0, containerVelocity);
    } else if (containerVelocity < 0) {
      containerVelocity += frictionA;
      containerVelocity = Math.min(0, containerVelocity);
    }

    if (isOutBound) {
      const k = 0.01;
      const restLength = containerPosition > 0 ? 0 : boundRight;
      const dragForce = k * (restLength - containerPosition);
      containerVelocity += dragForce;

      const nextPosition = containerPosition + containerVelocity;
      // 边界校正
      if (containerPosition < boundRight && nextPosition > boundRight) {
        containerVelocity = 0;
        containerPosition = boundRight;
      } else if (containerPosition > 0 && nextPosition < 0) {
        containerVelocity = 0;
        containerPosition = 0;
      }
    }
  }

  // 更新滑块位置
  if (isMouseDown) {
    const deltaX = mouseX - mouseXOnDown;
    containerPosition += deltaX;
    mouseXOnDown = mouseX; // 更新起点避免累积误差
  } else {
    containerPosition += containerVelocity;
  }

  // 强制限制在有效边界内
  containerPosition = Math.max(boundRight, Math.min(0, containerPosition));
  container.style.transform = `translate(${containerPosition}px)`;
};

const loop = () => {
  calculateMouseMomentum();
  updateContainer();
  window.requestAnimationFrame(loop);
};

loop();

关键修复点

  • 替换pageX为clientX,消除页面滚动对触摸坐标的影响
  • 添加activePointerId锁定活跃触摸点,避免多点触摸干扰
  • 每次更新滑块时实时计算边界值,适配窗口尺寸变化
  • 拖动时直接计算触摸位移更新位置,消除速度叠加带来的偏差
  • 绑定pointercancel事件处理触摸中断场景
  • 窗口resize时自动校正滑块位置

内容的提问来源于stack exchange,提问作者Chew

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 23:33:13