mcpField
F
fluid-interactions

fluid-interactions

github.com/rndrealm设计与 UI3190 次安装6 天前更新
代码仓库

关于 fluid-interactions

用于借助 motion 和 React 构建手势驱动界面的技能,提供交互模式及实现指导,涵盖动画中断、惯性、边缘阻力和减少动态效果。

主要功能

  1. 01弹簧手势与释放速度传递
  2. 02可中断的动画
  3. 03惯性与吸附点指导
  4. 04方向锁定与边缘阻力
  5. 05支持减少动态效果
  6. 06React Hooks 与物理工具

适用场景

  1. 01实现可拖动的底部面板
  2. 02构建滑动导航
  3. 03为自定义滚动器添加惯性
  4. 04实现精细媒体拖动

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rndrealm/fluid-interactions --skill fluid-interactions

安装后在 Claude Code 中输入/fluid-interactions确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

code/
physics.ts
use-drag.ts
use-motion-preference.ts
references/
direction-lock.md
drag-and-momentum.md
gesture-modes.md
morphing.md
pointer-events.md
reduced-motion.md
scrubbing.md
sheets.md
springs.md
swipe-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、贝塞尔曲线过冲)来伪造弹簧动画。它们无法从手指的速度开始。

规则

  1. 使用弹簧动画,而不是固定时长。 弹簧只需要一个目标值和一个起始速度,因此可以从手指离开时的位置接着运动。使用 springFromResponse,通过响应时间(秒)和阻尼比来描述弹簧,而不是直接设置刚度和阻尼。参见弹簧。
  2. 将释放速度传给弹簧。 使用 VelocityTracker 跟踪指针(仅追踪最近 100ms),并将 velocity 传入动画。否则,元素会先停顿一帧,再开始运动。
  3. 所有动画都必须可中断。 指针按下时,调用动画的 stop(),并将当前值作为新的起点。动画进行时,绝不要阻塞输入。根据动画正在前往的方向来判断状态(打开还是关闭、当前是哪个页面),而不是根据当前值或 onComplete:弹簧看起来早已完成,实际上还在运动。参见拖动与惯性和根据目标值判断。
  4. 根据运动的方向决定最终落点。 释放时,project(velocity) 会给出轻扫后滑行到的位置;从该位置寻找最近的目标点并对齐,而不是从释放点开始。这样短促而快速的轻扫才可以跳过一个卡点。
  5. 越过边缘时使用橡皮筋效果,而不是硬性停止:applyRubberBand(value, min, max, d)。拉得越远,移动幅度越小。
  6. 没有效果的方向要产生阻力。 如果只有一个方向会触发操作(向下关闭、向右返回),其他方向仍可移动,但要有橡皮筋效果,并在释放时弹回。不要冻结这些方向,也不要让它们自由移动:自由移动会让人以为该方向有对应操作。此规则仅适用于用户正按住并可自由移动的元素。如果另一条轴属于其他内容(页面滚动、父级列表),应通过方向锁交由其处理,而不是添加橡皮筋效果。参见拖动与惯性。
  7. 先锁定方向,再执行操作。 等待约 10px 后,再确定所沿的轴。只有锁定方向后才捕获指针,这样轻触和另一条轴(通常是原生滚动)仍可正常工作。参见方向锁。
  8. 逐帧操作绝不触碰 React 状态。 使用 refs 和 motion 值(或直接写入 DOM)。仅在释放时或发生离散变化时设置状态。
  9. 使用变换移动,而不是改变布局。 使用 x、y、scale、opacity。只在手势开始或结束时读取布局(getBoundingClientRect、clientWidth),绝不要每帧都读取。
  10. 始终结束手势。 处理 pointerup、pointercancel 和 lostpointercapture。如果手势没有正确结束,UI 就会卡住。参见指针事件。
  11. 遵循减少动态效果偏好,减弱自行运动的效果,而不是由手指驱动的效果。参见减少动态效果。
  12. 一种手势可以表示多种操作。 手势的移动方式(起点、距离、速度、停顿、第二条轴)可以决定相关操作中的哪一种,例如向上滑动返回主屏幕,或停顿后打开应用切换器;也可以在远离滑块时进行更精细的调节。基础操作必须始终可用,并且必须在释放前显示当前模式。参见手势模式。

代码

将这些文件复制到项目中并按需调整。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