Merge pull request 'feat/1.0.2' (#3) from feat/1.0.2 into main
Reviewed-on: #3
This commit was merged in pull request #3.
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` 变化时走更便宜的对账分支。
|
||||
|
||||
## 本地运行
|
||||
|
||||
|
||||
+53
-1
@@ -1,7 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Box, Text } from "leafer-ui";
|
||||
import { InfiniteCanvas } from "./components/InfiniteCanvas";
|
||||
import type { VisibleCard } from "./components/InfiniteCanvas";
|
||||
import type {
|
||||
VisibleCard,
|
||||
InfiniteCanvasHandle
|
||||
} from "./components/InfiniteCanvas";
|
||||
import { lazyItems } from "./lib/utils";
|
||||
|
||||
// 入场动画配置(单位:秒)
|
||||
@@ -41,6 +44,9 @@ const byWidth = <V,>(w: number, sm: V, md: V, lg: V): V =>
|
||||
w < 768 ? sm : w < 1440 ? md : lg;
|
||||
|
||||
function App() {
|
||||
// 画布命令式句柄(scrollBy / stopScroll)
|
||||
const canvasRef = useRef<InfiniteCanvasHandle>(null);
|
||||
|
||||
// 当前可视卡片列表
|
||||
const [visible, setVisible] = useState<VisibleCard<Card>[]>([]);
|
||||
|
||||
@@ -125,6 +131,7 @@ function App() {
|
||||
return (
|
||||
<div className="relative w-screen h-screen bg-[#171717]">
|
||||
<InfiniteCanvas
|
||||
ref={canvasRef}
|
||||
items={items}
|
||||
renderCard={renderCard}
|
||||
columns={({ width }) => byWidth(width, 3, 5, 7)}
|
||||
@@ -142,6 +149,51 @@ function App() {
|
||||
onMovingChange={(moving) => console.log("moving", moving)}
|
||||
onVisibleCardsChange={handleVisibleChange}
|
||||
/>
|
||||
{/* 程序化平滑平移 */}
|
||||
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2">
|
||||
<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"
|
||||
})
|
||||
}
|
||||
>
|
||||
↑ 上移一屏
|
||||
</button>
|
||||
<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"
|
||||
})
|
||||
}
|
||||
>
|
||||
↓ 下移一屏
|
||||
</button>
|
||||
<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({
|
||||
x: -800,
|
||||
duration: 900,
|
||||
easing: [0.22, 1, 0.36, 1]
|
||||
})
|
||||
}
|
||||
>
|
||||
→ 曲速右移(贝塞尔)
|
||||
</button>
|
||||
<button
|
||||
className="rounded bg-white/15 px-3 py-1.5 text-sm text-white backdrop-blur hover:bg-white/25"
|
||||
onClick={() => canvasRef.current?.stopScroll()}
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
</div>
|
||||
{/* 实时展示可视卡片:可视度 + 左上角坐标 */}
|
||||
<div className="pointer-events-none absolute right-3 top-3 max-h-[80vh] w-56 overflow-hidden rounded-lg bg-black/60 p-3 text-xs text-white">
|
||||
<div className="mb-1 font-bold">可视卡片 ({visible.length})</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useImperativeHandle, useRef } from "react";
|
||||
import type { CSSProperties, Ref } from "react";
|
||||
import { useInfiniteSlide } from "./useInfiniteSlide";
|
||||
import type { InfiniteCanvasProps } from "./types";
|
||||
import type { InfiniteCanvasProps, InfiniteCanvasHandle } from "./types";
|
||||
|
||||
const fillStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
@@ -12,11 +12,27 @@ const fillStyle: CSSProperties = {
|
||||
|
||||
/**
|
||||
* 通用无限滑动画布。卡片通过 renderCard 插槽以 Leafer 节点形式渲染,
|
||||
* 支持拖拽平移、惯性滑动、点击回调与四向无限环绕
|
||||
* 支持拖拽平移、惯性滑动、点击回调与四向无限环绕。
|
||||
* 通过 ref 暴露命令式 scrollBy / stopScroll(见 InfiniteCanvasHandle)。
|
||||
*/
|
||||
export function InfiniteCanvas<T>(props: InfiniteCanvasProps<T>) {
|
||||
export function InfiniteCanvas<T>({
|
||||
ref,
|
||||
...props
|
||||
}: InfiniteCanvasProps<T> & { ref?: Ref<InfiniteCanvasHandle> }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useInfiniteSlide(containerRef, props);
|
||||
// 引擎在内部把命令式句柄写进 apiRef
|
||||
const apiRef = useRef<InfiniteCanvasHandle | null>(null);
|
||||
useInfiniteSlide(containerRef, props, apiRef);
|
||||
|
||||
// 转发句柄:调用时读 apiRef.current,保证拿到最新闭包(effect 重建后仍有效)
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
scrollBy: (options) => apiRef.current?.scrollBy(options),
|
||||
stopScroll: () => apiRef.current?.stopScroll()
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Easing } from "./types";
|
||||
|
||||
/**
|
||||
* 标准 CSS cubic-bezier 求值:返回 (t)=>y。
|
||||
* 入参 t 是时间轴归一化进度(即贝塞尔的 x),先由 x 牛顿迭代反解参数 s,
|
||||
* 失败回退二分,再用 s 求贝塞尔 y。控制点 P0=(0,0)、P3=(1,1) 固定。
|
||||
*/
|
||||
export function cubicBezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number
|
||||
): (t: number) => number {
|
||||
// 多项式系数(B(t)=((a*t+b)*t+c)*t 形式)
|
||||
const cx = 3 * x1;
|
||||
const bx = 3 * (x2 - x1) - cx;
|
||||
const ax = 1 - cx - bx;
|
||||
const cy = 3 * y1;
|
||||
const by = 3 * (y2 - y1) - cy;
|
||||
const ay = 1 - cy - by;
|
||||
|
||||
const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;
|
||||
const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;
|
||||
const sampleDx = (t: number) => (3 * ax * t + 2 * bx) * t + cx;
|
||||
|
||||
// 由 x 反解贝塞尔参数 t
|
||||
const solve = (x: number) => {
|
||||
let t = x;
|
||||
// 牛顿迭代(最多 8 次)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const xt = sampleX(t) - x;
|
||||
if (Math.abs(xt) < 1e-6) return t;
|
||||
const d = sampleDx(t);
|
||||
if (Math.abs(d) < 1e-6) break;
|
||||
t -= xt / d;
|
||||
}
|
||||
// 回退二分
|
||||
let lo = 0;
|
||||
let hi = 1;
|
||||
t = x;
|
||||
while (lo < hi) {
|
||||
const xt = sampleX(t);
|
||||
if (Math.abs(xt - x) < 1e-6) return t;
|
||||
if (x > xt) lo = t;
|
||||
else hi = t;
|
||||
t = (lo + hi) / 2;
|
||||
}
|
||||
return t;
|
||||
};
|
||||
|
||||
return (t: number) => {
|
||||
if (t <= 0) return 0;
|
||||
if (t >= 1) return 1;
|
||||
return sampleY(solve(t));
|
||||
};
|
||||
}
|
||||
|
||||
/** 预设名 → 控制点 */
|
||||
const PRESETS: Record<string, [number, number, number, number]> = {
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
"ease-in": [0.42, 0, 1, 1],
|
||||
"ease-out": [0, 0, 0.58, 1],
|
||||
"ease-in-out": [0.42, 0, 0.58, 1]
|
||||
};
|
||||
|
||||
/** 把 Easing 联合类型归一成一个求值函数,缺省 ease-out */
|
||||
export function resolveEasing(
|
||||
easing: Easing | undefined
|
||||
): (t: number) => number {
|
||||
if (easing === "linear") return (t) => t;
|
||||
if (Array.isArray(easing)) {
|
||||
return cubicBezier(easing[0], easing[1], easing[2], easing[3]);
|
||||
}
|
||||
const p = PRESETS[easing ?? "ease-out"] ?? PRESETS["ease-out"];
|
||||
return cubicBezier(p[0], p[1], p[2], p[3]);
|
||||
}
|
||||
@@ -1,2 +1,9 @@
|
||||
export { InfiniteCanvas } from "./InfiniteCanvas";
|
||||
export type { InfiniteCanvasProps, Responsive, VisibleCard } from "./types";
|
||||
export type {
|
||||
InfiniteCanvasProps,
|
||||
InfiniteCanvasHandle,
|
||||
ScrollByOptions,
|
||||
Easing,
|
||||
Responsive,
|
||||
VisibleCard
|
||||
} from "./types";
|
||||
|
||||
@@ -103,3 +103,32 @@ export interface InfiniteCanvasProps<T> {
|
||||
/** 容器内联样式(默认填满父级 100% × 100%) */
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/** 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 方向位移(px),默认 0;正值=内容右移 */
|
||||
x?: number;
|
||||
/** y 方向位移(px),默认 0;正值=内容下移 */
|
||||
y?: number;
|
||||
/** 动画时长(ms),默认 0 = 立即跳变 */
|
||||
duration?: number;
|
||||
/** 缓动曲线,默认 "ease-out" */
|
||||
easing?: Easing;
|
||||
}
|
||||
|
||||
/** InfiniteCanvas 命令式句柄(通过 ref 暴露) */
|
||||
export interface InfiniteCanvasHandle {
|
||||
/** 让画布内容平滑平移指定像素 */
|
||||
scrollBy(options: ScrollByOptions): void;
|
||||
/** 立即停止当前程序化动画(停在当前位置) */
|
||||
stopScroll(): void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Leafer, Group, PointerEvent } from "leafer-ui";
|
||||
import type { IUI } from "leafer-ui";
|
||||
import type { InfiniteCanvasProps, Responsive, VisibleCard } from "./types";
|
||||
import type {
|
||||
InfiniteCanvasProps,
|
||||
InfiniteCanvasHandle,
|
||||
ScrollByOptions,
|
||||
Responsive,
|
||||
VisibleCard
|
||||
} from "./types";
|
||||
import { resolveEasing } from "./easing";
|
||||
import { indexAt, visibleRange, rangeEqual } from "./virtualGrid";
|
||||
import type { GridRange } from "./virtualGrid";
|
||||
|
||||
@@ -20,7 +27,8 @@ import type { GridRange } from "./virtualGrid";
|
||||
*/
|
||||
export function useInfiniteSlide<T>(
|
||||
containerRef: React.RefObject<HTMLDivElement | null>,
|
||||
props: InfiniteCanvasProps<T>
|
||||
props: InfiniteCanvasProps<T>,
|
||||
apiRef?: React.RefObject<InfiniteCanvasHandle | null>
|
||||
) {
|
||||
const {
|
||||
items,
|
||||
@@ -445,10 +453,70 @@ export function useInfiniteSlide<T>(
|
||||
raf = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
// —— 程序化平移(scrollBy)——
|
||||
// 程序化动画 rAF 句柄(与惯性 raf 分开,互不干扰)
|
||||
let animRaf = 0;
|
||||
/** 停止当前程序化动画(停在当前位置) */
|
||||
const stopAnim = () => {
|
||||
if (animRaf) {
|
||||
cancelAnimationFrame(animRaf);
|
||||
animRaf = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在 duration(ms) 内按 ease 曲线把内容平移 (dx,dy)。
|
||||
* 每帧按进度算「应到位移」,与上一帧已推进量作差,增量调 shift,避免累计误差。
|
||||
*/
|
||||
const animateBy = (
|
||||
dx: number,
|
||||
dy: number,
|
||||
duration: number,
|
||||
ease: (t: number) => number
|
||||
) => {
|
||||
if (duration <= 0) {
|
||||
shift(dx, dy);
|
||||
return;
|
||||
}
|
||||
const start = performance.now();
|
||||
let dxDone = 0;
|
||||
let dyDone = 0;
|
||||
setMoving(true);
|
||||
const step = () => {
|
||||
const t = Math.min(1, (performance.now() - start) / duration);
|
||||
const p = ease(t);
|
||||
shift(dx * p - dxDone, dy * p - dyDone);
|
||||
dxDone = dx * p;
|
||||
dyDone = dy * p;
|
||||
if (t >= 1) {
|
||||
animRaf = 0;
|
||||
setMoving(false); // 动画自然结束 → 静止
|
||||
return;
|
||||
}
|
||||
animRaf = requestAnimationFrame(step);
|
||||
};
|
||||
animRaf = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
/** 命令式:平滑平移。先停惯性与旧动画,从当前位置重新开始 */
|
||||
const scrollBy = (options: ScrollByOptions) => {
|
||||
const { x = 0, y = 0, duration = 0, easing } = options;
|
||||
stopInertia();
|
||||
stopAnim();
|
||||
if (x === 0 && y === 0) return;
|
||||
animateBy(x, y, duration, resolveEasing(easing));
|
||||
};
|
||||
/** 命令式:停止当前程序化动画 */
|
||||
const stopScroll = () => stopAnim();
|
||||
|
||||
// 把句柄写入外部传入的 apiRef(effect 重建时会刷新闭包)
|
||||
if (apiRef) apiRef.current = { scrollBy, stopScroll };
|
||||
|
||||
// 鼠标按下
|
||||
const onDown = (e: PointerEvent) => {
|
||||
if (!draggableRef.current) return;
|
||||
stopInertia();
|
||||
stopAnim(); // 用户一按下即打断程序化动画,自然接管
|
||||
dragging = true;
|
||||
lastX = e.x;
|
||||
lastY = e.y;
|
||||
@@ -501,6 +569,8 @@ export function useInfiniteSlide<T>(
|
||||
layoutRef.current = undefined;
|
||||
ro.disconnect();
|
||||
stopInertia();
|
||||
stopAnim();
|
||||
if (apiRef) apiRef.current = null; // 卸载后句柄失效
|
||||
// 拖拽中销毁画布时,置静止并清掉残留速度避免触发惯性
|
||||
clearIdle();
|
||||
// 销毁前先取消可视度 rAF,避免在 leafer.destroy() 后触发回调访问已销毁的节点
|
||||
@@ -511,7 +581,7 @@ export function useInfiniteSlide<T>(
|
||||
leafer.off(PointerEvent.UP, onUp);
|
||||
leafer.destroy(); // 连带销毁 group 及全部子节点
|
||||
};
|
||||
}, [containerRef, items, background]);
|
||||
}, [containerRef, items, background, apiRef]);
|
||||
|
||||
// props 中的布局配置变化时(含响应式函数返回值变化),触发重新布局;
|
||||
// layout 内部按签名去重,无实际变化则零开销
|
||||
|
||||
Reference in New Issue
Block a user