Flutter双容器卡片拖拽交互需求:重排与跨容器复制
可行,直接上实现思路和代码
这个需求完全可以通过reorderables库结合Flutter原生的Draggable/DragTarget实现,核心是把「内部重排」和「跨容器拖拽」的逻辑拆分处理:
核心实现步骤
1. 状态管理基础
用StatefulWidget维护两个独立的数据源:
leftCards:左侧卡片库的原始数据(拖拽后不删除,仅支持内部重排)rightCards:右侧列表的动态数据(支持重排和新增)
2. 左侧卡片库(Wrap流式+拖拽重排+可拖拽复制)
用ReorderableWrap实现内部重排,每个卡片嵌套Draggable:
child绑定原卡片(保持原位不消失)feedback绑定卡片副本(拖拽时跟随鼠标/手指的元素)- 不设置
childWhenDragging,避免原卡片隐藏
3. 右侧列表(Column+拖拽重排+接收拖拽+插入指示器)
用ReorderableColumn实现内部重排,同时在每个卡片的上下位置插入DragTarget,用来检测拖拽位置并显示插入指示器:
DragTarget的builder根据是否有拖拽进入,显示高亮的插入线onAccept回调中把拖拽的卡片数据插入到对应位置
完整代码示例
import 'package:flutter/material.dart'; import 'package:reorderables/reorderables.dart'; class CardDragDemo extends StatefulWidget { const CardDragDemo({super.key}); @override State<CardDragDemo> createState() => _CardDragDemoState(); } class _CardDragDemoState extends State<CardDragDemo> { // 左侧卡片库数据(拖拽后不删除) final List<String> _leftCards = List.generate(8, (index) => '卡片${index+1}'); // 右侧列表数据 List<String> _rightCards = []; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('卡片拖拽演示')), body: Row( children: [ // 左侧卡片库容器 Expanded( child: Padding( padding: const EdgeInsets.all(16), child: ReorderableWrap( spacing: 12, runSpacing: 12, onReorder: (oldIndex, newIndex) { setState(() { final item = _leftCards.removeAt(oldIndex); _leftCards.insert(newIndex, item); }); }, children: _leftCards.map((cardText) { return Draggable<String>( key: ValueKey(cardText), data: cardText, // 原卡片保持原位 child: _buildCard(cardText), // 拖拽时显示的副本 feedback: Material( elevation: 4, child: _buildCard(cardText), ), ); }).toList(), ), ), ), // 右侧列表容器 Expanded( child: Padding( padding: const EdgeInsets.all(16), child: _buildRightColumn(), ), ), ], ), ); } Widget _buildRightColumn() { // 构建带插入指示器的右侧列表:每个卡片上下都加DragTarget List<Widget> children = []; // 顶部插入位置 children.add(_buildInsertTarget(0)); for (int i = 0; i < _rightCards.length; i++) { children.add(ReorderableItem( key: ValueKey(_rightCards[i]), childBuilder: (context, dragState) { return _buildCard(_rightCards[i]); }, )); // 卡片之间的插入位置 children.add(_buildInsertTarget(i+1)); } return ReorderableColumn( onReorder: (oldIndex, newIndex) { setState(() { // 因为每个卡片前后都有DragTarget,所以实际卡片的索引是奇数位 final actualOldIndex = (oldIndex - 1) ~/ 2; final actualNewIndex = (newIndex - 1) ~/ 2; if (actualOldIndex == actualNewIndex) return; final item = _rightCards.removeAt(actualOldIndex); _rightCards.insert(actualNewIndex, item); }); }, children: children, ); } // 插入位置的DragTarget(显示指示器) Widget _buildInsertTarget(int insertIndex) { return DragTarget<String>( builder: (context, candidateData, rejectedData) { bool isDraggingOver = candidateData.isNotEmpty; return Container( height: isDraggingOver ? 4 : 1, margin: const EdgeInsets.symmetric(vertical: 8), color: isDraggingOver ? Colors.blueAccent : Colors.transparent, ); }, onAccept: (data) { setState(() { _rightCards.insert(insertIndex, data); }); }, ); } // 通用卡片组件 Widget _buildCard(String text) { return Card( elevation: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Text(text, style: const TextStyle(fontSize: 16)), ), ); } }
关键细节说明
- 左侧拖拽保持原位:通过
Draggable的child和feedback分离实现,原卡片始终保留在左侧列表中 - 插入指示器:在右侧列表的每个可能插入位置(顶部、卡片之间、底部)添加
DragTarget,拖拽进入时显示高亮线 - 重排逻辑适配:右侧列表因为插入了
DragTarget占位,所以ReorderableColumn的onReorder需要计算实际卡片的索引(奇数位) - 状态更新:所有列表操作都要放在
setState中,确保UI实时刷新
内容的提问来源于stack exchange,提问作者Otziii
相关产品推荐
相关产品推荐

