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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 23:38:30