橡皮筋:越界阻尼与回弹
iOS 的列表拖到顶还能再拽一段,但越拽越沉,松手弹回。这个手感由两段拼成:边界之内内容 1:1 跟手;越界之后,真实位移按一条阻尼函数对跟手位移做压缩,渐近饱和,怎么拽都到不了无穷。
其中 是越界量(跟手位移减去边界,界内不走这条式子), 是边界, 是阻尼系数(像素)。
1 · 两段拼接的手感
2 · 这条函数为什么好用
它有三个性质,缺一个手感就会塌。
渐近饱和: 时 ,越界量被压在 以内,所以怎么拽都拽不穿。 因此不是「阻力大小」而是「最多能拽出多远」,调它等于调那条看不见的天花板。
在边界处与跟手段一阶连续:导数是 ,在 处恰为 1,与界内的 1:1 跟手完全接上。这是这条式子相比其他饱和函数(如 或对数)的优势——不必额外配系数就自动衔接,越过边界的那一瞬不会有速度突变的卡顿感。
严格递增因而可逆:每个实际位移唯一对应一个跟手位移,松手回弹与继续拖拽之间可以随时切换,不会出现两个跟手位置映射到同一屏幕位置的歧义。
3 · 松手后交给谁
松手时分三种情况。已越界,用 createSpring 以当前位置与松手速度为初始条件、目标设为边界值拉回;在界内且甩得够快,用 createDecay 带速度滑行;否则原地不动。
第三种衔接最有意思:decay 滑行途中每帧检查是否滑出边界,一旦越界立刻废弃这条 decay、就地建一条 spring 拉回。两个物理模型在同一帧交接,而交接点的速度由 decay 的 velocity() 提供——惯性衰减那一页讲过它是位移的严格导数,正因如此交接才不会跳变。
import { createSpring, createDecay } from '@vega/anim';
// 越界阻尼: 界内 1:1, 越界部分按 limit + d·(1 − 1/(over/d + 1)) 压缩
function rubberBand(raw, limit, d) {
if (raw <= limit) return raw; // 边界之内, 跟手
const over = raw - limit; // 越界量
return limit + d * (1 - 1 / (over / d + 1)); // 渐近饱和到 limit + d
}
// 松手且越界: spring 拉回边界, 接住松手速度 velocity
const spring = createSpring({ stiffness, damping, mass: 1,
from: y, to: edge, velocity });
// 界内甩动: decay 带速度滑行, 滑出界再交给上面的 spring
const decay = createDecay({ velocity, power: 0.5, from: y });
回弹的物理形态(欠阻尼时会小幅过冲再收敛)见弹簧。把这一整套接到真实 pointer 手势上、做成可复用的拖拽行为,见手势。