feat: 添加交互状态插件,增加悬停和移动状态回调

This commit is contained in:
2026-06-27 15:38:10 +08:00
parent b7805a0966
commit 205f464473
6 changed files with 93 additions and 4 deletions
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@leafer-in/animate": "^2.1.8",
"@leafer-in/color": "^2.1.8",
"@leafer-in/state": "^2.1.8",
"@leafer-ui/core": "^2.1.8",
"@leafer-ui/draw": "^2.1.8",
"@tailwindcss/vite": "^4.3.1",
+16
View File
@@ -14,6 +14,9 @@ importers:
'@leafer-in/color':
specifier: ^2.1.8
version: 2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/draw@2.1.8)(@leafer-ui/interface@2.1.8)
'@leafer-in/state':
specifier: ^2.1.8
version: 2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/core@2.1.8)(@leafer-ui/interface@2.1.8)
'@leafer-ui/core':
specifier: ^2.1.8
version: 2.1.8
@@ -247,6 +250,13 @@ packages:
'@leafer-ui/interface': ^2.1.8
'@leafer/interface': ^2.1.8
'@leafer-in/state@2.1.8':
resolution: {integrity: sha512-YHz1dg3VaTG74y9/GWhbiJ9OsUjv1CQxMxCRABFYAY0bO8PqUDYw82Wk4eoVCYIHXK3olr9vnQpyvmjUMn7rTw==}
peerDependencies:
'@leafer-in/interface': ^2.1.8
'@leafer-ui/core': ^2.1.8
'@leafer-ui/interface': ^2.1.8
'@leafer-ui/app@2.1.8':
resolution: {integrity: sha512-oOxmObO1TEfm4ZVJP0k95BU42MuYh4Que8PkJs5LKxIgofqmS1kF5SGGkbGBM/5he8KGVzsYQJXPC2MiFkqJ9Q==}
@@ -1431,6 +1441,12 @@ snapshots:
'@leafer-ui/interface': 2.1.8
'@leafer/interface': 2.1.8
'@leafer-in/state@2.1.8(@leafer-in/interface@2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8))(@leafer-ui/core@2.1.8)(@leafer-ui/interface@2.1.8)':
dependencies:
'@leafer-in/interface': 2.1.8(@leafer-ui/interface@2.1.8)(@leafer/interface@2.1.8)
'@leafer-ui/core': 2.1.8
'@leafer-ui/interface': 2.1.8
'@leafer-ui/app@2.1.8':
dependencies:
'@leafer-ui/data': 2.1.8
+9
View File
@@ -71,6 +71,11 @@ function App() {
fill: item.color,
cornerRadius: 16,
opacity: entering ? 0 : 1,
around: "center",
hoverStyle: {
scale: 1.05,
opacity: 0.7
},
children: [
new Text({
width: size.width,
@@ -128,6 +133,10 @@ function App() {
draggable={ready}
background="#171717"
onCardClick={(item) => console.log("clicked", item)}
// onCardHover={(item, _index, hovering) =>
// console.log(hovering ? "hover in" : "hover out", item)
// }
onMovingChange={(moving) => console.log("moving", moving)}
onVisibleCardsChange={handleVisibleChange}
/>
{/* 实时展示可视卡片:可视度 + 左上角坐标 */}
+10
View File
@@ -61,6 +61,16 @@ export interface InfiniteCanvasProps<T> {
friction?: number;
/** 点击卡片回调 */
onCardClick?: (item: T, index: number) => void;
/**
* 悬停卡片回调:指针进入时 hovering=true,移出时 hovering=false
* 同一张卡片因无限平铺可能存在多个实例,进出各实例都会触发
*/
onCardHover?: (item: T, index: number, hovering: boolean) => void;
/**
* 移动状态回调:拖拽或惯性滑动开始时 moving=true,完全静止后 moving=false
* 仅在状态翻转时触发一次(拖拽接惯性视为持续移动,中途不会触发 false)
*/
onMovingChange?: (moving: boolean) => void;
/**
* 可视区域卡片变化回调:每当可视卡片集合或其可视度发生变化时触发
* 内部用 requestAnimationFrame 合帧、并对结果做去重,空闲时零开销
@@ -26,6 +26,8 @@ export function useInfiniteSlide<T>(
// 易变的回调/配置用 ref 存最新值,避免进入 effect 依赖导致画布重建
// 在 passive effect 中更新(不能在 render 期间写 ref
const onCardClickRef = useRef(props.onCardClick);
const onCardHoverRef = useRef(props.onCardHover);
const onMovingChangeRef = useRef(props.onMovingChange);
const onVisibleCardsChangeRef = useRef(props.onVisibleCardsChange);
const visibilityThresholdRef = useRef(props.visibilityThreshold ?? 0);
const draggableRef = useRef(draggable);
@@ -43,6 +45,8 @@ export function useInfiniteSlide<T>(
useEffect(() => {
onCardClickRef.current = props.onCardClick;
onCardHoverRef.current = props.onCardHover;
onMovingChangeRef.current = props.onMovingChange;
onVisibleCardsChangeRef.current = props.onVisibleCardsChange;
visibilityThresholdRef.current = props.visibilityThreshold ?? 0;
draggableRef.current = draggable;
@@ -222,6 +226,13 @@ export function useInfiniteSlide<T>(
node.on(PointerEvent.TAP, () =>
onCardClickRef.current?.(item, index)
);
// 指针进入/移出:用于 hover 回调(拖拽中也会触发,由使用方自行判断)
node.on(PointerEvent.ENTER, () =>
onCardHoverRef.current?.(item, index, true)
);
node.on(PointerEvent.LEAVE, () =>
onCardHoverRef.current?.(item, index, false)
);
leafer.add(node);
nodes.push(node);
meta.push({ item, index });
@@ -256,6 +267,37 @@ export function useInfiniteSlide<T>(
let vy = 0;
let raf = 0;
// 移动状态(拖拽 ∪ 惯性):仅在翻转时回调一次,拖拽接惯性不会中途回 false
let moving = false;
const setMoving = (next: boolean) => {
if (moving === next) return;
moving = next;
onMovingChangeRef.current?.(next);
};
// 按住但停住不动的空闲检测:超过 IDLE_MS 没有新的位移就判定静止
// (浏览器在指针不动时不再派发 move 事件,否则 moving 会一直卡在 true
const IDLE_MS = 150;
let idleTimer: ReturnType<typeof setTimeout> | 0 = 0;
const clearIdle = () => {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = 0;
}
};
const armIdle = () => {
clearIdle();
idleTimer = setTimeout(() => {
idleTimer = 0;
// 仍按着但已停住:置静止,并清掉残留速度避免松手误触发惯性
if (dragging) {
vx = 0;
vy = 0;
setMoving(false);
}
}, IDLE_MS);
};
// 停止惯性滑动(取消 requestAnimationFrame
const stopInertia = () => {
if (raf) {
@@ -272,6 +314,7 @@ export function useInfiniteSlide<T>(
vy *= frictionRef.current;
if (Math.abs(vx) < MIN && Math.abs(vy) < MIN) {
raf = 0;
setMoving(false); // 惯性自然衰减到静止
return;
}
shift(vx, vy);
@@ -290,6 +333,7 @@ export function useInfiniteSlide<T>(
vx = 0;
vy = 0;
container.style.cursor = "grabbing"; // 按下:抓握手势
// 注意:按下还未移动不算「移动中」,moving 留到 onMove 真正位移时再置 true
};
const onMove = (e: PointerEvent) => {
@@ -300,15 +344,22 @@ export function useInfiniteSlide<T>(
lastY = e.y;
vx = dx;
vy = dy;
if (dx !== 0 || dy !== 0) {
setMoving(true); // 真正产生位移才算移动中
armIdle(); // 重置空闲计时:停住不动到点后会判定静止
}
shift(dx, dy);
};
const onUp = () => {
if (!dragging) return;
dragging = false;
clearIdle(); // 松手即结束拖拽,停掉空闲计时
container.style.cursor = "grab"; // 松开:恢复可抓握手势
if (inertiaRef.current && (Math.abs(vx) > 0.5 || Math.abs(vy) > 0.5)) {
startInertia();
startInertia(); // 拖拽接惯性,保持 moving=true
} else {
setMoving(false); // 无惯性,松手即静止
}
};
@@ -325,6 +376,7 @@ export function useInfiniteSlide<T>(
layoutRef.current = undefined;
ro.disconnect();
stopInertia();
clearIdle();
if (visRaf) cancelAnimationFrame(visRaf);
leafer.off(PointerEvent.DOWN, onDown);
leafer.off(PointerEvent.MOVE, onMove);
+4 -3
View File
@@ -1,8 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
// Leafer 插件按副作用导入:把 animate()/颜色能力挂到节点上(仅安装不导入不生效)
import "@leafer-in/animate";
import "@leafer-in/color";
// Leafer 插件
import "@leafer-in/animate"; // 动画插件
import "@leafer-in/color"; // 颜色插件(提供 Color 类、渐变、调色板等)
import "@leafer-in/state"; // 交互状态插件
import "./index.css";
import App from "./App.tsx";