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

如何使用Interact.js实现多个可拖拽元素的同时独立滚动

如何使用Interact.js实现多个可拖拽元素的同时独立滚动

嘿,我明白你在这个多触摸应用里遇到的麻烦了——当一个用户滚动某个方块的内容时,另一个用户滚动另一个方块会打断前者的滚动,没法同时独立操作,对吧?别担心,咱们一步步来解决这个问题。

问题根源分析

你当前的实现有几个关键问题导致了这个现象:

  • 用了全局的scrolling变量来跟踪滚动状态,新的触摸会覆盖这个全局状态,导致旧的滚动被中断。
  • 滚动时用interact(element).unset()禁用了整个元素的交互,这会把拖拽、手势等所有操作都关掉,而且没法区分不同的触摸点。
  • iframe的pointer-events切换逻辑是全局的,同一时间只能有一个iframe能响应触摸,限制了并行操作。
  • 没有针对每个触摸点(通过touch.identifier)单独处理状态,多触摸场景下无法区分不同用户/手指的操作。

解决方案步骤

咱们要做的就是让每个触摸点对应独立的元素状态,同时保留Interact.js的拖拽和手势能力,让多个方块可以同时处理各自的滚动和交互。

1. 修改CSS配置

首先调整CSS,去掉那些限制并行交互的设置,让每个方块和iframe能独立响应触摸:

/* Remove body margins and center content vertically and horizontally */
body, html {
    background-color: #3a3a3a;
    margin: 0;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

/* Define the boundary container for draggable squares */
#boundary {
    width: 1920px;
    height: 1080px;
    border: 2px solid #333;
    position: relative;
}

/* Style for draggable squares */
.square {
    width: 480px;
    height: 1080px;
    position: absolute;
    user-select: none;
    transform-origin: center;
    background-color: white;
    overflow: hidden;
    z-index: 500;
    /* 允许每个元素独立处理触摸行为,不要全局禁用 */
    touch-action: manipulation;
}

/* Content inside each square (scrollable if necessary) */
.square-content {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    /* 确保内容区域可以独立滚动 */
    touch-action: pan-y;
}

/* Make iframes fit square dimensions and remove borders */
.square iframe {
    width: 100%;
    height: 100%;
    border: none;
    /* 始终允许iframe响应触摸,不要动态切换 */
    pointer-events: auto;
}

/* Individual styling for each square with unique colors and positions */
#square1 {
    border: 2px solid red;
    left: 50px;
    top: 50px;
    border-top: 30px solid #FF7F7F;
}
#square2 {
    border: 2px solid red;
    left: 300px;
    top: 50px;
    border-top: 30px solid #ADD8E6;
}
#square3 {
    border: 2px solid red;
    left: 50px;
    top: 300px;
    border-top: 30px solid #90EE90;
}
#square4 {
    border: 2px solid blue;
    left: 50px;
    top: 50px;
    border-top: 30px solid #ADD8E6;
}

/* Close button styling */
.close-btn {
    position: absolute;
    top: 5px;
    right: 5px;
    background: white;
    border: 1px solid #333;
    border-radius: 50%;
    width: 20px;
    height: 20px;
    line-height: 18px;
    text-align: center;
    cursor: pointer;
    font-family: Arial, sans-serif;
    font-size: 12px;
    z-index: 500;
    touch-action: none;
}

/* Styling for the show buttons container */
.show-buttons {
    position: absolute;
    top: 0%;             /* Position from the top */
    left: 50%;             /* Center horizontally */
    transform: translateX(-50%); /* Adjust to truly center */
    z-index: 1;
    display: flex;         /* Use flexbox for alignment */
}

/* Styling for each show button */
.show-btn {
    width: 50px;           /* Set a fixed width */
    height: 50px;          /* Set a fixed height */
    margin: 5px;           /* Space between circles */
    border-radius: 50%;    /* Make it circular */
    cursor: pointer;        /* Pointer cursor on hover */
    touch-action: none;     /* Disable touch actions */
}

/* Specific styles for each show button */
#circle1 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: red; 
}
#circle2 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: blue; 
}
#circle3 {
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: green; 
}
#circle4 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: yellow; 
}

/* Hide buttons by default */
.hidden {
    display: none;
}

2. 修改JavaScript逻辑

接下来重写JS代码,核心是跟踪每个触摸点的状态,让多个触摸可以独立处理各自的元素:

