You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Konva中实现矩形拖动自动横竖滚动(仿Trello效果)

嘿,这个需求我之前做过类似的,刚好可以给你分享一个贴近Trello交互的实现方案!核心思路是靠外层滚动容器+拖动时的边缘检测来实现,具体步骤如下:

实现方案拆解

1. 先搭好滚动基础结构

Konva的Stage本身不具备滚动能力,所以我们需要给它套一个外层的滚动容器。这个容器设置固定的可视范围,Stage的宽高要远大于容器,这样才能产生滚动空间:

import { useRef } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const ScrollableBoard = () => {
  // 用来操作滚动的容器ref
  const scrollContainerRef = useRef(null);
  // Stage的ref(可选,用来做一些坐标转换)
  const stageRef = useRef(null);

  return (
    {/* 外层滚动容器:固定视口,超出部分滚动 */}
    <div 
      ref={scrollContainerRef}
      style={{
        width: '100vw',
        height: '90vh',
        overflow: 'auto',
        // 隐藏滚动条,完全模仿Trello的无滚动条体验
        scrollbarWidth: 'none',
        msOverflowStyle: 'none'
      }}
    >
      {/* Stage的宽高要足够大,比如模拟横向长列表+纵向高度 */}
      <Stage ref={stageRef} width={3000} height={2000}>
        <Layer>
          {/* 可拖动的矩形元素 */}
          <Rect
            x={100}
            y={100}
            width={200}
            height={150}
            fill="#2563eb"
            draggable
            shadowBlur={5}
          />
          {/* 在右侧/底部添加更多元素,测试滚动 */}
          <Rect
            x={2700}
            y={1700}
            width={200}
            height={150}
            fill="#ef4444"
            draggable
            shadowBlur={5}
          />
        </Layer>
      </Stage>
    </div>
  );
};

export default ScrollableBoard;

注:上面的scrollbarWidth: 'none'是用来隐藏滚动条的,完全贴合Trello的无滚动条交互。

2. 实现拖动时的边缘触发滚动逻辑

接下来是核心:在拖动元素的过程中,实时检测鼠标位置,当靠近容器边缘时触发自动滚动。我们需要监听元素的onDragMove事件,结合requestAnimationFrame实现平滑滚动:

import { useRef, useState, useEffect } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const ScrollableBoard = () => {
  const scrollContainerRef = useRef(null);
  const stageRef = useRef(null);
  // 标记是否正在滚动,避免重复触发
  const [isScrolling, setIsScrolling] = useState(false);
  // 触发滚动的边缘阈值(比如距离边缘20px时开始滚动)
  const edgeThreshold = 20;
  // 基础滚动速度,可根据需求调整
  const baseScrollSpeed = 6;

  const handleDragMove = (e) => {
    const container = scrollContainerRef.current;
    if (!container) return;

    // 获取鼠标相对于浏览器视口的位置
    const mouseX = e.evt.clientX;
    const mouseY = e.evt.clientY;
    // 获取滚动容器的位置信息
    const containerRect = container.getBoundingClientRect();

    let scrollX = 0;
    let scrollY = 0;

    // 检测水平边缘:右侧/左侧
    if (mouseX >= containerRect.right - edgeThreshold) {
      scrollX = baseScrollSpeed;
    } else if (mouseX <= containerRect.left + edgeThreshold) {
      scrollX = -baseScrollSpeed;
    }

    // 检测垂直边缘:底部/顶部
    if (mouseY >= containerRect.bottom - edgeThreshold) {
      scrollY = baseScrollSpeed;
    } else if (mouseY <= containerRect.top + edgeThreshold) {
      scrollY = -baseScrollSpeed;
    }

    // 有滚动需求时开启滚动循环
    if (scrollX !== 0 || scrollY !== 0) {
      if (!isScrolling) setIsScrolling(true);
      // 执行一次滚动
      container.scrollLeft += scrollX;
      container.scrollTop += scrollY;
    } else {
      if (isScrolling) setIsScrolling(false);
    }
  };

  // 用requestAnimationFrame实现持续平滑滚动
  useEffect(() => {
    let animationId;
    const container = scrollContainerRef.current;

    const scrollLoop = () => {
      if (!isScrolling || !container) return;
      // 这里可以重复之前的边缘检测逻辑,保持持续滚动
      // (或者直接复用handleDragMove的逻辑,简化写法)
      animationId = requestAnimationFrame(scrollLoop);
    };

    if (isScrolling) {
      animationId = requestAnimationFrame(scrollLoop);
    }

    // 组件卸载时取消动画,防止内存泄漏
    return () => cancelAnimationFrame(animationId);
  }, [isScrolling]);

  return (
    <div 
      ref={scrollContainerRef}
      style={{
        width: '100vw',
        height: '90vh',
        overflow: 'auto',
        scrollbarWidth: 'none',
        msOverflowStyle: 'none'
      }}
    >
      <Stage ref={stageRef} width={3000} height={2000}>
        <Layer>
          <Rect
            x={100}
            y={100}
            width={200}
            height={150}
            fill="#2563eb"
            draggable
            onDragMove={handleDragMove}
            shadowBlur={5}
          />
          <Rect
            x={2700}
            y={1700}
            width={200}
            height={150}
            fill="#ef4444"
            draggable
            onDragMove={handleDragMove}
            shadowBlur={5}
          />
        </Layer>
      </Stage>
    </div>
  );
};

export default ScrollableBoard;

3. 优化体验的小技巧

  • 动态调整滚动速度:可以根据鼠标距离边缘的远近调整速度,越靠近边缘速度越快,比如:
    const distanceToEdge = containerRect.right - mouseX;
    scrollX = baseScrollSpeed + (edgeThreshold - distanceToEdge) * 2;
    
  • 节流处理:如果拖动时事件触发太频繁,可以给handleDragMove加节流,避免性能问题。
  • 坐标转换:如果需要更精准的元素位置判断,可以用Konva的stage.getPointerPosition()来获取相对于Stage的鼠标坐标。

内容的提问来源于stack exchange,提问作者Nidhi Dadiya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 21:18:13