docs: document imperative scrollBy/stopScroll in README
This commit is contained in:
@@ -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
@@ -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"
|
||||
})
|
||||
}
|
||||
>
|
||||
↓ 下移一屏
|
||||
|
||||
Reference in New Issue
Block a user