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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 12:18:16