导读:本期聚焦于布兰登创作的《微信小程序自定义modal拖拽惯性如何计算?根据拖拽速度计算惯性滑动距离》,敬请观看详情。自定义 modal 的拖拽体验好不好,往往取决于松手瞬间的惯性处理是否自然。在微信小程序中实现可拖拽弹窗时,不能只处理 touchmove 的跟手位移,还要基于最后一段速度估算松手后的滑行距离。做法是在 touchstart、touchmove、touchend 事件里记录最近几帧的位移和时间差,通过滑动窗口或加权平均计算末速度,过滤掉手指抖动带来的瞬时跳变。得到末速度后,可以按固定减速度 a 计算惯性距离,公式为 s 等于 v 平方除以 2a;也可以使用 ease-out 曲线和定时器逐帧更新位置,让减速过程更柔和。实际落地时还要处理 modal 的上下边界、最大允许位移以及越界回弹,并用 WXS 或动画 API 降低 setData 通信压力。按这个思路,松手后的惯性滑动不会生硬,也不会滑出容器。

拖拽惯性要解决的核心问题是:手指离开屏幕之后,modal 继续移动多远、以什么样的减速曲线停下来。微信小程序提供的触摸事件已经包含足够信息,只是不能在 touchend 里获取系统级惯性参数,需要自己根据 touchmove 记录的速度近似计算。下面先给出一个可运行的自定义 modal 拖拽基础结构,然后分别讨论速度采样、距离公式、边界回弹和性能优化。

微信小程序自定义modal拖拽惯性如何计算?根据拖拽速度计算惯性滑动距离

一、速度采样:把 touchmove 转成可计算的末速度

触摸事件对象中,e.touches[0].clientY 表示当前触摸点的纵坐标,时间戳可以用 Date.now() 或 e.timeStamp。由于 touchmove 触发频率高,手指微小抖动会带来速度噪声,所以不能只取最后两个点计算瞬时速度,最好维护一个最近速度数组。取最近 5 次采样做平均,或者对速度做指数平滑,可以让最终传入惯性计算的末速度更稳定。

下面是 Page 中触摸事件的基础实现,主要完成三件事:记录起始位置、在每次移动时更新 modal 位移、把最近几帧的位移与时间差存入速度队列。

Page({
  data: {
    modalY: 0,
    dragging: false
  },
  onTouchStart(e) {
    const touch = e.touches[0];
    this.dragState = {
      startY: touch.clientY,
      lastY: touch.clientY,
      lastTime: Date.now(),
      velocities: []
    };
  },
  onTouchMove(e) {
    const touch = e.touches[0];
    const state = this.dragState;
    if (!state) return;
    const now = Date.now();
    const dt = now - state.lastTime;
    if (dt > 0) {
      const dy = touch.clientY - state.lastY;
      const velocity = dy / dt;
      state.velocities.push(velocity);
      if (state.velocities.length > 5) {
        state.velocities.shift();
      }
    }
    const moveY = touch.clientY - state.startY;
    this.setData({
      modalY: moveY
    });
    state.lastY = touch.clientY;
    state.lastTime = now;
  },
  onTouchEnd() {
    const state = this.dragState;
    if (!state) return;
    const velocity = this.getFinalVelocity(state.velocities);
    const distance = this.calcInertiaDistance(velocity);
    this.startInertia(distance);
    this.dragState = null;
  }
});

速度单位在这里是像素每毫秒,因为 clientY 是像素,时间差是毫秒。实际取 5 次速度队列是为了抹平最后几帧的抖动,如果希望更跟手,可以增加最近一帧的权重,例如使用 0.7 和 0.3 的指数平滑。得到末速度后,下一步就是把它转换成距离。

二、惯性距离计算与减速动画

如果把松手后的减速过程简化为匀减速运动,设末速度为 v0,摩擦减速度为 a,最终速度归零时满足 v0² = 2as,所以惯性距离 s = v0² / (2a)。这里 a 决定阻尼感,越小滑得越远,越大停得越快。以 0.002 px/ms² 为例,末速度 0.6 px/ms 对应的距离约为 90px,属于轻扫的常规范围。

计算时代码可以写成:

getFinalVelocity(velocities) {
  if (!velocities || velocities.length === 0) return 0;
  const recent = velocities.slice(-3);
  const sum = recent.reduce((acc, v) => acc + v, 0);
  return sum / recent.length;
},
calcInertiaDistance(velocity) {
  const deceleration = 0.002;
  const direction = velocity > 0 ? 1 : -1;
  const distance = (velocity * velocity) / (2 * deceleration);
  return distance * direction;
}

注意 velocity 带符号,平方后要再乘方向,否则向上拖动也会被算成正距离。计算得到惯性距离后,不能一次性把目标位置写进 modalY,否则会显得跳变,需要配合动画逐帧过渡。可以用 setInterval 以 16ms 左右的间隔更新位置,并套用 ease-out 三次缓动,让减速更自然。

