如何使用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'); }); });
关键修改说明
- 多触摸状态跟踪:用
activeTouches对象记录每个触摸点(通过pointerId或touch.identifier)的类型(滚动/拖拽/手势)和对应的元素,这样不同的触摸点可以独立处理,不会互相覆盖。 - Interact.js交互优化:
- 拖拽的
allowFrom限制为.square-header,这样点击头部才会拖拽,内容区域的触摸不会触发拖拽,减少冲突。 - 不再用
unset()禁用交互,而是通过事件冒泡阻止和触摸状态跟踪来区分操作。
- 拖拽的
- 滚动逻辑独立:每个方块的内容区域单独处理自己的触摸事件,阻止事件冒泡和默认行为,确保滚动只作用于当前方块。
- iframe事件控制:让iframe始终保持
pointer-events: auto,不再动态切换,这样每个iframe都能独立响应触摸。
这样修改后,多个用户(或多个手指)就可以同时滚动不同的方块,各自的操作不会互相打断,同时还保留了拖拽和手势的功能。
备注:内容来源于stack exchange,提问作者Sahid Molla
相关产品推荐
相关产品推荐

