docs: document imperative scrollBy/stopScroll in README

This commit is contained in:
2026-06-27 20:31:43 +08:00
parent fa29cffa73
commit 1be34d1479
2 changed files with 98 additions and 4 deletions
+88 -2
View File
@@ -7,6 +7,7 @@
- **四向无限环绕** — 任意方向无限滑动,网格坐标取模映射到数据下标,无缝衔接。
- **视口虚拟化** — 只为「视口 + 一圈缓冲」创建少量节点,节点数与 `items` 总量解耦;平移只改一次容器偏移(O(1)),跨边界时回收/新建少数格子。
- **拖拽 + 惯性** — 拖拽平移,松手按 `friction` 衰减滑动。
- **命令式平滑平移** — 通过 `ref` 调用 `scrollBy`,在指定时长内按贝塞尔曲线(曲速)或预设缓动平移画布,可随时 `stopScroll` 停止。
- **响应式布局** — 布局 props 可传函数,依容器尺寸动态计算;内部 `ResizeObserver` 监听并按签名去重,无变化零开销。
- **可视区域回调** — `onVisibleCardsChange` 实时拿到屏幕内的卡片及其可视度(0~1)。
- **移动 / 悬停回调** — `onMovingChange` 监听拖拽与惯性的起止,`onCardHover` 监听指针进出卡片。
@@ -121,6 +122,49 @@ const byWidth = <V,>(w: number, sm: V, md: V, lg: V): V =>
性能上回调用 `requestAnimationFrame` 合帧、并对结果按可视度量化去重,空闲时零开销;容器尺寸变化也会触发重算(边缘卡片可视度会变)。
## 命令式平滑平移(scrollBy
通过 `ref` 拿到画布句柄,调用 `scrollBy` 让画布内容在指定时长内平滑平移,缓动可传贝塞尔控制点(曲速)或预设名;`stopScroll` 随时停在当前位置。
```tsx
import { useRef } from "react";
import { InfiniteCanvas } from "./components/InfiniteCanvas";
import type { InfiniteCanvasHandle } from "./components/InfiniteCanvas";
function App() {
const canvasRef = useRef<InfiniteCanvasHandle>(null);
return (
<>
<InfiniteCanvas ref={canvasRef} items={items} renderCard={renderCard} />
{/* 600ms 内向上平移 400px,结尾减速 */}
<button onClick={() => canvasRef.current?.scrollBy({ y: -400, duration: 600, easing: "ease-out" })}>
</button>
{/* 贝塞尔曲速:900ms 内向右平移 800px */}
<button onClick={() => canvasRef.current?.scrollBy({ x: -800, duration: 900, easing: [0.22, 1, 0.36, 1] })}>
</button>
{/* 不传 duration(或 ≤0)则立即跳变,无动画 */}
<button onClick={() => canvasRef.current?.scrollBy({ x: 200 })}></button>
{/* 中途停止 */}
<button onClick={() => canvasRef.current?.stopScroll()}></button>
</>
);
}
```
行为说明:
- `x > 0` 内容右移、`y > 0` 内容下移(与浏览器 `Element.scrollBy` 方向相反,本组件移动的是内容而非视口)。
- `easing` 可传 cubic-bezier 控制点 `[x1, y1, x2, y2]`(自定义曲速),或预设名 `"linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out"`,默认 `"ease-out"`
- 用户一旦拖拽即打断当前程序化动画并接管;重复调用 `scrollBy` 会从当前位置替换上一个动画。
- 平移期间 `onMovingChange``onVisibleCardsChange` 等回调照常触发。
## API
### `InfiniteCanvas<T>` Props
@@ -147,6 +191,24 @@ const byWidth = <V,>(w: number, sm: V, md: V, lg: V): V =>
| `className` | `string` | — | 容器 `className`。 |
| `style` | `CSSProperties` | 填满父级 100%×100% | 容器内联样式。 |
### 命令式句柄 `InfiniteCanvasHandle`
通过 `ref` 获取,提供程序化平移能力。
| 方法 | 签名 | 说明 |
| --- | --- | --- |
| `scrollBy` | `(options: ScrollByOptions) => void` | 让画布内容平滑平移指定像素。 |
| `stopScroll` | `() => void` | 立即停止当前程序化动画(停在当前位置)。 |
`ScrollByOptions`
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `x` | `number` | `0` | x 方向位移(px),正值=内容右移。 |
| `y` | `number` | `0` | y 方向位移(px),正值=内容下移。 |
| `duration` | `number` | `0` | 动画时长(ms),`0` 或负数=立即跳变。 |
| `easing` | `Easing` | `"ease-out"` | 缓动曲线。 |
### 类型
```ts
@@ -163,6 +225,29 @@ export interface VisibleCard<T> {
rect: { x: number; y: number; width: number; height: number }; // 相对容器左上角的位置与完整尺寸
key: string; // 本次出现实例的稳定标识(形如 "col,row" 的虚拟网格坐标),可用作 React list key
}
/** cubic-bezier 控制点 [x1,y1,x2,y2],或预设缓动名 */
export type Easing =
| [number, number, number, number]
| "linear"
| "ease"
| "ease-in"
| "ease-out"
| "ease-in-out";
/** scrollBy 选项 */
export interface ScrollByOptions {
x?: number; // x 方向位移(px),正值=内容右移
y?: number; // y 方向位移(px),正值=内容下移
duration?: number; // 动画时长(ms),0 = 立即跳变
easing?: Easing; // 缓动曲线,默认 "ease-out"
}
/** 通过 ref 暴露的命令式句柄 */
export interface InfiniteCanvasHandle {
scrollBy(options: ScrollByOptions): void;
stopScroll(): void;
}
```
## 工作原理
@@ -173,8 +258,9 @@ export interface VisibleCard<T> {
2. **视口虚拟化** — 只为「视口 + `overscan` 圈缓冲」维护活动节点;平移跨越网格边界时,离开的节点销毁、新进入的用 `renderCard` 新建。节点开销与 `items` 总量彻底解耦,百万级数据也只渲染几十个节点。
3. **取模环绕** — 由 `virtualGrid.indexAt` 将无限延伸的整数网格坐标 `(col, row)` 取模映射到 `items` 下标,环绕不依赖节点是否真实存在。
4. **惯性滑动** — 松手记录速度,每帧按 `friction` 衰减直到停止。
5. **可视扫描**平移或 resize 后用 rAF 合帧,扫描活动节点与视口的重叠面积算出可视度,并对结果量化去重后回调
6. **响应式重建**`ResizeObserver` 监听容器,仅当几何签名(列数/卡片尺寸/间距)变化时才丢弃节点重建;仅视口或 `overscan` 变化时走更便宜的对账分支
5. **程序化平移**`scrollBy` 用独立 rAF 按缓动曲线分帧推进同一套平移逻辑(每帧按进度差量平移,避免累计误差);与惯性各用独立句柄,拖拽按下即打断
6. **可视扫描**平移或 resize 后用 rAF 合帧,扫描活动节点与视口的重叠面积算出可视度,并对结果量化去重后回调
7. **响应式重建**`ResizeObserver` 监听容器,仅当几何签名(列数/卡片尺寸/间距)变化时才丢弃节点重建;仅视口或 `overscan` 变化时走更便宜的对账分支。
## 本地运行
+10 -2
View File
@@ -154,7 +154,11 @@ function App() {
<button
className="rounded bg-white/15 px-3 py-1.5 text-sm text-white backdrop-blur hover:bg-white/25"
onClick={() =>
canvasRef.current?.scrollBy({ y: 500, duration: 600, easing: "ease-out" })
canvasRef.current?.scrollBy({
y: 500,
duration: 600,
easing: "ease-out"
})
}
>
@@ -162,7 +166,11 @@ function App() {
<button
className="rounded bg-white/15 px-3 py-1.5 text-sm text-white backdrop-blur hover:bg-white/25"
onClick={() =>
canvasRef.current?.scrollBy({ y: -500, duration: 600, easing: "ease-out" })
canvasRef.current?.scrollBy({
y: -500,
duration: 600,
easing: "ease-out"
})
}
>