startInertia(distance) {
  const startY = this.data.modalY;
  const targetY = this.clampTarget(startY + distance);
  const change = targetY - startY;
  const duration = Math.min(Math.abs(distance) / 0.4, 500);
  const startTime = Date.now();
  clearInterval(this.inertiaTimer);
  this.inertiaTimer = setInterval(() => {
    const elapsed = Date.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 3);
    this.setData({
      modalY: startY + change * eased
    });
    if (progress >= 1) {
      clearInterval(this.inertiaTimer);
      this.inertiaTimer = null;
    }
  }, 16);
}

上面把动画时长与惯性距离做了简单关联:距离越大,动画持续时间会略长,但最长限制在 500ms,避免长距离滑动拖沓。定时器在页面卸载时要注意清理,否则可能造成内存泄漏。基础库 2.9.0 以上还可以用 this.animate 直接驱动节点动画,性能更好,但需要提前给节点配置动画参数。

三、边界限制与越界回弹

自定义 modal 一般不在全屏任意位置滑动,而是限制在某个容器内。假设 modal 只能从顶部 0 移动到容器底部减去自身高度,那么所有目标位置都要经过边界收拢。跟手阶段可以完全跟随手指,但惯性目标必须先调用 clampTarget,否则快速滑动后 modal 会冲出可视区域。

clampTarget(target) {
  const maxY = this.data.containerHeight - this.data.modalHeight;
  return Math.max(0, Math.min(target, maxY));
}

如果拖拽过程中已经越过边界,直接使用线性跟手会让用户感觉很硬。常见做法是超过边界的那一段位移乘一个 0.3 左右的阻尼系数,使 modal 看起来像被边缘挡住。例如 moveY 大于最大位移时,只取 maxY + (moveY - maxY) * 0.3。这个值只影响跟手显示,不影响松手后的惯性目标计算。

回弹动画可以复用前面逐帧动画逻辑,只是目标位置变成边界值。用户越界松手后,惯性目标被 clamp 到边界,动画自然会朝着边界回弹。为了代码清晰,建议把动画函数抽象成 animateYTo(targetY, duration),跟手结束和回弹都调用它,内部负责清理旧定时器、缓动和结束回调。

四、WXS 降低逻辑层通信开销

如果每次 touchmove 都通过 setData 把新坐标发到渲染层,低端设备上可能出现拖拽不跟手。微信小程序的 WXS 可以在渲染层直接响应触摸事件并修改节点样式,省去逻辑层往返。做法是把触摸记录、速度队列和基础位移计算放到 WXS 中,松手后再把末速度通过 callMethod 回传逻辑层。

下面是一个简化版 WXS 模块,它只负责跟手位移和末速度统计:

var state = {
  startY: 0,
  lastY: 0,
  lastTime: 0,
  velocities: []
};

function touchstart(event, ownerInstance) {
  var touch = event.touches[0];
  state.startY = touch.clientY;
  state.lastY = touch.clientY;
  state.lastTime = Date.now();
  state.velocities = [];
}

function touchmove(event, ownerInstance) {
  var touch = event.touches[0];
  var now = Date.now();
  var dt = now - state.lastTime;
  if (dt > 0) {
    var velocity = (touch.clientY - state.lastY) / dt;
    state.velocities.push(velocity);
    if (state.velocities.length > 5) {
      state.velocities.shift();
    }
  }
  state.lastY = touch.clientY;
  state.lastTime = now;
  var offsetY = touch.clientY - state.startY;
  ownerInstance.selectComponent('.modal').setStyle({
    transform: 'translateY(' + offsetY + 'px)'
  });
}

function touchend(event, ownerInstance) {
  var sum = 0;
  for (var i = 0; i < state.velocities.length; i++) {
    sum += state.velocities[i];
  }
  var velocity = state.velocities.length ? sum / state.velocities.length : 0;
  ownerInstance.callMethod('onInertiaStart', { velocity: velocity });
}

module.exports = {
  touchstart: touchstart,
  touchmove: touchmove,
  touchend: touchend
};

回到逻辑层后,onInertiaStart 接收速度,再走前面的距离计算公式和 JS 动画。这样高频的跟手操作留在渲染层,低频的惯性启动和参数计算留在逻辑层,整体响应会顺畅很多。如果项目对兼容性要求较高,也可以继续使用 setData 方案,但需要控制每帧更新的数据量,只更新 modal 的 transform 属性,避免整页重绘。

整体来看,惯性滑动并不是某个孤立参数,而是速度采样、减速模型、边界策略和渲染方式共同作用的结果。先把末速度算准,再选择一个合适的减速度,最后用 clamp 和回弹兜底,自定义 modal 在松手后的滑动就会接近原生拖拽体验。

微信小程序拖拽惯性计算自定义modal修改时间:2026-09-23 03:53:52

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0923/60743.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。