在微信小程序里用自定义 tabbar 加上一个可拖拽的悬浮按钮,已经成了不少工具类应用的标配。但真要把拖拽手感做舒服,光靠 catchtouchmove 改 left 和 top 是不够的。用户快速甩动按钮后松手,按钮如果立刻停住会显得很生硬;拖到屏幕边缘时如果直接卡死或者飞出可视区,又会误触或遮挡内容。这时候就需要两套配合的算法:边缘回弹与速度衰减。

为什么需要边缘回弹与速度衰减
普通拖拽逻辑通常是手指在哪按钮就在哪,松开手位置不变。这种方式在桌面端还行,在手机上就有两个问题。第一,手指粗、屏幕小,用户很容易把按钮拖到贴近状态栏、底部安全区甚至超出小程序可视范围,导致点不到或者挡住系统手势。第二,人拖东西有惯性,松手瞬间手指速度可能很快,如果按钮瞬间静止,大脑会觉得“东西断了”,不是自然物理世界的反馈。
边缘回弹解决的是空间边界问题,它让按钮在离开安全区域时被一个隐形弹簧拉回来。速度衰减解决的是时间连续性问题,它让按钮按松手前的速度继续滑行一小段,再慢慢停下。两者结合,拖拽就有了“重量感”和“弹性”,这是很多成熟 App 悬浮球手感好的核心原因。
拖拽时如何采集速度与位置
要实现衰减和回弹,第一步是在 touchmove 里持续记录按钮的实时位置和时间间隔。微信小程序的 touch 事件对象里,e.touches[0].clientX 和 clientY 是相对于视口的坐标,e.timeStamp 是当前事件时间戳。我们在 js 的 data 外挂一个变量,保存上一次的坐标和时间,就能算出这一帧的位移和速度。
具体做法是:每次 touchmove 触发,用当前 clientX 减上次 clientX 得出 dx,用当前 timeStamp 减上次 timeStamp 得出 dt,速度 vx = dx / dt。为防抖动,可以取最近三帧速度做简单平均。同时把按钮的 left、top 直接设为当前 clientX 减按钮半径,保证手指“抓”在按钮中心。这样我们就拿到了松手时所需的初始速度和位置,供后续动画使用。
边缘回弹的弹簧模型
边缘回弹一般用弹簧振子模型,也叫胡克定律思路。假设安全区左边界是 safeLeft,按钮左缘 x 小于 safeLeft 时,产生一个向右的回弹力 F = -k * (x - safeLeft),k 是刚度系数。每帧把力转成加速度,再积分到速度、位置,按钮就会像被橡皮筋拉着回弹。为了不让它一直晃,还要加阻尼,让振幅快速变小。
实际小程序里可以简化:如果松手时 x 越界,直接给一个回弹目标值 targetX = safeLeft,然后用缓动函数 left = current + (target - current) * 0.2 每帧逼近,连跑十几帧就回去了。这种写法没有真实物理弹簧严谨,但肉眼看和回弹几乎一样,且不会因参数没调好导致按钮疯抖。下面给出常见边界处理对照:
| 边界情况 | 回弹目标 | 处理方式 |
|---|---|---|
| 左缘小于安全左区 | safeLeft | 每帧向左缓动逼近 |
| 右缘大于安全右区 | screenWidth - btnW - safeRight | 每帧向右缓动逼近 |
| 上缘小于状态栏高 | statusBarHeight | 向下缓动逼近 |
| 下缘大于可用高度 | usableHeight - btnH | 向上缓动逼近 |
速度衰减的滑行实现
松手时如果按钮在安全区内,就进入衰减滑行。我们用 touchmove 最后算出的 vx、vy 作为初速度,每帧执行 x = x + vx * frameTime,然后 vx = vx * friction。friction 取 0.92 到 0.96 之间,代表每帧损失百分之四到八的速度。当速度绝对值低于某个阈值,比如 0.1px/帧,就停止动画并做一次边缘检查,若此时位置越界再走回弹逻辑。
滑行必须用 wx.createAnimation 或者自己用 requestAnimationFrame 驱动,不建议在 setData 里直接写死时长动画,因为衰减是动态算出来的。自己管动画循环能随时根据新速度、新边界调整,也不会出现官方 animation 播完才发觉越界了的滞后。注意每次循环结束要手动取消,否则页面隐藏后还在跑会耗电。
用 requestAnimationFrame 组合两者
小程序支持 wx.requestAnimationFrame,我们可以写一个 tick 函数:先判断是否在越界,越界就套用回弹缓动;没越界但速度未耗尽就套用衰减;两者都不满足就取消帧。这样一套循环同时管回弹和衰减,代码不散。伪结构如下:
function tick() { if (outOfBounds) { applySpring(); } else if (speed > min) { applyFriction(); } else { cancel(); return; } updatePosition(); raf = wx.requestAnimationFrame(tick); }
这种结构的好处是用户松手瞬间如果既在边界外又有速度,会先衰减着往回弹,手感比“先滑再弹”更连贯。调试时把 k 和 friction 打在控制台,用真机试拖三十次就能找到舒服的参数。记住自定义 tabbar 的页面栈里按钮是覆盖层,坐标系要和页面滚动脱钩,只用视口坐标算,别混入 scrollTop。
常见坑与排查
第一个坑是安卓低端机 touchmove 丢帧,导致算出的速度突然暴增,松手后按钮飞出屏幕。解决办法是对单帧速度做上限截断,比如超过 3px/ms 就按 3 算。第二个坑是安全区没算 iPhone 底部小黑条,回弹下边界要拿 wx.getSystemInfoSync 的 safeArea.bottom 来减,不能硬写 20px。
第三个坑是自定义 tabbar 本身有切换动画,如果按钮 position 用 fixed 但父级不是页面根,某些机型会跟随 tabbar 平移。要把悬浮按钮挂到 app 级覆盖层或者 tabbar 组件外层的独立 cover-view 里。只要坐标体系干净,边缘回弹和速度衰减算法就能稳定生效,拖起来像原生一样跟手。
微信小程序自定义tabbar悬浮按钮拖拽边缘回弹算法修改时间:2026-08-12 01:42:38