关于 fluid-interactions
用于借助 motion 和 React 构建手势驱动界面的技能,提供交互模式及实现指导,涵盖动画中断、惯性、边缘阻力和减少动态效果。
主要功能
- 01弹簧手势与释放速度传递
- 02可中断的动画
- 03惯性与吸附点指导
- 04方向锁定与边缘阻力
- 05支持减少动态效果
- 06React Hooks 与物理工具
适用场景
- 01实现可拖动的底部面板
- 02构建滑动导航
- 03为自定义滚动器添加惯性
- 04实现精细媒体拖动
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rndrealm/ fluid-interactions --skill fluid-interactions安装后在 Claude Code 中输入/fluid-interactions确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
code/physics.tsuse-drag.tsuse-motion-preference.tsreferences/direction-lock.mddrag-and-momentum.mdgesture-modes.mdmorphing.mdpointer-events.mdreduced-motion.mdscrubbing.mdsheets.mdsprings.mdswipe-back.md详细介绍
流畅交互
让界面元素像实体一样运动,而不是按时间线播放动画。包括:
- 手势: 元素跟随手指移动,松手时继承手指的速度,滑动时落在预期位置,在边缘处产生阻力,并且可以在动画中途被抓住。
- 转场: 面板、导航和视图从你点击的元素处展开,并且可以拖回。
- 手势模式: 同一个手势会根据移动方式执行不同操作。
- 确保在各种情况下都表现良好: 触控方面的特殊情况、性能和减少动态效果。
使用 motion
始终使用 motion 构建这些交互。它的弹簧动画可以从初始速度开始,运动值可以在不触发重新渲染的情况下更新,并且会遵循减少动态效果设置——这正是本技能所依赖的一切。不要用 GSAP、anime.js 或 CSS 转场来实现这些交互;此处和参考资料中的代码都基于 motion。(唯一一个手写循环是在自定义滚动器中实现惯性滚动,它仍会写入运动值,并交由 motion 弹簧继续处理。)
如果 package.json 中尚未包含它,请在编写任何代码前使用项目的包管理器安装(查看锁文件):
npm install motion # package-lock.json
pnpm add motion # pnpm-lock.yaml
yarn add motion # yarn.lock
bun add motion # bun.lock / bun.lockb
- React: 从
motion/react导入(motion、animate、useMotionValue、useTransform、MotionConfig、useReducedMotion)。 - 已安装
framer-motion: 它就是旧名称下的 motion。直接使用并从framer-motion导入;不要同时安装两者。 - 已使用其他动画库: 继续用它实现现有功能,并添加 motion 来构建这些交互。不要用其他库重新实现这些模式。
绝不要用持续时间和缓动曲线(elastic、back、贝塞尔曲线过冲)来伪造弹簧动画。它们无法从手指的速度开始。
规则
- 使用弹簧动画,而不是固定时长。 弹簧只需要一个目标值和一个起始速度,因此可以从手指离开时的位置接着运动。使用
springFromResponse,通过响应时间(秒)和阻尼比来描述弹簧,而不是直接设置刚度和阻尼。参见弹簧。 - 将释放速度传给弹簧。 使用
VelocityTracker跟踪指针(仅追踪最近 100ms),并将velocity传入动画。否则,元素会先停顿一帧,再开始运动。 - 所有动画都必须可中断。 指针按下时,调用动画的
stop(),并将当前值作为新的起点。动画进行时,绝不要阻塞输入。根据动画正在前往的方向来判断状态(打开还是关闭、当前是哪个页面),而不是根据当前值或onComplete:弹簧看起来早已完成,实际上还在运动。参见拖动与惯性和根据目标值判断。 - 根据运动的方向决定最终落点。 释放时,
project(velocity)会给出轻扫后滑行到的位置;从该位置寻找最近的目标点并对齐,而不是从释放点开始。这样短促而快速的轻扫才可以跳过一个卡点。 - 越过边缘时使用橡皮筋效果,而不是硬性停止:
applyRubberBand(value, min, max, d)。拉得越远,移动幅度越小。 - 没有效果的方向要产生阻力。 如果只有一个方向会触发操作(向下关闭、向右返回),其他方向仍可移动,但要有橡皮筋效果,并在释放时弹回。不要冻结这些方向,也不要让它们自由移动:自由移动会让人以为该方向有对应操作。此规则仅适用于用户正按住并可自由移动的元素。如果另一条轴属于其他内容(页面滚动、父级列表),应通过方向锁交由其处理,而不是添加橡皮筋效果。参见拖动与惯性。
- 先锁定方向,再执行操作。 等待约 10px 后,再确定所沿的轴。只有锁定方向后才捕获指针,这样轻触和另一条轴(通常是原生滚动)仍可正常工作。参见方向锁。
- 逐帧操作绝不触碰 React 状态。 使用 refs 和 motion 值(或直接写入 DOM)。仅在释放时或发生离散变化时设置状态。
- 使用变换移动,而不是改变布局。 使用
x、y、scale、opacity。只在手势开始或结束时读取布局(getBoundingClientRect、clientWidth),绝不要每帧都读取。 - 始终结束手势。 处理
pointerup、pointercancel和lostpointercapture。如果手势没有正确结束,UI 就会卡住。参见指针事件。 - 遵循减少动态效果偏好,减弱自行运动的效果,而不是由手指驱动的效果。参见减少动态效果。
- 一种手势可以表示多种操作。 手势的移动方式(起点、距离、速度、停顿、第二条轴)可以决定相关操作中的哪一种,例如向上滑动返回主屏幕,或停顿后打开应用切换器;也可以在远离滑块时进行更精细的调节。基础操作必须始终可用,并且必须在释放前显示当前模式。参见手势模式。
代码
将这些文件复制到项目中并按需调整。physics.ts 没有依赖项;这些 hooks 需要 React,而 use-motion-preference.ts 需要 motion。
- code/physics.ts:
springFromResponse、noBounce、VelocityTracker、rubberBand、applyRubberBand、rubberBandInverse、project、DECELERATION、nearest、nearestPoint、clamp、FADE。 - code/use-drag.ts:一个基于指针事件的拖动 hook,用于报告相对于起点的偏移量和释放速度,并始终调用
onEnd。 - code/use-motion-preference.ts:
{ reduceMotion, spring };启用减少动态效果时,spring()会去掉回弹效果。
每种手势的基本结构
const x = useMotionValue(0);
const origin = useRef(0);
const drag = useDrag({
onStart: () => {
x.stop(); // catch it mid-flight
origin.current = x.get(); // continue from where it is on screen
},
onMove: (delta) => {
x.set(applyRubberBand(origin.current + delta.x, min, max, width));
},
onEnd: (velocity) => {
const landing = x.get() + project(velocity.x);
animate(x, nearest(landing, targets), {
...spring(SNAP),
velocity: velocity.x,
});
},
});
return (
<motion.div {...drag} style={{ x }} className="touch-none select-none" />
);
模式
请阅读与所构建模式对应的参考文档:
| 构建内容 | 阅读 |
|---|---|
| 任意弹簧效果、选取响应和阻尼 | 弹簧 |
| 可拖动或可轻扫的元素、吸附到角落或点、自定义滚动器(带惯性和分页) | 拖动和惯性 |
| 垂直列表中的水平滑动、滑动操作、下拉刷新 | 方向锁定 |
| 带有停靠位置的底部面板 | 面板 |
| 支持精细拖动的视频或音频进度条 | 精细拖动 |
| 放大为详情视图并可向下拖动关闭的缩略图 | 形变 |
| 在导航栈中通过边缘滑动返回 | 滑动返回 |
| 有多种结果或模式的单个手势(主屏幕横条、精细拖动、拉伸式滑动操作) | 手势模式 |
| 减少动态效果 | 减少动态效果 |
| 手势冻结、卡住或导致触摸点击失灵 | 指针事件 |
合适的默认值
| 项目 | 值 |
|---|---|
| 大多数 UI 弹簧效果 | springFromResponse(0.35–0.45, 0.85–0.9) |
| 导航、分页,以及任何不应产生过冲的效果 | 阻尼比 1 |
| 速度窗口 | 最近 100ms |
| 方向锁定阈值 | 10px |
| 严格轴向的锁定比率 | 主导轴是另一轴的 2 倍 |
| 橡皮筋常量 | c = 0.55,d = 容器在该轴上的尺寸 |
| 投影减速 | 0.998(正常),0.99(快速停止) |
| 边缘滑动区域 | 距边缘 24px |
| 减少动态效果时的淡出 | 200ms 缓出 |
有两项选择有各自的指导原则,请勿凭空猜测:弹簧应有多大回弹幅度(弹簧),以及滚动器应正常惯性滑动、快速停止还是分页(拖动和惯性)。不确定时,选择无回弹和正常减速。
验收前检查清单
- 在动画进行到一半时抓住它:它会原地停下,不会跳动。
- 动画看起来刚结束就立刻再次操作(打开另一行、点击显示出的页面、向另一方向滑动):它会立即响应,不会有四分之一秒的延迟。
- 快速滑动:它会以手指的速度离开,松手时不会卡顿。
- 拖过边缘:它会产生阻力并弹回。
- 放下一个可以停在边界内任意位置的东西:它会准确停在那里,不会晃动。
- 朝不起作用的方向拖动:它会产生阻力(不会冻结,也不会自由移动),然后弹回。
- 在手机上慢慢拖动:它会跟随手指,不会滚动页面(
touch-action)。 - 可拖动区域内的按钮和行仍可正常点击。
- 拖动过程中按下第二根手指:不会出问题。
- 拖动过程中切换应用或打开上下文菜单:它会停下来,不会卡住。
- 开启减少动态效果:不会过冲,大幅自动移动会淡出,拖动不受影响。
评论
支持 Markdown