$(document).ready(function () {
    // Object to store transformation states (position, scale, rotation) for each square
    const transformStates = {};
    // Boundary element where squares are confined
    const boundary = document.getElementById('boundary');
    // Track highest z-index dynamically
    let highestZIndex = 1000;

    // 关键:用对象跟踪每个触摸点对应的滚动元素和初始位置,支持多触摸并行
    const activeTouches = {};

    // Prevent default behavior for touch events on buttons to avoid accidental gestures
    $('.show-btn, .close-btn').on('touchstart touchmove touchend', function (e) {
        e.preventDefault();
    });

    // Disable dragging for the show-buttons
    interact('.show-btn').unset();
    $('.show-btn').on('mousedown touchstart', function (e) {
        e.stopPropagation();
    });

    // Function to set up interactions for draggable and gesturable elements
    function setupInteractions(elementId) {
        const element = document.getElementById(elementId);
        // 初始化元素的变换状态
        if (!transformStates[elementId]) {
            transformStates[elementId] = { x: 0, y: 0, scale: 1, rotate: 0 };
        }

        interact(element)
            .draggable({
                inertia: true,
                modifiers: [
                    interact.modifiers.restrict({
                        restriction: boundary,
                        elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
                        endOnly: true
                    })
                ],
                // 拖拽时只处理非滚动的触摸点
                allowFrom: '.square-header',
                onstart: function (event) {
                    // 拖拽时提升层级,避免被遮挡
                    element.style.zIndex = ++highestZIndex;
                    // 标记当前触摸点为拖拽状态,避免和滚动冲突
                    activeTouches[event.pointerId] = { type: 'drag', element: element };
                },
                onmove: function (event) {
                    const state = transformStates[elementId];
                    state.x += event.dx;
                    state.y += event.dy;
                    element.style.transform = `translate(${state.x}px, ${state.y}px) scale(${state.scale}) rotate(${state.rotate}deg)`;
                },
                onend: function (event) {
                    delete activeTouches[event.pointerId];
                }
            })
            .gesturable({
                inertia: true,
                onstart: function (event) {
                    element.style.zIndex = ++highestZIndex;
                    activeTouches[event.pointerId] = { type: 'gesture', element: element };
                },
                onmove: function (event) {
                    const state = transformStates[elementId];
                    const newScale = state.scale * event.ds;
                    // 限制缩放范围,避免过小或过大
                    state.scale = Math.max(0.5, Math.min(2, newScale));
                    state.rotate += event.da;
                    element.style.transform = `translate(${state.x}px, ${state.y}px) scale(${state.scale}) rotate(${state.rotate}deg)`;
                },
                onend: function (event) {
                    delete activeTouches[event.pointerId];
                }
            });
    }

    // 为每个方块设置交互
    ['square1', 'square2', 'square3', 'square4'].forEach(setupInteractions);

    // 处理方块内容的触摸滚动,支持多触摸并行
    $('.square-content').on('touchstart', function (e) {
        const touch = e.originalEvent.touches[0];
        const contentEl = this;
        // 记录当前触摸点的初始位置和对应的内容元素
        activeTouches[touch.identifier] = {
            type: 'scroll',
            element: contentEl,
            startY: touch.clientY,
            startScrollTop: contentEl.scrollTop
        };
        // 阻止事件冒泡,避免触发拖拽
        e.stopPropagation();
    });

    $('.square-content').on('touchmove', function (e) {
        const touch = e.originalEvent.touches[0];
        const touchData = activeTouches[touch.identifier];
        if (!touchData || touchData.type !== 'scroll' || touchData.element !== this) return;

        // 计算滚动距离
        const deltaY = touch.clientY - touchData.startY;
        touchData.element.scrollTop = touchData.startScrollTop - deltaY;
        // 阻止默认行为,避免页面整体滚动
        e.preventDefault();
        // 阻止冒泡,避免干扰拖拽
        e.stopPropagation();
    });

    $('.square-content').on('touchend touchcancel', function (e) {
        const touch = e.originalEvent.changedTouches[0];
        delete activeTouches[touch.identifier];
    });

    // 显示/隐藏方块逻辑
    $('.show-btn').click(function () {
        const squareId = $(this).attr('id').replace('circle', 'square');
        $(`#${squareId}`).toggleClass('hidden');
        // 显示时提升层级
        if (!$(#${squareId}).hasClass('hidden')) {
            $(`#${squareId}`).css('z-index', ++highestZIndex);
        }
    });

    // 关闭按钮逻辑
    $('.close-btn').click(function () {
        $(this).closest('.square').addClass('hidden');
    });
});

关键修改说明

  1. 多触摸状态跟踪:用activeTouches对象记录每个触摸点(通过pointerId或touch.identifier)的类型(滚动/拖拽/手势)和对应的元素,这样不同的触摸点可以独立处理,不会互相覆盖。
  2. Interact.js交互优化:
    • 拖拽的allowFrom限制为.square-header,这样点击头部才会拖拽,内容区域的触摸不会触发拖拽,减少冲突。
    • 不再用unset()禁用交互,而是通过事件冒泡阻止和触摸状态跟踪来区分操作。
  3. 滚动逻辑独立:每个方块的内容区域单独处理自己的触摸事件,阻止事件冒泡和默认行为,确保滚动只作用于当前方块。
  4. iframe事件控制:让iframe始终保持pointer-events: auto,不再动态切换,这样每个iframe都能独立响应触摸。

这样修改后,多个用户(或多个手指)就可以同时滚动不同的方块,各自的操作不会互相打断,同时还保留了拖拽和手势的功能。

备注:内容来源于stack exchange,提问作者Sahid Molla

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 03:38:02