如何在Flutter中实现可滑动洗牌的卡片堆叠组件?
实现可滑动切换的卡片堆叠组件(支持前后端移动)
下面是一套可直接复用的实现方案,核心是监听水平滑动事件判断方向,通过DOM操作调整卡片顺序,配合CSS过渡实现平滑的堆叠动画效果。
核心逻辑
- 卡片初始以堆叠形式排列,仅最上层卡片可触发滑动交互
- 左滑(从右向左拖动):将当前卡片移至堆叠末尾
- 右滑(从左向右拖动):将当前卡片移至堆叠最前端
- 所有状态切换均通过CSS过渡实现平滑动画
HTML结构
先搭建基础的卡片容器和卡片元素:
<div class="card-stack-container"> <div class="card">卡片 1</div> <div class="card">卡片 2</div> <div class="card">卡片 3</div> <div class="card">卡片 4</div> </div>
CSS样式
实现卡片堆叠效果、滑动反馈及过渡动画:
.card-stack-container { position: relative; width: 300px; height: 400px; margin: 2rem auto; overflow: hidden; } .card { position: absolute; width: 100%; height: 100%; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 2rem; cursor: grab; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* 初始堆叠偏移 */ .card:nth-child(1) { background: #ff6b6b; z-index: 4; top: 0; left: 0; } .card:nth-child(2) { background: #4ecdc4; z-index: 3; top: 10px; left: 10px; } .card:nth-child(3) { background: #45b7d1; z-index: 2; top: 20px; left: 20px; } .card:nth-child(4) { background: #96ceb4; z-index: 1; top: 30px; left: 30px; } /* 拖动中的卡片状态 */ .card.dragging { cursor: grabbing; transition: none; /* 拖动时取消过渡,保证跟随流畅 */ }
JavaScript交互逻辑
处理滑动事件、判断方向并更新卡片顺序:
const container = document.querySelector('.card-stack-container'); const cards = Array.from(document.querySelectorAll('.card')); let startX = 0; let currentX = 0; let isDragging = false; let activeCard = null; // 初始化事件监听 function initEvents() { // 鼠标事件 container.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', endDrag); // 触摸事件(适配移动端) container.addEventListener('touchstart', startDrag); document.addEventListener('touchmove', drag); document.addEventListener('touchend', endDrag); } // 开始拖动 function startDrag(e) { // 仅允许拖动最上层卡片 activeCard = e.target.closest('.card'); if (parseInt(activeCard.style.zIndex) !== cards.length) return; isDragging = true; startX = getClientX(e); activeCard.classList.add('dragging'); } // 拖动过程 function drag(e) { if (!isDragging || !activeCard) return; currentX = getClientX(e); const diffX = currentX - startX; // 让卡片跟随拖动方向移动 activeCard.style.transform = `translateX(${diffX}px)`; } // 结束拖动 function endDrag() { if (!isDragging || !activeCard) return; isDragging = false; const diffX = currentX - startX; const threshold = 50; // 滑动阈值,避免误触 // 判断滑动方向并调整卡片位置 if (Math.abs(diffX) > threshold) { diffX < 0 ? container.appendChild(activeCard) // 左滑移至末尾 : container.prepend(activeCard); // 右滑移至最前 } // 重置卡片样式 activeCard.classList.remove('dragging'); activeCard.style.transform = ''; // 更新所有卡片的堆叠样式 updateCardStackStyles(); } // 兼容鼠标/触摸事件的X轴坐标获取 function getClientX(e) { return e.type.includes('touch') ? e.touches[0].clientX : e.clientX; } // 更新卡片的层级和偏移量,维持堆叠效果 function updateCardStackStyles() { const updatedCards = Array.from(container.querySelectorAll('.card')); updatedCards.forEach((card, index) => { const zIndex = updatedCards.length - index; const offset = index * 10; card.style.zIndex = zIndex; card.style.top = `${offset}px`; card.style.left = `${offset}px`; }); } // 初始化组件 initEvents(); updateCardStackStyles();
关键细节说明
- 滑动阈值:设置
threshold = 50px,只有滑动距离超过该值才会触发卡片切换,有效避免误触 - 移动端适配:同时监听鼠标和触摸事件,保证在PC和移动端都能正常交互
- 动画优化:拖动时取消CSS过渡,保证卡片跟随鼠标/触摸的流畅性;结束拖动后恢复过渡,让堆叠调整更平滑
- 层级控制:每次切换后重新计算所有卡片的
z-index,确保最上层卡片始终可交互
内容的提问来源于stack exchange,提问作者Sekar
相关产品推荐
相关产品推荐

